diff --git a/.gitignore b/.gitignore index ec55cbe2..bcd89d60 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,4 @@ npm-debug.log shinobi.sqlite dist ._* +generatedLanguageFiles diff --git a/COPYING.md b/COPYING.md index 956571a3..0cf4376c 100644 --- a/COPYING.md +++ b/COPYING.md @@ -3,95 +3,119 @@ SHINOBI OPEN SOURCE SOFTWARE LICENSE AGREEMENT Version 1, 04 June 2018 Copyright (C) 2018 [Shinobi Systems](https://shinobi.systems) - + *We'll try to keep it simple. Thanks for using Shinobi Software!* - -Defintions. + +Definitions. ----------- -In this license, which also serves as a general End User License Agreement [EULA], the following +In this license, which also serves as a general End User License Agreement [EULA], the following terms shall be interpreted by these definitions: * "EULA" shall mean this End User Licence Agreement * "Licensor" shall mean SHINOBI SYSTEMS * "Licensee" shall mean YOU, or the organisation (if any) on whose behalf YOU are taking the EULA. -"SOFTWARE PRODUCTS" or "SOFTWARE" or "PRODUCTS" shall mean the Software Product this License is -included with and any additional modules or add-ons delivered by Shinobi Systems. The term +"SOFTWARE PRODUCTS" or "SOFTWARE" or "PRODUCTS" shall mean the Software Product this License is +included with and any additional modules or add-ons delivered by Shinobi Systems. The term "SOFTWARE" includes, to the extent provided by SHINOBI SYSTEMS: 1) any revisions, updates and/or upgrades thereto; - 2) any data, image or executable files, databases, data engines, computer software, or similar + 2) any data, image or executable files, databases, data engines, computer software, or similar items customarily used or distributed with computer software products; 3) anything in any form whatsoever intended to be used with or in conjunction with the SOFTWARE; - 4) any associated media, documentation (including physical, electronic and on-line) and printed + 4) any associated media, documentation (including physical, electronic and on-line) and printed materials (the "Documentation"). Purpose of the Agreement. ------------------------- **The Short**: Protect the rights of this Software Product and the LICENSOR. -**The Long**: The LICENSOR grants the LICENSEE a non-exclusive, non-transferable and perpetual -licence to use the SOFTWARE PRODUCTS listed therein and under the terms thereof. By accepting -the terms and conditions established in this agreement, the LICENSEE does not acquire any -ownership of copyright or other intellectual property rights in any part of the SOFTWARE -PRODUCTS. The LICENSEE is only entitled to use the SOFTWARE PRODUCTS in accordance with the -terms and conditions set forth by Shinobi Systems. By using the SOFTWARE PRODUCTS, the +**The Long**: The LICENSOR grants the LICENSEE a non-exclusive, non-transferable and perpetual +licence to use the SOFTWARE PRODUCTS listed therein and under the terms thereof. By accepting +the terms and conditions established in this agreement, the LICENSEE does not acquire any +ownership of copyright or other intellectual property rights in any part of the SOFTWARE +PRODUCTS. The LICENSEE is only entitled to use the SOFTWARE PRODUCTS in accordance with the +terms and conditions set forth by Shinobi Systems. By using the SOFTWARE PRODUCTS, the LICENSEE agrees to accept the terms and conditions presented. -LICENSEE must purchase the applicable subscription in any other use case unless otherwise -granted. If the use case does not have a subscription applicable please contact a +LICENSEE must purchase the applicable subscription in any other use case unless otherwise +granted. If the use case does not have a subscription applicable please contact a representative at support@shinobi.systems. #### Commercial Uses - Selling usage of the software +- Selling hardware with the software on it - Using the software in locations that engage in the buying and selling of goods and services +As of 2022-07-12 the noted situations below are seen the same as "Commercial Use". + +- 25 Active Monitor Rule : Having at least 25 Active Monitors and not a Primary School or Secondary School. Does not apply to Personal Use. +- 150 Active Monitor Rule : If you have more than 150 Active Monitors please contact support for an Enterprise License. The retail Shinobi Pro license will not be applicable for these installations. +- Used on a Device that was part of a commercial transaction. This can be, but not limited to, being sold or provided additionally to a sale. + #### Conditions for Free (Unpaid) use. +If you fall under any conditions for "Commercial Use" none of the following can apply unless otherwise noted. + +- Personal use - Use in a non commercial area - Used for non commercial purposes - When used for research or educational purposes - Testing Purposes -- Usage by Educational institutions -- Use for Emergency Services and facilties associated like Search and Rescue Services or -Ambulance Services -- Use in Health Care facility like a hospital or walk-in clinic +- Usage by Primary Education or Secondary Education Schools +- Use for Emergency Services and facilities associated like Search and Rescue Services or +Ambulance Services with less than 50 cameras +- Use in Health Care facility like a hospital or walk-in clinic with less than 100 cameras +- Schools and Organizations that have been given exemption + +As of 2022-07-12 If you are an organization that falls under one or more of these conditions for free use then you must display that you are using Shinobi Systems software as "shinobi.video". This can be physically on premise or on your organization's public web page. + +Falling under the conditions for Free use does not guarantee you a License Key. It simply means you are allowed to use it without paying or trading for that use. + +#### Unsure if your use is commercial or non-commercial + +Please contact us through the Live Chat of our website and we can negotiate or make exceptions based on the circumstances of your use case. + +#### Registration + +Schools and Resellers must register with Shinobi Systems at https://licenses.shinobi.video. +In your account please fill out the "Apply for a School License" form or "Apply to be a Reseller" form. #### Support Services. -The Maintenance and Support Service shall be contracted and provided as per selected plan +The Maintenance and Support Service shall be contracted and provided as per selected plan agreement, taxes will be included in all prices for Support Services. Support Services will only provide support services as per the selected agreement. -This is not the entire agreement on support services. You must also review all agreements +This is not the entire agreement on support services. You must also review all agreements provided with subscription plans provided. #### Software Product Ownership. -This software is property of Shinobi Systems. LICENSEE must keep all copyright notices +This software is property of Shinobi Systems. LICENSEE must keep all copyright notices unchanged. #### Modification of this Software Product. -LICENSEE may modify code for personal use but must provide these changes upon request from -Shinobi Systems or an authorized Shinobi representative. LICENSEE may not alter or change -copyright notices. All code changes by LICENSEE shall fall under the copyright of Shinobi +LICENSEE may modify code for but must provide these changes upon request from +Shinobi Systems or an authorized Shinobi representative. LICENSEE may not alter or change +copyright notices. All code changes by LICENSEE shall fall under the copyright of Shinobi Systems in the case code modified by LICENSEE is integrated into the official Shinobi code base. #### Software Product Rebranding or "White-Labelling". -LICENSEE can remove the Shinobi branding from the front end but all copyright notices must +LICENSEE can remove the Shinobi branding from the front end but all copyright notices must remain unchanged. #### Software Product Contributions. -All contributed code becomes the property of Shinobi Systems. All contributors give permission +All contributed code becomes the property of Shinobi Systems. All contributors give permission to Shinobi and Shinobi developers to use the code however it is seen fit. #### Disclaimer of Warranty. -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN -OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM -"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK -AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM +"AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK +AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. #### Changes to the Agreement. -Shinobi Systems reserves the right to change the license and set of terms at any time. -Continued use is agreement to those possible changes. Changes to this license will be provided +Shinobi Systems reserves the right to change the license and set of terms at any time. +Continued use is agreement to those possible changes. Changes to this license will be provided in the commit history of the repository it is located in. #### Legal Proceedings. @@ -100,31 +124,18 @@ All lawsuits must be filed at the Vancouver Court House. Courthouse Vancouver Robson Square 800 Hornby St, Vancouver, BC V6Z 2C5 -#### List of Included Software +#### List of Included Software - This list is completed to best of our knowledge. + This list is completed to the best of our knowledge. Node.js - https://nodejs.org/en/ MariaDB - https://mariadb.org/ FFmpeg - https://www.ffmpeg.org/ - request - https://www.npmjs.com/package/request - Express (npm) - https://expressjs.com/ https://www.npmjs.com/package/express - EJS (npm) - http://ejs.co/ https://www.npmjs.com/package/ejs - pam-diff (npm) (Motion Detector) - https://github.com/kevinGodell/pam-diff - pipe2pam (npm) (for pam-diff) - https://github.com/kevinGodell/pipe2pam - mp4frag (npm) (Poseidon's main engine) - https://github.com/kevinGodell/mp4frag + pam-diff (Motion Detector) - https://github.com/kevinGodell/pam-diff + pipe2pam (for pam-diff) - https://github.com/kevinGodell/pipe2pam + mp4frag (Poseidon's main engine) - https://github.com/kevinGodell/mp4frag mse-live-player (for mp4frag) - https://github.com/kevinGodell/mse-live-player - pipe2jpeg (npm) - https://github.com/kevinGodell/pipe2jpeg - webdav (npm) - https://www.npmjs.com/package/webdav - jsonfile (npm) - https://www.npmjs.com/package/jsonfile - connectionTester (npm) - https://www.npmjs.com/package/connectionTester - node-onvif (npm) - https://www.npmjs.com/package/node-onvif - knex (npm) - https://www.npmjs.com/package/knex - nodemailer (npm) - https://www.npmjs.com/package/nodemailer - mysql (npm) - https://www.npmjs.com/package/mysql - sqlite3 (npm) - https://www.npmjs.com/package/sqlite3 - ldapauth-fork (npm) - https://www.npmjs.com/package/ldapauth-fork - http-proxy (npm) - https://www.npmjs.com/package/http-proxy + pipe2jpeg - https://github.com/kevinGodell/pipe2jpeg hls.js - https://github.com/video-dev/hls.js/ flv.js - https://github.com/Bilibili/flv.js/ Material Design Lite - https://getmdl.io/ @@ -142,3 +153,50 @@ Courthouse Vancouver Robson Square Moment.js - https://momentjs.com/ Livestamp.js - https://mattbradley.github.io/livestampjs/ Lodash - https://lodash.com/ + + **npmjs.com packages** + + async - https://www.npmjs.com/package/async + aws-sdk - https://www.npmjs.com/package/aws-sdk + backblaze-b2 - https://www.npmjs.com/package/backblaze-b2 + body-parser - https://www.npmjs.com/package/body-parser + bson - https://www.npmjs.com/package/bson + connection-tester - https://www.npmjs.com/package/connection-tester + cws - https://www.npmjs.com/package/cws + digest-fetch - https://www.npmjs.com/package/digest-fetch + discord.js - https://www.npmjs.com/package/discord.js + ejs - https://www.npmjs.com/package/ejs + express - https://www.npmjs.com/package/express + express-fileupload - https://www.npmjs.com/package/express-fileupload + fs-extra - https://www.npmjs.com/package/fs-extra + ftp-srv - https://www.npmjs.com/package/ftp-srv + googleapis - https://www.npmjs.com/package/googleapis + http-proxy - https://www.npmjs.com/package/http-proxy + jsonfile - https://www.npmjs.com/package/jsonfile + knex - https://www.npmjs.com/package/knex + ldapauth-fork - https://www.npmjs.com/package/ldapauth-fork + moment - https://www.npmjs.com/package/moment + mp4frag - https://www.npmjs.com/package/mp4frag + mqtt - https://www.npmjs.com/package/mqtt + mysql - https://www.npmjs.com/package/mysql + node-abort-controller - https://www.npmjs.com/package/node-abort-controller + node-fetch - https://www.npmjs.com/package/node-fetch + node-onvif-events - https://www.npmjs.com/package/node-onvif-events + node-ssh - https://www.npmjs.com/package/node-ssh + node-telegram-bot-api - https://www.npmjs.com/package/node-telegram-bot-api + nodemailer - https://www.npmjs.com/package/nodemailer + pam-diff - https://www.npmjs.com/package/pam-diff + path - https://www.npmjs.com/package/path + pipe2pam - https://www.npmjs.com/package/pipe2pam + pixel-change - https://www.npmjs.com/package/pixel-change + pushover-notifications - https://www.npmjs.com/package/pushover-notifications + sat - https://www.npmjs.com/package/sat + shinobi-onvif - https://www.npmjs.com/package/shinobi-onvif + shinobi-sound-detection - https://www.npmjs.com/package/shinobi-sound-detection + shinobi-zwave - https://www.npmjs.com/package/shinobi-zwave + smtp-server - https://www.npmjs.com/package/smtp-server + socket.io - https://www.npmjs.com/package/socket.io + socket.io-client - https://www.npmjs.com/package/socket.io-client + tree-kill - https://www.npmjs.com/package/tree-kill + unzipper - https://www.npmjs.com/package/unzipper + webdav-fs - https://www.npmjs.com/package/webdav-fs diff --git a/Docker/README.md b/Docker/README.md index 519daa14..8dd44bb5 100644 --- a/Docker/README.md +++ b/Docker/README.md @@ -4,7 +4,11 @@ ## Docker Ninja Way -> This method uses `docker-compose` and has the ability to quick install the TensorFlow Object Detection plugin. +> This method uses `docker-compose` and has the ability to quick install the TensorFlow Object Detection plugin. This will build your container from the images hosted on Gitlab. + +> **We no longer use Docker Hub** and will not in the foreseeable future. + +Docker Image Used : `registry.gitlab.com/shinobi-systems/shinobi:dev` ``` bash <(curl -s https://gitlab.com/Shinobi-Systems/Shinobi-Installer/raw/master/shinobi-docker.sh) @@ -17,10 +21,11 @@ bash <(curl -s https://gitlab.com/Shinobi-Systems/Shinobi-Installer/raw/master/s > Please remember to check out the Environment Variables table further down this README. ``` -docker run -d --name='Shinobi' -p '8080:8080/tcp' -v "/dev/shm/Shinobi/streams":'/dev/shm/streams':'rw' -v "$HOME/Shinobi/config":'/config':'rw' -v "$HOME/Shinobi/customAutoLoad":'/home/Shinobi/libs/customAutoLoad':'rw' -v "$HOME/Shinobi/database":'/var/lib/mysql':'rw' -v "$HOME/Shinobi/videos":'/home/Shinobi/videos':'rw' -v "$HOME/Shinobi/plugins":'/home/Shinobi/plugins':'rw' -v '/etc/localtime':'/etc/localtime':'ro' shinobisystems/shinobi:dev +docker run -d --name='Shinobi' -p '8080:8080/tcp' -v "/dev/shm/Shinobi/streams":'/dev/shm/streams':'rw' -v "$HOME/Shinobi/config":'/config':'rw' -v "$HOME/Shinobi/customAutoLoad":'/home/Shinobi/libs/customAutoLoad':'rw' -v "$HOME/Shinobi/database":'/var/lib/mysql':'rw' -v "$HOME/Shinobi/videos":'/home/Shinobi/videos':'rw' -v "$HOME/Shinobi/plugins":'/home/Shinobi/plugins':'rw' -v '/etc/localtime':'/etc/localtime':'ro' registry.gitlab.com/shinobi-systems/shinobi:dev ``` #### Installing Object Detection (TensorFlow.js) +**DEPRECATED, UPDATED IMAGE COMING SOON** > This requires that you add the plugin key to the Shinobi container. This key is generated and displayed in the startup logs of the Object Detection docker container. diff --git a/INSTALL/CentOS - Quick Install.sh b/INSTALL/CentOS - Quick Install.sh index 614de9b9..053aeaa6 100644 --- a/INSTALL/CentOS - Quick Install.sh +++ b/INSTALL/CentOS - Quick Install.sh @@ -85,7 +85,7 @@ echo "=========================================================" #Check if Node.js is installed if ! [ -x "$(command -v node)" ]; then echo "Node.js not found, installing..." - sudo curl --silent --location https://rpm.nodesource.com/setup_12.x | sudo bash - + sudo curl --silent --location https://rpm.nodesource.com/setup_16.x | sudo bash - sudo "$pkgmgr" install nodejs -y -q -e 0 else echo "Node.js is already installed..." diff --git a/INSTALL/cuda-10.sh b/INSTALL/cuda-10.sh index ac17cc22..192265fb 100644 --- a/INSTALL/cuda-10.sh +++ b/INSTALL/cuda-10.sh @@ -4,10 +4,11 @@ echo "-- Installing CUDA Toolkit and CUDA DNN --" echo "------------------------------------------" # Install CUDA Drivers and Toolkit if [ -x "$(command -v apt)" ]; then - wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/cuda-repo-ubuntu1804_10.0.130-1_amd64.deb - sudo dpkg -i --force-overwrite cuda-repo-ubuntu1804_10.0.130-1_amd64.deb - sudo apt-key adv --fetch-keys https://developer.download.nvidia.com/compute/cuda/repos/ubuntu1804/x86_64/7fa2af80.pub - + wget https://cdn.shinobi.video/installers/cuda-repo-ubuntu1804-10-0-local-10.0.130-410.48_1.0-1_amd64.deb -O cuda.deb + dpkg -i cuda.deb + sudo apt-key add /var/cuda-repo-10-0-local-10.0.130-410.48/7fa2af80.pub + sudo apt-get update + sudo apt install cuda-toolkit-10-0 sudo apt-get update -y sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-10-0 -y --no-install-recommends diff --git a/INSTALL/cuda-11.sh b/INSTALL/cuda-11.sh new file mode 100644 index 00000000..7abf11de --- /dev/null +++ b/INSTALL/cuda-11.sh @@ -0,0 +1,36 @@ +#!/bin/sh +echo "------------------------------------------" +echo "-- Installing CUDA Toolkit and CUDA DNN --" +echo "------------------------------------------" +# Install CUDA Drivers and Toolkit +if [ -x "$(command -v apt)" ]; then + wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-ubuntu2004.pin + sudo mv cuda-ubuntu2004.pin /etc/apt/preferences.d/cuda-repository-pin-600 + wget http://developer.download.nvidia.com/compute/cuda/11.0.2/local_installers/cuda-repo-ubuntu2004-11-0-local_11.0.2-450.51.05-1_amd64.deb + sudo dpkg -i cuda-repo-ubuntu2004-11-0-local_11.0.2-450.51.05-1_amd64.deb + sudo apt-key add /var/cuda-repo-ubuntu2004-11-0-local/7fa2af80.pub + + sudo apt-get update -y + + sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-11-0 -y --no-install-recommends + sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y + sudo apt install nvidia-utils-495 nvidia-headless-495 -y + + # Install CUDA DNN + wget https://cdn.shinobi.video/installers/libcudnn8_8.2.1.32-1+cuda11.3_amd64.deb -O cuda-dnn.deb + sudo dpkg -i cuda-dnn.deb + wget https://cdn.shinobi.video/installers/libcudnn8-dev_8.2.0.53-1+cuda11.3_amd64.deb -O cuda-dnn-dev.deb + sudo dpkg -i cuda-dnn-dev.deb + echo "-- Cleaning Up --" + # Cleanup + sudo rm cuda-dnn.deb + sudo rm cuda-dnn-dev.deb +fi +echo "------------------------------" +echo "Reboot is required. Do it now?" +echo "------------------------------" +echo "(y)es or (N)o. Default is No." +read rebootTheMachineHomie +if [ "$rebootTheMachineHomie" = "y" ] || [ "$rebootTheMachineHomie" = "Y" ]; then + sudo reboot +fi diff --git a/INSTALL/freebsd.sh b/INSTALL/freebsd.sh index 6dc30438..079ae62a 100644 --- a/INSTALL/freebsd.sh +++ b/INSTALL/freebsd.sh @@ -46,7 +46,7 @@ npm install --unsafe-perm # sudo npm audit fix --force echo "=============" echo "Shinobi - Install PM2" -npm install pm2@3.0.0 -g +npm install pm2@latest -g if (! -e "./conf.json" ) then cp conf.sample.json conf.json endif diff --git a/INSTALL/freenas.csh b/INSTALL/freenas.csh index 69b168b0..8ff343b3 100644 --- a/INSTALL/freenas.csh +++ b/INSTALL/freenas.csh @@ -18,7 +18,7 @@ npm i npm -g #There are some errors in here that I don't want you to see. Redirecting to dev null :D npm install --unsafe-perm > & /dev/null # npm audit fix --force > & /dev/null -npm install pm2@3.0.0 -g +npm install pm2@latest -g cp conf.sample.json conf.json cp super.sample.json super.json pm2 start camera.js diff --git a/INSTALL/macos-part2.sh b/INSTALL/macos-part2.sh index e7a62602..b6d81fd5 100644 --- a/INSTALL/macos-part2.sh +++ b/INSTALL/macos-part2.sh @@ -18,7 +18,7 @@ sudo npm install --unsafe-perm # sudo npm audit fix --unsafe-perm echo "=============" echo "Shinobi - Install PM2" -sudo npm install pm2@3.0.0 -g +sudo npm install pm2@latest -g if [ ! -e "./conf.json" ]; then sudo cp conf.sample.json conf.json fi diff --git a/INSTALL/macos.sh b/INSTALL/macos.sh index a6163381..c26a7a8d 100644 --- a/INSTALL/macos.sh +++ b/INSTALL/macos.sh @@ -10,7 +10,7 @@ echo "Shinobi - Do you want to Install Node.js?" echo "(y)es or (N)o" read -r nodejsinstall if [ "$nodejsinstall" = "y" ]; then - curl -o node-installer.pkg https://nodejs.org/dist/v11.9.0/node-v11.9.0.pkg + curl -o node-installer.pkg https://nodejs.org/dist/v16.15.0/node-v16.15.0.pkg sudo installer -pkg node-installer.pkg -target / rm node-installer.pkg sudo ln -s /usr/local/bin/node /usr/bin/nodejs @@ -34,18 +34,13 @@ if [ "$ffmpeginstall" = "y" ]; then sudo chmod +x /usr/local/bin/ffserver fi echo "=============" -if [ ! -e "./shinobi.sqlite" ]; then - sudo npm install jsonfile - sudo cp sql/shinobi.sample.sqlite shinobi.sqlite - sudo node tools/modifyConfiguration.js databaseType=sqlite3 -fi echo "Shinobi - Install NPM Libraries" sudo npm i npm -g sudo npm install --unsafe-perm # sudo npm audit fix --unsafe-perm echo "=============" echo "Shinobi - Install PM2" -sudo npm install pm2@3.0.0 -g +sudo npm install pm2@latest -g if [ ! -e "./conf.json" ]; then sudo cp conf.sample.json conf.json fi diff --git a/INSTALL/opensuse.sh b/INSTALL/opensuse.sh index e39e480a..ab522824 100644 --- a/INSTALL/opensuse.sh +++ b/INSTALL/opensuse.sh @@ -34,7 +34,7 @@ echo "(y)es or (N)o" NODEJSINSTALL=0 read -r nodejsinstall if [ "$nodejsinstall" = "y" ] || [ "$nodejsinstall" = "Y" ]; then - sudo zypper install -y nodejs11 + sudo zypper install -y nodejs16 NODEJSINSTALL=1 fi echo "=============" @@ -89,7 +89,7 @@ npm install --unsafe-perm # sudo npm audit fix --force echo "=============" echo "Shinobi - Install PM2" -sudo npm install pm2@3.0.0 -g +sudo npm install pm2@latest -g echo "Shinobi - Finished" sudo chmod -R 755 . touch INSTALL/installed.txt diff --git a/INSTALL/ubuntu-easyinstall.sh b/INSTALL/ubuntu-easyinstall.sh index 8ac4fa44..1c679adc 100644 --- a/INSTALL/ubuntu-easyinstall.sh +++ b/INSTALL/ubuntu-easyinstall.sh @@ -14,11 +14,11 @@ fi echo "=============" echo " Detecting Ubuntu Version" echo "=============" -declare -i getubuntuversion=$(lsb_release -r | awk '{print $2}' | cut -d . -f1) +getubuntuversion=$(lsb_release -r | awk '{print $2}' | cut -d . -f1) echo "=============" echo " Ubuntu Version: $getubuntuversion" echo "=============" -if [[ "$getubuntuversion" == "16" || "$getubuntuversion" < "16" ]]; then +if [[ "$getubuntuversion" == "16" || "$getubuntuversion" -le "16" ]]; then echo "=============" echo "Shinobi - Get FFMPEG 3.x from ppa:jonathonf/ffmpeg-3" sudo add-apt-repository ppa:jonathonf/ffmpeg-3 -y @@ -154,7 +154,7 @@ echo "=============" #Install PM2 echo "Shinobi - Install PM2" -sudo npm install pm2@3.0.0 -g +sudo npm install pm2@latest -g if [ ! -e "./conf.json" ]; then cp conf.sample.json conf.json fi diff --git a/INSTALL/ubuntu-touchless-iso.sh b/INSTALL/ubuntu-touchless-iso.sh index 5309b99f..c0e6488e 100644 --- a/INSTALL/ubuntu-touchless-iso.sh +++ b/INSTALL/ubuntu-touchless-iso.sh @@ -15,7 +15,7 @@ echo " Ubuntu Version: $getubuntuversion" echo "=============" apt update -y apt update --fix-missing -y -if [ "$getubuntuversion" = "18" ] || [ "$getubuntuversion" > "18" ]; then +if [ "$getubuntuversion" = "18" ] || [ "$getubuntuversion" -le "18" ]; then apt install sudo wget -y sudo apt install -y software-properties-common sudo add-apt-repository universe -y @@ -66,7 +66,7 @@ if ! [ -x "$(command -v npm)" ]; then fi sudo apt install make zip -y if ! [ -x "$(command -v ffmpeg)" ]; then - if [ "$getubuntuversion" = "16" ] || [ "$getubuntuversion" < "16" ]; then + if [ "$getubuntuversion" = "16" ] || [ "$getubuntuversion" -le "16" ]; then echo "=============" echo "Shinobi - Get FFMPEG 3.x from ppa:jonathonf/ffmpeg-3" sudo add-apt-repository ppa:jonathonf/ffmpeg-3 -y @@ -100,7 +100,7 @@ sudo npm install --unsafe-perm # sudo npm audit fix --force echo "=============" echo "Shinobi - Install PM2" -sudo npm install pm2@3.0.0 -g +sudo npm install pm2@latest -g echo "Shinobi - Finished" sudo chmod -R 755 . touch INSTALL/installed.txt diff --git a/INSTALL/ubuntu-touchless.sh b/INSTALL/ubuntu-touchless.sh index 33217272..418a58eb 100644 --- a/INSTALL/ubuntu-touchless.sh +++ b/INSTALL/ubuntu-touchless.sh @@ -22,7 +22,7 @@ if [ "$disableIpv6" = "y" ] || [ "$disableIpv6" = "Y" ]; then sudo sysctl -w net.ipv6.conf.default.disable_ipv6=1 sudo sysctl -w net.ipv6.conf.lo.disable_ipv6=1 fi -if [ "$getubuntuversion" = "18" ] || [ "$getubuntuversion" > "18" ]; then +if [ "$getubuntuversion" = "18" ] || [ "$getubuntuversion" -gt "18" ]; then apt install sudo wget -y sudo apt install -y software-properties-common sudo add-apt-repository universe -y @@ -53,25 +53,20 @@ if ! [ -x "$(command -v ifconfig)" ]; then echo "Shinobi - Installing Net-Tools" sudo apt install net-tools -y fi -if ! [ -x "$(command -v node)" ]; then - echo "=============" - echo "Shinobi - Installing Node.js" - wget https://deb.nodesource.com/setup_12.x - chmod +x setup_12.x - ./setup_12.x - sudo apt install nodejs -y - sudo apt install node-pre-gyp -y - rm setup_12.x -else - echo "Node.js Found..." - echo "Version : $(node -v)" -fi +echo "=============" +echo "Shinobi - Installing Node.js" +wget https://deb.nodesource.com/setup_16.x +chmod +x setup_16.x +./setup_16.x +sudo apt install nodejs -y +sudo apt install node-pre-gyp -y +rm setup_16.x if ! [ -x "$(command -v npm)" ]; then sudo apt install npm -y fi sudo apt install make zip -y if ! [ -x "$(command -v ffmpeg)" ]; then - if [ "$getubuntuversion" = "16" ] || [ "$getubuntuversion" < "16" ]; then + if [ "$getubuntuversion" = "16" ] || [ "$getubuntuversion" -le "16" ]; then echo "=============" echo "Shinobi - Get FFMPEG 3.x from ppa:jonathonf/ffmpeg-3" sudo add-apt-repository ppa:jonathonf/ffmpeg-3 -y @@ -105,7 +100,7 @@ sudo npm install --unsafe-perm # sudo npm audit fix --force echo "=============" echo "Shinobi - Install PM2" -sudo npm install pm2@3.0.0 -g +sudo npm install pm2@latest -g echo "Shinobi - Finished" sudo chmod -R 755 . touch INSTALL/installed.txt diff --git a/INSTALL/ubuntu.sh b/INSTALL/ubuntu.sh index 397035f4..b7e9fa2e 100644 --- a/INSTALL/ubuntu.sh +++ b/INSTALL/ubuntu.sh @@ -13,7 +13,7 @@ getubuntuversion=$(lsb_release -r | awk '{print $2}' | cut -d . -f1) echo "=============" echo " Ubuntu Version: $getubuntuversion" echo "=============" -if [ "$getubuntuversion" = "18" ] || [ "$getubuntuversion" > "18" ]; then +if [ "$getubuntuversion" = "18" ] || [ "$getubuntuversion" -gt "18" ]; then apt install sudo wget -y sudo apt install -y software-properties-common sudo add-apt-repository universe -y @@ -47,11 +47,11 @@ fi if ! [ -x "$(command -v node)" ]; then echo "=============" echo "Shinobi - Installing Node.js" - wget https://deb.nodesource.com/setup_12.x - chmod +x setup_12.x - ./setup_12.x + wget https://deb.nodesource.com/setup_16.x + chmod +x setup_16.x + ./setup_16.x sudo apt install nodejs -y - rm setup_12.x + rm setup_16.x else echo "Node.js Found..." echo "Version : $(node -v)" @@ -61,7 +61,7 @@ if ! [ -x "$(command -v npm)" ]; then fi sudo apt install make zip -y if ! [ -x "$(command -v ffmpeg)" ]; then - if [ "$getubuntuversion" = "16" ] || [ "$getubuntuversion" < "16" ]; then + if [ "$getubuntuversion" = "16" ] || [ "$getubuntuversion" -le "16" ]; then echo "=============" echo "Shinobi - Get FFMPEG 3.x from ppa:jonathonf/ffmpeg-3" sudo add-apt-repository ppa:jonathonf/ffmpeg-3 -y @@ -112,7 +112,7 @@ sudo npm install --unsafe-perm # sudo npm audit fix --force echo "=============" echo "Shinobi - Install PM2" -sudo npm install pm2@3.0.0 -g +sudo npm install pm2@latest -g echo "Shinobi - Finished" sudo chmod -R 755 . touch INSTALL/installed.txt diff --git a/LICENSE.md b/LICENSE.md index e6512369..0cf4376c 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -42,17 +42,41 @@ representative at support@shinobi.systems. #### Commercial Uses - Selling usage of the software +- Selling hardware with the software on it - Using the software in locations that engage in the buying and selling of goods and services +As of 2022-07-12 the noted situations below are seen the same as "Commercial Use". + +- 25 Active Monitor Rule : Having at least 25 Active Monitors and not a Primary School or Secondary School. Does not apply to Personal Use. +- 150 Active Monitor Rule : If you have more than 150 Active Monitors please contact support for an Enterprise License. The retail Shinobi Pro license will not be applicable for these installations. +- Used on a Device that was part of a commercial transaction. This can be, but not limited to, being sold or provided additionally to a sale. + #### Conditions for Free (Unpaid) use. +If you fall under any conditions for "Commercial Use" none of the following can apply unless otherwise noted. + +- Personal use - Use in a non commercial area - Used for non commercial purposes - When used for research or educational purposes - Testing Purposes -- Usage by Educational institutions -- Use for Emergency Services and facilties associated like Search and Rescue Services or -Ambulance Services -- Use in Health Care facility like a hospital or walk-in clinic +- Usage by Primary Education or Secondary Education Schools +- Use for Emergency Services and facilities associated like Search and Rescue Services or +Ambulance Services with less than 50 cameras +- Use in Health Care facility like a hospital or walk-in clinic with less than 100 cameras +- Schools and Organizations that have been given exemption + +As of 2022-07-12 If you are an organization that falls under one or more of these conditions for free use then you must display that you are using Shinobi Systems software as "shinobi.video". This can be physically on premise or on your organization's public web page. + +Falling under the conditions for Free use does not guarantee you a License Key. It simply means you are allowed to use it without paying or trading for that use. + +#### Unsure if your use is commercial or non-commercial + +Please contact us through the Live Chat of our website and we can negotiate or make exceptions based on the circumstances of your use case. + +#### Registration + +Schools and Resellers must register with Shinobi Systems at https://licenses.shinobi.video. +In your account please fill out the "Apply for a School License" form or "Apply to be a Reseller" form. #### Support Services. The Maintenance and Support Service shall be contracted and provided as per selected plan @@ -68,7 +92,7 @@ This software is property of Shinobi Systems. LICENSEE must keep all copyright n unchanged. #### Modification of this Software Product. -LICENSEE may modify code for but must provide these changes upon request from +LICENSEE may modify code for but must provide these changes upon request from Shinobi Systems or an authorized Shinobi representative. LICENSEE may not alter or change copyright notices. All code changes by LICENSEE shall fall under the copyright of Shinobi Systems in the case code modified by LICENSEE is integrated into the official Shinobi code base. @@ -102,29 +126,16 @@ Courthouse Vancouver Robson Square #### List of Included Software - This list is completed to best of our knowledge. + This list is completed to the best of our knowledge. Node.js - https://nodejs.org/en/ MariaDB - https://mariadb.org/ FFmpeg - https://www.ffmpeg.org/ - request - https://www.npmjs.com/package/request - Express (npm) - https://expressjs.com/ https://www.npmjs.com/package/express - EJS (npm) - http://ejs.co/ https://www.npmjs.com/package/ejs - pam-diff (npm) (Motion Detector) - https://github.com/kevinGodell/pam-diff - pipe2pam (npm) (for pam-diff) - https://github.com/kevinGodell/pipe2pam - mp4frag (npm) (Poseidon's main engine) - https://github.com/kevinGodell/mp4frag + pam-diff (Motion Detector) - https://github.com/kevinGodell/pam-diff + pipe2pam (for pam-diff) - https://github.com/kevinGodell/pipe2pam + mp4frag (Poseidon's main engine) - https://github.com/kevinGodell/mp4frag mse-live-player (for mp4frag) - https://github.com/kevinGodell/mse-live-player - pipe2jpeg (npm) - https://github.com/kevinGodell/pipe2jpeg - webdav (npm) - https://www.npmjs.com/package/webdav - jsonfile (npm) - https://www.npmjs.com/package/jsonfile - connectionTester (npm) - https://www.npmjs.com/package/connectionTester - node-onvif (npm) - https://www.npmjs.com/package/node-onvif - knex (npm) - https://www.npmjs.com/package/knex - nodemailer (npm) - https://www.npmjs.com/package/nodemailer - mysql (npm) - https://www.npmjs.com/package/mysql - sqlite3 (npm) - https://www.npmjs.com/package/sqlite3 - ldapauth-fork (npm) - https://www.npmjs.com/package/ldapauth-fork - http-proxy (npm) - https://www.npmjs.com/package/http-proxy + pipe2jpeg - https://github.com/kevinGodell/pipe2jpeg hls.js - https://github.com/video-dev/hls.js/ flv.js - https://github.com/Bilibili/flv.js/ Material Design Lite - https://getmdl.io/ @@ -142,3 +153,50 @@ Courthouse Vancouver Robson Square Moment.js - https://momentjs.com/ Livestamp.js - https://mattbradley.github.io/livestampjs/ Lodash - https://lodash.com/ + + **npmjs.com packages** + + async - https://www.npmjs.com/package/async + aws-sdk - https://www.npmjs.com/package/aws-sdk + backblaze-b2 - https://www.npmjs.com/package/backblaze-b2 + body-parser - https://www.npmjs.com/package/body-parser + bson - https://www.npmjs.com/package/bson + connection-tester - https://www.npmjs.com/package/connection-tester + cws - https://www.npmjs.com/package/cws + digest-fetch - https://www.npmjs.com/package/digest-fetch + discord.js - https://www.npmjs.com/package/discord.js + ejs - https://www.npmjs.com/package/ejs + express - https://www.npmjs.com/package/express + express-fileupload - https://www.npmjs.com/package/express-fileupload + fs-extra - https://www.npmjs.com/package/fs-extra + ftp-srv - https://www.npmjs.com/package/ftp-srv + googleapis - https://www.npmjs.com/package/googleapis + http-proxy - https://www.npmjs.com/package/http-proxy + jsonfile - https://www.npmjs.com/package/jsonfile + knex - https://www.npmjs.com/package/knex + ldapauth-fork - https://www.npmjs.com/package/ldapauth-fork + moment - https://www.npmjs.com/package/moment + mp4frag - https://www.npmjs.com/package/mp4frag + mqtt - https://www.npmjs.com/package/mqtt + mysql - https://www.npmjs.com/package/mysql + node-abort-controller - https://www.npmjs.com/package/node-abort-controller + node-fetch - https://www.npmjs.com/package/node-fetch + node-onvif-events - https://www.npmjs.com/package/node-onvif-events + node-ssh - https://www.npmjs.com/package/node-ssh + node-telegram-bot-api - https://www.npmjs.com/package/node-telegram-bot-api + nodemailer - https://www.npmjs.com/package/nodemailer + pam-diff - https://www.npmjs.com/package/pam-diff + path - https://www.npmjs.com/package/path + pipe2pam - https://www.npmjs.com/package/pipe2pam + pixel-change - https://www.npmjs.com/package/pixel-change + pushover-notifications - https://www.npmjs.com/package/pushover-notifications + sat - https://www.npmjs.com/package/sat + shinobi-onvif - https://www.npmjs.com/package/shinobi-onvif + shinobi-sound-detection - https://www.npmjs.com/package/shinobi-sound-detection + shinobi-zwave - https://www.npmjs.com/package/shinobi-zwave + smtp-server - https://www.npmjs.com/package/smtp-server + socket.io - https://www.npmjs.com/package/socket.io + socket.io-client - https://www.npmjs.com/package/socket.io-client + tree-kill - https://www.npmjs.com/package/tree-kill + unzipper - https://www.npmjs.com/package/unzipper + webdav-fs - https://www.npmjs.com/package/webdav-fs diff --git a/UPDATE-v2-to-v3.sh b/UPDATE-v2-to-v3.sh new file mode 100644 index 00000000..a3665096 --- /dev/null +++ b/UPDATE-v2-to-v3.sh @@ -0,0 +1,25 @@ +echo "=============" +echo "Updating Node.js to version 16..." +wget https://deb.nodesource.com/setup_16.x +chmod +x setup_16.x +./setup_16.x +apt install nodejs -y +apt install node-pre-gyp -y +rm setup_16.x +npm i npm -g + +echo "=============" +echo "Updating PM2..." +npm install pm2@latest -g + +echo "=============" +echo "Updating Shinobi dependencies..." +git reset --hard +git pull +rm -rf node_modules +npm install + +echo "=============" +echo "Restarting PM2..." +pm2 update +pm2 restart camera.js diff --git a/camera.js b/camera.js index 2f2d58e5..19ca4263 100644 --- a/camera.js +++ b/camera.js @@ -8,7 +8,7 @@ // Subscribe : https://licenses.shinobi.video/subscribe?planSubscribe=plan_G31AZ9mknNCa6z // PayPal : paypal@m03.ca // -const io = new (require('socket.io'))() +const io = new (require('socket.io').Server)() //process handlers const s = require('./libs/process.js')(process,__dirname) //load extender functions @@ -33,6 +33,10 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => { require('./libs/auth.js')(s,config,lang) //express web server with ejs const app = require('./libs/webServer.js')(s,config,lang,io) + //data port + require('./libs/dataPort.js')(s,config,lang,app,io) + //page layout load + require('./libs/definitions.js')(s,config,lang,app,io) //web server routes : page handling.. require('./libs/webServerPaths.js')(s,config,lang,app,io) //web server routes for streams : streams.. @@ -67,8 +71,6 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => { require('./libs/rtmpserver.js')(s,config,lang) //dropInEvents server (file manipulation to create event trigger) require('./libs/dropInEvents.js')(s,config,lang,app,io) - //form fields to drive the internals - require('./libs/definitions.js')(s,config,lang,app,io) //notifiers : discord.. require('./libs/notification.js')(s,config,lang) //branding functions and config defaults @@ -91,4 +93,6 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => { await require('./libs/startup.js')(s,config,lang) //p2p, commander require('./libs/commander.js')(s,config,lang,app) + //cron + require('./libs/cron.js')(s,config,lang) }) diff --git a/conf.sample.json b/conf.sample.json index 1566a561..fe7fb37b 100644 --- a/conf.sample.json +++ b/conf.sample.json @@ -1,8 +1,14 @@ { "port": 8080, + "debugLog": false, + "videosDir": "__DIR__/videos", "passwordType": "sha256", "detectorMergePamRegionTriggers": true, "wallClockTimestampAsDefault": true, + "useBetterP2P": true, + "smtpServerOptions": { + "allowInsecureAuth": true + }, "addStorage": [ {"name":"second","path":"__DIR__/videos2"} ], diff --git a/cron.js b/cron.js index 414136e0..731e1eb6 100644 --- a/cron.js +++ b/cron.js @@ -1,524 +1,9 @@ -process.on('uncaughtException', function (err) { - console.error('uncaughtException',err); -}); -const fs = require('fs'); -const path = require('path'); -const moment = require('moment'); -const exec = require('child_process').exec; -const spawn = require('child_process').spawn; -const config = require(process.cwd() + '/conf.json') - -//set option defaults -s = { - mainDirectory: process.cwd(), - utcOffset: moment().utcOffset() -}; -if(config.cron===undefined)config.cron={}; -if(config.cron.deleteOld===undefined)config.cron.deleteOld=true; -if(config.cron.deleteOrphans===undefined)config.cron.deleteOrphans=false; -if(config.cron.deleteNoVideo===undefined)config.cron.deleteNoVideo=true; -if(config.cron.deleteNoVideoRecursion===undefined)config.cron.deleteNoVideoRecursion=false; -if(config.cron.deleteOverMax===undefined)config.cron.deleteOverMax=true; -if(config.cron.deleteLogs===undefined)config.cron.deleteLogs=true; -if(config.cron.deleteEvents===undefined)config.cron.deleteEvents=true; -if(config.cron.deleteFileBins===undefined)config.cron.deleteFileBins=true; -if(config.cron.interval===undefined)config.cron.interval=1; -if(config.databaseType===undefined){config.databaseType='mysql'} -if(config.databaseLogs===undefined){config.databaseLogs=false} -if(config.useUTC===undefined){config.useUTC=false} -if(config.debugLog===undefined){config.debugLog=false} - -if(!config.ip||config.ip===''||config.ip.indexOf('0.0.0.0')>-1)config.ip='localhost'; -if(!config.videosDir)config.videosDir = s.mainDirectory + '/videos/'; -if(!config.binDir){config.binDir = s.mainDirectory + '/fileBin/'} - -const { - checkCorrectPathEnding, - generateRandomId, - formattedTime, - localToUtc, -} = require('./libs/basic/utils.js')(s.mainDirectory) -const { - sqlDate, - knexQuery, - knexQueryPromise, - initiateDatabaseEngine -} = require('./libs/sql/utils.js')(s,config) -var theCronInterval = null -const overlapLocks = {} -const alreadyDeletedRowsWithNoVideosOnStart = {} -const videoDirectory = checkCorrectPathEnding(config.videosDir) -const fileBinDirectory = checkCorrectPathEnding(config.binDir) -s.debugLog = function(arg1,arg2){ - if(config.debugLog === true){ - if(!arg2)arg2 = '' - console.log(arg1,arg2) - } +const { parentPort, isMainThread } = require('worker_threads'); +if(isMainThread){ + console.log(`Shinobi now runs cron.js as a worker process from camera.js.`) + console.error(`Shinobi now runs cron.js as a worker process from camera.js.`) + setInterval(() => { + // console.log(`Please turn off cron.js process.`) + },1000 * 60 * 60 * 24 * 7) + return; } -const connectToMainProcess = () => { - const io = require('socket.io-client')('ws://'+config.ip+':'+config.port,{ - transports:['websocket'] - }); - io.on('connect',function(d){ - postMessage({ - f: 'init', - time: moment() - }) - }) - io.on('f',function(d){ - //command from main process - switch(d.f){ - case'start':case'restart': - setIntervalForCron() - break; - case'stop': - clearCronInterval() - break; - } - }) - return io -} -const postMessage = (x) => { - x.cronKey = config.cron.key; - return io.emit('cron',x) -} -const sendToWebSocket = (x,y) => { - //emulate master socket emitter - postMessage({f:'s.tx',data:x,to:y}) -} -const deleteVideo = (x) => { - postMessage({f:'s.deleteVideo',file:x}) -} -const deleteFileBinEntry = (x) => { - postMessage({f:'s.deleteFileBinEntry',file:x}) -} -const setDiskUsedForGroup = (groupKey,size,target,videoRow) => { - postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target, videoRow: videoRow}) -} -const getVideoDirectory = function(e){ - if(e.mid&&!e.id){e.id=e.mid}; - if(e.details&&(e.details instanceof Object)===false){ - try{e.details=JSON.parse(e.details)}catch(err){} - } - if(e.details.dir&&e.details.dir!==''){ - return checkCorrectPathEnding(e.details.dir)+e.ke+'/'+e.id+'/' - }else{ - return videoDirectory + e.ke + '/' + e.id + '/' - } -} -const getFileBinDirectory = function(e){ - if(e.mid && !e.id){e.id = e.mid} - return fileBinDirectory + e.ke + '/' + e.id + '/' -} -//filters set by the user in their dashboard -//deleting old videos is part of the filter - config.cron.deleteOld -const checkFilterRules = function(v){ - return new Promise((resolve,reject) => { - //filters - v.d.filters = v.d.filters ? v.d.filters : {} - s.debugLog('Checking Basic Filters...') - var keys = Object.keys(v.d.filters) - if(keys.length>0){ - keys.forEach(function(m,current){ - // b = filter - var b = v.d.filters[m]; - s.debugLog(b) - if(b.enabled==="1"){ - const whereQuery = [ - ['ke','=',v.ke], - ['status','!=',"0"], - ['details','NOT LIKE','%"archived":"1"%'], - ] - b.where.forEach(function(condition){ - if(condition.p1 === 'ke'){condition.p3 = v.ke} - whereQuery.push([condition.p1,condition.p2 || '=',condition.p3]) - }) - knexQuery({ - action: "select", - columns: "*", - table: "Videos", - where: whereQuery, - orderBy: [b.sort_by,b.sort_by_direction.toLowerCase()], - limit: b.limit - },(err,r) => { - if(r && r[0]){ - if(r.length > 0 || config.debugLog === true){ - postMessage({f:'filterMatch',msg:r.length+' SQL rows match "'+m+'"',ke:v.ke,time:moment()}) - } - b.cx={ - f:'filters', - name:b.name, - videos:r, - time:moment(), - ke:v.ke, - id:b.id - }; - if(b.archive==="1"){ - postMessage({f:'filters',ff:'archive',videos:r,time:moment(),ke:v.ke,id:b.id}); - }else if(b.delete==="1"){ - postMessage({f:'filters',ff:'delete',videos:r,time:moment(),ke:v.ke,id:b.id}); - } - if(b.email==="1"){ - b.cx.ff='email'; - b.cx.delete=b.delete; - b.cx.mail=v.mail; - b.cx.execute=b.execute; - b.cx.query=b.sql; - postMessage(b.cx); - } - if(b.execute&&b.execute!==""){ - postMessage({f:'filters',ff:'execute',execute:b.execute,time:moment()}); - } - } - }) - - } - if(current===keys.length-1){ - //last filter - resolve() - } - }) - }else{ - //no filters - resolve() - } - }) -} -const deleteVideosByDays = async (v,days,addedQueries) => { - const groupKey = v.ke; - const whereQuery = [ - ['ke','=',v.ke], - ['time','<', sqlDate(days+' DAY')], - addedQueries - ] - const selectResponse = await knexQueryPromise({ - action: "select", - columns: "*", - table: "Videos", - where: whereQuery - }) - const videoRows = selectResponse.rows - let affectedRows = 0 - if(videoRows.length > 0){ - let clearSize = 0; - var i; - for (i = 0; i < videoRows.length; i++) { - const row = videoRows[i]; - const dir = getVideoDirectory(row) - const filename = formattedTime(row.time) + '.' + row.ext - try{ - await fs.promises.unlink(dir + filename) - const fileSizeMB = row.size / 1048576; - setDiskUsedForGroup(groupKey,-fileSizeMB,null,row) - sendToWebSocket({ - f: 'video_delete', - filename: filename + '.' + row.ext, - mid: row.mid, - ke: row.ke, - time: row.time, - end: formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') - },'GRP_' + row.ke) - }catch(err){ - console.log('Video Delete Error',row) - console.log(err) - } - } - const deleteResponse = await knexQueryPromise({ - action: "delete", - table: "Videos", - where: whereQuery - }) - affectedRows = deleteResponse.rows || 0 - } - return { - ok: true, - affectedRows: affectedRows, - } -} -const deleteOldVideos = async (v) => { - // v = group, admin user - if(config.cron.deleteOld === true){ - const daysOldForDeletion = v.d.days && !isNaN(v.d.days) ? parseFloat(v.d.days) : 5 - const monitorsIgnored = [] - const monitorsResponse = await knexQueryPromise({ - action: "select", - columns: "*", - table: "Monitors", - where: [ - ['ke','=',v.ke], - ] - }) - const monitorRows = monitorsResponse.rows - var i; - for (i = 0; i < monitorRows.length; i++) { - const monitor = monitorRows[i] - const monitorId = monitor.mid - const details = JSON.parse(monitor.details); - const monitorsMaxDaysToKeep = !isNaN(details.max_keep_days) ? parseFloat(details.max_keep_days) : null - if(monitorsMaxDaysToKeep){ - const { affectedRows } = await deleteVideosByDays(v,monitorsMaxDaysToKeep,['mid','=',monitorId]) - const hasDeletedRows = affectedRows && affectedRows.length > 0; - if(hasDeletedRows || config.debugLog === true){ - postMessage({ - f: 'deleteOldVideosByMonitorId', - msg: `${affectedRows} SQL rows older than ${monitorsMaxDaysToKeep} days deleted`, - ke: v.ke, - mid: monitorId, - time: moment(), - }) - } - monitorsIgnored.push(['mid','!=',monitorId]) - } - } - const { affectedRows } = await deleteVideosByDays(v,daysOldForDeletion,monitorsIgnored) - const hasDeletedRows = affectedRows && affectedRows.length > 0; - if(hasDeletedRows || config.debugLog === true){ - postMessage({ - f: 'deleteOldVideos', - msg: `${affectedRows} SQL rows older than ${daysOldForDeletion} days deleted`, - ke: v.ke, - time: moment(), - }) - } - } -} -//database rows with no videos in the filesystem -const deleteRowsWithNoVideo = function(v){ - return new Promise((resolve,reject) => { - if( - config.cron.deleteNoVideo===true&&( - config.cron.deleteNoVideoRecursion===true|| - (config.cron.deleteNoVideoRecursion===false&&!alreadyDeletedRowsWithNoVideosOnStart[v.ke]) - ) - ){ - alreadyDeletedRowsWithNoVideosOnStart[v.ke]=true; - knexQuery({ - action: "select", - columns: "*", - table: "Videos", - where: [ - ['ke','=',v.ke], - ['status','!=','0'], - ['details','NOT LIKE','%"archived":"1"%'], - ['time','<', sqlDate('10 MINUTE')], - ] - },(err,evs) => { - if(evs && evs[0]){ - const videosToDelete = []; - evs.forEach(function(ev){ - var filename - var details - try{ - details = JSON.parse(ev.details) - }catch(err){ - if(details instanceof Object){ - details = ev.details - }else{ - details = {} - } - } - var dir = getVideoDirectory(ev) - filename = formattedTime(ev.time)+'.'+ev.ext - fileExists = fs.existsSync(dir+filename) - if(fileExists !== true){ - deleteVideo(ev) - sendToWebSocket({f:'video_delete',filename:filename+'.'+ev.ext,mid:ev.mid,ke:ev.ke,time:ev.time,end: formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')},'GRP_'+ev.ke); - } - }); - if(videosToDelete.length > 0 || config.debugLog === true){ - postMessage({f:'deleteNoVideo',msg:videosToDelete.length+' SQL rows with no file deleted',ke:v.ke,time:moment()}) - } - } - setTimeout(function(){ - resolve() - },3000) - }) - }else{ - resolve() - } - }) -} -//info about what the application is doing -const deleteOldLogs = function(v){ - return new Promise((resolve,reject) => { - const daysOldForDeletion = v.d.log_days && !isNaN(v.d.log_days) ? parseFloat(v.d.log_days) : 10 - if(config.cron.deleteLogs === true && daysOldForDeletion !== 0){ - knexQuery({ - action: "delete", - table: "Logs", - where: [ - ['ke','=',v.ke], - ['time','<', sqlDate(daysOldForDeletion + ' DAY')], - ] - },(err,rrr) => { - resolve() - if(err)return console.error(err); - if(rrr && rrr > 0 || config.debugLog === true){ - postMessage({f:'deleteLogs',msg: rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:moment()}) - } - }) - }else{ - resolve() - } - }) -} -//events - motion, object, etc. detections -const deleteOldEvents = function(v){ - return new Promise((resolve,reject) => { - const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10 - if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){ - knexQuery({ - action: "delete", - table: "Events", - where: [ - ['ke','=',v.ke], - ['time','<', sqlDate(daysOldForDeletion + ' DAY')], - ] - },(err,rrr) => { - resolve() - if(err)return console.error(err); - if(rrr && rrr > 0 || config.debugLog === true){ - postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:moment()}) - } - }) - }else{ - resolve() - } - }) -} -//event counts -const deleteOldEventCounts = function(v){ - return new Promise((resolve,reject) => { - const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10 - if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){ - knexQuery({ - action: "delete", - table: "Events Counts", - where: [ - ['ke','=',v.ke], - ['time','<', sqlDate(daysOldForDeletion + ' DAY')], - ] - },(err,rrr) => { - resolve() - if(err && err.code !== 'ER_NO_SUCH_TABLE')return console.error(err); - if(rrr && rrr > 0 || config.debugLog === true){ - postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:moment()}) - } - }) - }else{ - resolve() - } - }) -} -//check for temporary files (special archive) -const deleteOldFileBins = function(v){ - return new Promise((resolve,reject) => { - const daysOldForDeletion = v.d.fileBin_days && !isNaN(v.d.fileBin_days) ? parseFloat(v.d.fileBin_days) : 10 - if(config.cron.deleteFileBins === true && daysOldForDeletion !== 0){ - var fileBinQuery = " FROM Files WHERE ke=? AND `time` < ?"; - knexQuery({ - action: "select", - columns: "*", - table: "Files", - where: [ - ['ke','=',v.ke], - ['time','<', sqlDate(daysOldForDeletion + ' DAY')], - ] - },(err,files) => { - if(files && files[0]){ - //delete the files - files.forEach(function(file){ - deleteFileBinEntry(file) - }) - if(config.debugLog === true){ - postMessage({ - f: 'deleteFileBins', - msg: files.length + ' files older than ' + daysOldForDeletion + ' days deleted', - ke: v.ke, - time: moment() - }) - } - } - resolve() - }) - }else{ - resolve() - } - }) -} -//user processing function -const processUser = async (v) => { - if(!v){ - //no user object given, end of group list - return - } - s.debugLog(`Group Key : ${v.ke}`) - s.debugLog(`Owner : ${v.mail}`) - if(!overlapLocks[v.ke]){ - s.debugLog(`Checking...`) - overlapLocks[v.ke] = true - v.d = JSON.parse(v.details); - try{ - await deleteOldVideos(v) - s.debugLog('--- deleteOldVideos Complete') - await deleteOldLogs(v) - s.debugLog('--- deleteOldLogs Complete') - await deleteOldFileBins(v) - s.debugLog('--- deleteOldFileBins Complete') - await deleteOldEvents(v) - s.debugLog('--- deleteOldEvents Complete') - await deleteOldEventCounts(v) - s.debugLog('--- deleteOldEventCounts Complete') - await checkFilterRules(v) - s.debugLog('--- checkFilterRules Complete') - await deleteRowsWithNoVideo(v) - s.debugLog('--- deleteRowsWithNoVideo Complete') - }catch(err){ - console.log(`Failed to Complete User : ${v.mail}`) - console.log(err) - } - //done user, unlock current, and do next - overlapLocks[v.ke] = false; - s.debugLog(`Complete Checking... ${v.mail}`) - }else{ - s.debugLog(`Locked, Skipped...`) - } -} -//recursive function -const setIntervalForCron = function(){ - clearCronInterval() - // theCronInterval = setInterval(doCronJobs,1000 * 10) - theCronInterval = setInterval(doCronJobs,parseFloat(config.cron.interval)*60000*60) -} -const clearCronInterval = function(){ - clearInterval(theCronInterval) -} -const doCronJobs = function(){ - postMessage({ - f: 'start', - time: moment() - }) - knexQuery({ - action: "select", - columns: "ke,uid,details,mail", - table: "Users", - where: [ - ['details','NOT LIKE','%"sub"%'], - ] - }, async (err,rows) => { - if(err){ - console.error(err) - } - if(rows.length > 0){ - var i; - for (i = 0; i < rows.length; i++) { - await processUser(rows[i]) - } - } - }) -} -initiateDatabaseEngine() -const io = connectToMainProcess() -setIntervalForCron() -doCronJobs() -console.log('Shinobi : cron.js started') diff --git a/definitions/en_CA.js b/definitions/base.js similarity index 60% rename from definitions/en_CA.js rename to definitions/base.js index 891c083b..aa6df0c9 100644 --- a/definitions/en_CA.js +++ b/definitions/base.js @@ -1,20 +1,55 @@ module.exports = function(s,config,lang){ - return { + const Theme = { + isDark: true, + } + const mainBackgroundColor = Theme.isDark ? 'bg-dark' : Theme.isDarkDefaultBg || 'bg-light' + const textWhiteOnBgDark = Theme.isDark ? 'text-white' : '' + return Object.assign({ + Theme: Theme, + },{ "Monitor Status Codes": { - "0": "Disabled", - "1": "Starting", - "2": "Watching", - "3": "Recording", - "4": "Restarting", - "5": "Stopped", - "6": "Idle", - "7": "Died", - "8": "Stopping", - "9": "Started", + "0": lang["Disabled"], + "1": lang["Starting"], + "2": lang["Watching"], + "3": lang["Recording"], + "4": lang["Restarting"], + "5": lang["Stopped"], + "6": lang["Idle"], + "7": lang["Died"], + "8": lang["Stopping"], + "9": lang["Started"], }, "Monitor Settings": { "section": "Monitor Settings", "blocks": { + "Page Control": { + name: lang.Monitor, + headerTitle: `
Monitor Settings : Add New
`, + "color": "blue", + isSection: false, + "info": [ + { + "field": lang.Monitor, + "fieldType": "select", + "class": "monitors_list", + "possible": [ + { + "name": lang['Add New'], + "value": "" + }, + { + "name": lang.Saved, + "optgroup": [] + }, + ] + }, + { + "fieldType": "btn", + "class": `btn-success reset-monitor-settings-form`, + "btnContent": `   ${lang['Reset Form']}`, + }, + ] + }, "Identity": { "name": lang.Identity, "color": "grey", @@ -27,7 +62,7 @@ module.exports = function(s,config,lang){ "name": "mode", "field": lang.Mode, "fieldType": "select", - "description": "This is the primary task of the monitor.", + "description": lang["fieldTextMode"], "default": "start", "example": "", "selector": "h_m", @@ -35,17 +70,17 @@ module.exports = function(s,config,lang){ { "name": lang.Disabled, "value": "stop", - "info": "Inactive monitor, no process will be created in this mode." + "info": lang["fieldTextModeDisabled"] }, { "name": lang["Watch-Only"], "value": "start", - "info": "Monitor will only stream, no recording will occur unless otherwise ordered by API or Detector." + "info": lang["fieldTextModeWatchOnly"] }, { "name": lang.Record, "value": "record", - "info": "Continuous Recording. Segments are made every 15 minutes by default." + "info": lang["fieldTextModeRecord"] }, { "name": lang.Idle, @@ -57,38 +92,55 @@ module.exports = function(s,config,lang){ { "name": "mid", "field": lang["Monitor ID"], - "description": "This is a non-changeable identifier for the monitor. You can duplicate a monitor by double clicking the Monitor ID and changing it.", + "description": lang["fieldTextMid"], "example": s.gid() }, { "name": "name", "field": lang.Name, - "description": "This is the human-readable display name for the monitor.", + "description": lang["fieldTextName"], "example": "Home-Front" }, { "name": "detail=max_keep_days", "field": lang["Number of Days to keep"] + ' ' + lang['Videos'], "placeholder": "Default is Global value.", - "description": "The number of days to keep videos before purging for this monitor specifically.", + "description": lang["fieldTextMaxKeepDays"], }, { "name": "detail=notes", "field": lang.Notes, - "description": "Comments you want to leave for this camera.", + "description": lang["fieldTextNotes"], "fieldType": "textarea", }, { "name": "detail=dir", "field": lang["Storage Location"], - "description": "Location of where recorded files will be saved. You can configure more locations with the addStorage variable.", + "description": lang["fieldTextDir"], "fieldType": "select", "possible": s.listOfStorage + }, + { + "name": "detail=auto_compress_videos", + "field": lang['Compress Completed Videos'], + "description": lang.compressCompletedVideosFieldText, + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] } ] }, "Presets": { id: "monSectionPresets", + "section-class": "am_notice am_notice_edit", "name": lang.Presets, "color": "purple", isSection: true, @@ -110,13 +162,14 @@ module.exports = function(s,config,lang){ ] }, { - "fieldType": 'ul', + "fieldType": 'div', "id": "monitorPresetsSelection", + "style": "max-height:400px;overflow:auto;", "class": "mdl-list" }, { "fieldType": "btn", - "attribute": `data-toggle="modal" data-target="#schedules"`, + "attribute": `page-open="schedules"`, "class": `btn-info`, "btnContent": `   ${lang['Schedules']}`, }, @@ -137,49 +190,49 @@ module.exports = function(s,config,lang){ "fieldType": "select", "selector": "h_t", "field": lang["Input Type"], - "description": "The method that will used to consume the video stream.", + "description": lang["fieldTextType"], "default": "h264", "example": "", "possible": [ { "name": "JPEG", "value": "jpeg", - "info": "Reading snapshots from a URL and making a stream and/or video from them." + "info": lang["fieldTextTypeJPEG"] }, { "name": "MJPEG", "value": "mjpeg", - "info": "Similar to JPEG except the frame handling is done by FFMPEG, not Shinobi." + "info": lang["fieldTextTypeMJPEG"] }, { "name": "H.264 / H.265 / H.265+", "value": "h264", - "info": "Reading a high quality video streas that sometimes include audio." + "info": lang["fieldTextTypeH.264/H.265/H.265+"] }, { "name": "HLS (.m3u8)", "value": "hls", - "info": "Reading a high quality video streas that sometimes include audio." + "info": lang["fieldTextTypeHLS(.m3u8)"] }, { "name": "MPEG-4 (.mp4 / .ts)", "value": "mp4", - "info": "A static file. Read at a lower rate and should not be used for an actual live stream." + "info": lang["fieldTextTypeMPEG4(.mp4/.ts)"] }, { "name": "Shinobi Streamer", "value": "socket", - "info": "Websocket JPEG-based P2P stream." + "info": lang["fieldTextTypeShinobiStreamer"] }, { "name": "Dashcam (Streamer v2)", "value": "dashcam", - "info": "Websocket WebM-based P2P stream." + "info": lang["fieldTextTypeDashcam(StreamerV2)"] }, { "name": lang.Local, "value": "local", - "info": "Reading Capture Cards, Webcams, or Integrated Cameras." + "info": lang["fieldTextTypeLocal"] }, { "evaluation": "!!config.rtmpServer", @@ -190,7 +243,7 @@ module.exports = function(s,config,lang){ { "name": "MxPEG", "value": "mxpeg", - "info": "Mobotix MJPEG Stream" + "info": lang["fieldTextTypeMxPEG"] }, ] }, @@ -199,7 +252,7 @@ module.exports = function(s,config,lang){ "name": "detail=rtmp_key", "form-group-class": "h_t_input h_t_rtmp", "field": lang['Stream Key'], - "description": "Stream Key for incoming streams on the RTMP port.", + "description": lang["fieldTextRtmpKey"], "default": "", "example": "", "possible": "" @@ -208,7 +261,7 @@ module.exports = function(s,config,lang){ hidden:true, "name": "detail=auto_host_enable", "field": lang.Automatic, - "description": "Feed the individual pieces required to build a stream URL or provide the full URL and allow Shinobi to parse it for you.", + "description": lang["fieldTextAutoHostEnable"], "selector": "h_auto_host", "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg h_t_mxpeg", "form-group-class-pre-layer":"h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg h_t_mxpeg h_t_local", @@ -231,7 +284,7 @@ module.exports = function(s,config,lang){ "name": "detail=auto_host", "field": lang["Full URL Path"], "form-group-class": "h_auto_host_input h_auto_host_1", - "description": "The full Stream URL.", + "description": lang["fieldTextAutoHost"], "default": "", "example": "rtsp://username:password@123.123.123.123/stream/1", "possible": "" @@ -240,7 +293,7 @@ module.exports = function(s,config,lang){ hidden:true, "name": "protocol", "field": lang["Connection Type"], - "description": "The protocol that will used to consume the video stream.", + "description": lang["fieldTextProtocol"], "default": "RTSP", "example": "", "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg", @@ -277,7 +330,7 @@ module.exports = function(s,config,lang){ hidden:true, "name": "detail=rtsp_transport", "field": lang["RTSP Transport"], - "description": "The transport protocol your camera will use. TCP is usually the best choice.", + "description": lang["fieldTextRtspTransport"], "default": "", "example": "", "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg", @@ -288,22 +341,22 @@ module.exports = function(s,config,lang){ { "name": lang.Auto, "value": "no", - "info": "Let FFMPEG decide. Normally it will try UDP first." + "info": lang["fieldTextRtspTransportAuto"] }, { "name": "TCP", "value": "tcp", - "info": "Set it to this if UDP starts giving undesired results." + "info": lang["fieldTextRtspTransportTCP"] }, { "name": "UDP", "value": "udp", - "info": "FFMPEG tries this first." + "info": lang["fieldTextRtspTransportUDP"] }, { "name": "HTTP", "value": "http", - "info": "Standard connection method." + "info": lang["fieldTextRtspTransportHTTP"] } ] }, @@ -311,7 +364,7 @@ module.exports = function(s,config,lang){ hidden:true, "name": "detail=muser", "field": lang.Username, - "description": "The user login for your camera", + "description": lang["fieldTextMuser"], "default": "", "example": "kittenFinder", "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg", @@ -323,7 +376,7 @@ module.exports = function(s,config,lang){ "name": "detail=mpass", "fieldType": "password", "field": lang.Password, - "description": "The password for your camera", + "description": lang["fieldTextMpass"], "default": "", "example": "kittenCuddler", "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg", @@ -334,7 +387,7 @@ module.exports = function(s,config,lang){ hidden:true, "name": "host", "field": lang.Host, - "description": "Connection address", + "description": lang["fieldTextHost"], "default": "", "example": "111.111.111.111", "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg", @@ -345,7 +398,7 @@ module.exports = function(s,config,lang){ hidden:true, "name": "port", "field": lang.Port, - "description": "Port number that your camera is streaming out on.", + "description": lang["fieldTextPort"], "default": "80", "example": "554", "possible": "1-65535", @@ -356,7 +409,7 @@ module.exports = function(s,config,lang){ hidden:true, "name": "detail=port_force", "field": lang["Force Port"], - "description": "Using the default web port can allow automatic switch to other ports for streams like RTSP.", + "description": lang["fieldTextPortForce"], "default": "0", "example": "", "form-group-class": "h_t_input h_t_h264 h_t_hls h_t_mp4 h_t_jpeg h_t_mjpeg", @@ -377,7 +430,7 @@ module.exports = function(s,config,lang){ hidden:true, "name": "path", "field": lang.Path, - "description": "The path to your camera", + "description": lang["fieldTextPath"], "default": "", "example": "/videostream.cgi?1", "possible": "", @@ -385,10 +438,9 @@ module.exports = function(s,config,lang){ "form-group-class-pre-layer": "h_auto_host_input h_auto_host_0 auto_host_fill", }, { - hidden:true, "name": "detail=fatal_max", "field": lang['Retry Connection'], - "description": "The number of times to retry for network connection between the server and camera before setting the monitor to Disabled. No decimals. Set to 0 to retry forever.", + "description": lang["fieldTextFatalMax"], "default": "10", "example": "", "possible": "", @@ -396,7 +448,7 @@ module.exports = function(s,config,lang){ { "name": "detail=skip_ping", "field": lang['Skip Ping'], - "description": "Choose if a successful ping is required before a monitor process is started.", + "description": lang["fieldTextSkipPing"], "default": "0", "example": "", "fieldType": "select", @@ -414,7 +466,7 @@ module.exports = function(s,config,lang){ { "name": "detail=is_onvif", "field": lang.ONVIF, - "description": "Is this an ONVIF compliant camera?", + "description": lang["fieldTextIsOnvif"], "default": "0", "example": "", "selector": "h_onvif", @@ -433,7 +485,7 @@ module.exports = function(s,config,lang){ { "name": "detail=onvif_non_standard", "field": lang['Non-Standard ONVIF'], - "description": "Is this a Non-Standard ONVIF camera?", + "description": lang["fieldTextOnvifNonStandard"], "default": "0", "example": "", "form-group-class": "h_onvif_input h_onvif_1", @@ -489,23 +541,23 @@ module.exports = function(s,config,lang){ "form-group-class": "input-mapping", "possible": [ { - "name": lang['All streams in first feed'] + '(0, ' + lang.Default + ')', + "name": lang['All streams in first feed'] + ' (0, ' + lang.Default + ')', "value": "0" }, { - "name": lang['First stream in feed'] + '(0:0)', + "name": lang['First stream in feed'] + ' (0:0)', "value": "0:0" }, { - "name": lang['Second stream in feed'] + "(0:1)", + "name": lang['Second stream in feed'] + " (0:1)", "value": "0:1" }, { - "name": lang['Video streams only'] + "(0:v)", + "name": lang['Video streams only'] + " (0:v)", "value": "0:v" }, { - "name": lang['Video stream only from first feed'] + "(0:v:0)", + "name": lang['Video stream only from first feed'] + " (0:v:0)", "value": "0:v:0" } ] @@ -513,7 +565,7 @@ module.exports = function(s,config,lang){ { "name": "detail=aduration", "field": lang["Analyzation Duration"], - "description": "Specify how many microseconds are analyzed to probe the input. Set to 100000 if you are using RTSP and having stream issues.", + "description": lang["fieldTextAduration"], "default": "", "example": "100000", "possible": "" @@ -521,7 +573,7 @@ module.exports = function(s,config,lang){ { "name": "detail=probesize", "field": lang["Probe Size"], - "description": "Specify how big to make the analyzation probe for the input. Set to 100000 if you are using RTSP and having stream issues.", + "description": lang["fieldTextProbesize"], "default": "", "example": "100000", "possible": "" @@ -530,7 +582,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_loop", "field": lang['Loop Stream'], - "description": "Loop a static file so the file stream behaves like a live stream.", + "description": lang["fieldTextStreamLoop"], "default": "1", "example": "", "form-group-class": "h_t_input h_t_mp4 h_t_local", @@ -549,7 +601,7 @@ module.exports = function(s,config,lang){ { "name": "detail=sfps", "field": lang['Monitor Capture Rate'], - "description": "Specify the Frame Rate (FPS) in which the camera is providing its stream in.", + "description": lang["fieldTextSfps"], "default": "", "example": "25", "possible": "" @@ -557,7 +609,7 @@ module.exports = function(s,config,lang){ { "name": "detail=wall_clock_timestamp_ignore", "field": lang['Use Camera Timestamps'], - "description": "Base all incoming camera data in camera time instead of server time.", + "description": lang["fieldTextWallClockTimestampIgnore"], "default": "0", "example": "", "form-group-class": "h_t_input h_t_h264", @@ -577,7 +629,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "height", "field": lang["Height"], - "description": "Height of the stream image.", + "description": lang["fieldTextHeight"], "default": "480", "example": "720, 0 for Auto", "possible": "" @@ -586,7 +638,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "width", "field": lang["Width"], - "description": "Width of the stream image.", + "description": lang["fieldTextWidth"], "default": "640", "example": "1280, 0 for Auto", "possible": "" @@ -594,7 +646,7 @@ module.exports = function(s,config,lang){ { "name": "detail=accelerator", "field": lang.Accelerator, - "description": "Hardware Acceleration (HWAccel) for decoding streams.", + "description": lang["fieldTextAccelerator"], "default": "", "example": "", "selector": "h_gpud", @@ -613,7 +665,7 @@ module.exports = function(s,config,lang){ { "name": "detail=hwaccel", "field": lang.hwaccel, - "description": "Decoding Engine", + "description": lang["fieldTextHwaccel"], "default": "", "example": "", "form-group-class": "h_gpud_input h_gpud_1", @@ -623,7 +675,7 @@ module.exports = function(s,config,lang){ { "name": "detail=hwaccel_vcodec", "field": lang.hwaccel_vcodec, - "description": "Decoding Engine", + "description": lang["fieldTextHwaccelVcodec"], "default": "", "example": "", "form-group-class": "h_gpud_input h_gpud_1", @@ -699,12 +751,15 @@ module.exports = function(s,config,lang){ "form-group-class": "h_gpud_input h_gpud_1", "possible": "" }, - { - "fieldType": 'div', - "id": "monSectionInputMaps" - }, ] }, + "Input Maps": { + "name": lang["Additional Inputs"], + "color": "orange", + "id": "monSectionInputMaps", + "section-class": "pb-0", + "emptyDiv": true + }, "Stream": { "name": lang.Stream, @@ -718,7 +773,7 @@ module.exports = function(s,config,lang){ { "name": "detail=stream_type", "field": lang["Stream Type"], - "description": "The method that will used to consume the video stream.", + "description": lang["fieldTextStreamType"], "default": "mp4", "example": "", "selector": "h_st", @@ -728,16 +783,12 @@ module.exports = function(s,config,lang){ { "name": lang.Poseidon, "value": "mp4", - "info": "Poseidon is built on Kevin Godell's MP4 processing code. It simulates a streaming MP4 file but using the data of a live stream. Includes Audio. Some browsers can play it like a regular MP4 file. Streams over HTTP or WebSocket." - }, - { - "name": lang['HEVC (H.265)'], - "value": "h265" + "info": lang["fieldTextStreamTypePoseidon"] }, { "name": lang['Base64 over Websocket'], "value": "b64", - "info": "Sending Base64 encoded frames over WebSocket. This avoids caching but there is no audio." + "info": lang["fieldTextStreamTypeBase64OverWebsocket"] }, { "name": lang['JPEG (Auto Enables JPEG API)'], @@ -746,29 +797,34 @@ module.exports = function(s,config,lang){ { "name": lang['MJPEG'], "value": "mjpeg", - "info": "Standard Motion JPEG image. No audio." + "info": lang["fieldTextStreamTypeMJPEG"] }, { "name": lang['FLV'], "value": "flv", - "info": "Sending FLV encoded frames over WebSocket." + "info": lang["fieldTextStreamTypeFLV"] }, { "name": lang['HLS (includes Audio)'], "value": "hls", - "info": "Similar method to facebook live streams. Includes audio if input provides it. There is a delay of about 4-6 seconds because this method records segments then pushes them to the client rather than push as while it creates them." + "info": lang["fieldTextStreamTypeHLS(includesAudio)"] + }, + { + "name": lang.useSubStreamOnlyWhenWatching, + "value": "useSubstream", } ] }, { + isAdvanced: true, hidden:true, "name": "detail=stream_flv_type", "field": lang["Connection Type"], - "description": "This is for the Shinobi dashboard only. Both stream methods are still active and ready to use.", + "description": lang["fieldTextStreamFlvType"], "default": "0", "example": "", "fieldType": "select", - "form-group-class": "h_st_input h_st_flv h_st_mp4 h_st_h265", + "form-group-class": "h_st_input h_st_flv h_st_mp4", "possible": [ { "name": lang.HTTP, @@ -781,6 +837,7 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, hidden:true, "name": "detail=stream_flv_maxLatency", "field": lang["Max Latency"], @@ -792,6 +849,7 @@ module.exports = function(s,config,lang){ "possible": "" }, { + isAdvanced: true, hidden:true, "name": "detail=stream_mjpeg_clients", "field": lang["# of Allow MJPEG Clients"], @@ -804,32 +862,32 @@ module.exports = function(s,config,lang){ { "name": "detail=stream_vcodec", "field": lang['Video Codec'], - "description": "Video codec for streaming.", + "description": lang["fieldTextStreamVcodec"], "default": "copy", "example": "", - "form-group-class": "h_st_input h_st_hls h_st_flv h_st_mp4 h_st_h265", + "form-group-class": "h_st_input h_st_hls h_st_flv h_st_mp4", "fieldType": "select", "selector": "h_hls_v", "possible": [ { "name": lang.Auto, "value": "no", - "info": "Let FFMPEG choose." + "info": lang["fieldTextStreamVcodecAuto"] }, { "name": "libx264", "value": "libx264", - "info": "Used for MP4 video." + "info": lang["fieldTextStreamVcodecLibx264"] }, { "name": "libx265", "value": "libx265", - "info": "Used for MP4 video." + "info": lang["fieldTextStreamVcodecLibx265"] }, { "name": lang.copy, "value": "copy", - "info": "Used for MP4 video. Has very low CPU usage but cannot use video filters and filesizes may be gigantic. Best to setup your MP4 settings camera-side when using this option." + "info": lang["fieldTextStreamVcodecCopy"] }, { "name": lang['Hardware Accelerated'], @@ -885,132 +943,126 @@ module.exports = function(s,config,lang){ { "name": "detail=stream_acodec", "field": lang["Audio Codec"], - "description": "Audio codec for streaming.", + "description": lang["fieldTextStreamAcodec"], "default": "0", "example": "", "fieldType": "select", - "form-group-class": "h_st_input h_st_hls h_st_flv h_st_mp4 h_st_h265", + "form-group-class": "h_st_input h_st_hls h_st_flv h_st_mp4", "possible": [ { "name": lang.Auto, - "info": "Let FFMPEG choose.", + "info": lang["fieldTextStreamAcodecAuto"], "value": "" }, { "name": lang["No Audio"], - "info": "No Audio, this is an option that must be set in some parts of the world due to legal reasons.", + "info": lang["fieldTextStreamAcodecNoAudio"], "value": "no" }, { "name": "libvorbis", - "info": "Used for WebM video.", + "info": lang["fieldTextStreamAcodecLibvorbis"], "value": "libvorbis" }, { "name": "libopus", - "info": "Used for WebM video.", + "info": lang["fieldTextStreamAcodecLibopus"], "value": "libopus" }, { "name": "libmp3lame", - "info": "Used for MP4 video.", + "info": lang["fieldTextStreamAcodecLibmp3lame"], "value": "libmp3lame" }, { "name": "aac", - "info": "Used for MP4 video.", + "info": lang["fieldTextStreamAcodecAac"], "value": "aac" }, { "name": "ac3", - "info": "Used for MP4 video.", + "info": lang["fieldTextStreamAcodecAc3"], "value": "ac3" }, { "name": "copy", - "info": "Used for MP4 video. Has very low CPU usage but some audio codecs need custom flags like -strict 2 for aac.", + "info": lang["fieldTextStreamAcodecCopy"], "value": "copy" } ] }, { + isAdvanced: true, "name": "detail=hls_time", "field": "HLS Segment Length", - "description": "How long each video segment should be, in minutes. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.", + "description": lang["fieldTextHlsTime"], "default": "2", - "example": "", - "form-group-class-pre-layer": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", "form-group-class": "h_st_input h_st_hls", - "possible": "" }, { + isAdvanced: true, "name": "detail=hls_list_size", "field": "HLS List Size", - "description": "The number of segments maximum before deleting old segments automatically.", + "description": lang["fieldTextHlsListSize"], "default": "2", - "example": "", - "form-group-class-pre-layer": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", "form-group-class": "h_st_input h_st_hls", - "possible": "" }, { + isAdvanced: true, "name": "detail=preset_stream", "field": "HLS Preset", - "description": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.", - "default": "", + "description": lang["fieldTextPresetStream"], "example": "ultrafast", - "form-group-class-pre-layer": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", - "form-group-class": "h_st_input h_st_hls h_st_flv h_st_mp4 h_st_h265", - "possible": "" + "form-group-class": "h_st_input h_st_hls h_st_flv h_st_mp4", }, { "name": "detail=stream_quality", "field": lang.Quality, - "description": "Low number means higher quality. Higher number means less quality.", + "description": lang["fieldTextStreamQuality"], "default": "15", "example": "1", - "form-group-class": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", + uiVisibilityConditions: 'streamSectionCopyModeVisibilities', "possible": "1-23" }, { "name": "detail=stream_fps", "field": lang['Frame Rate'], - "description": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.", + "description": lang["fieldTextStreamFps"], "default": "", "example": "1", - "form-group-class": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", + uiVisibilityConditions: 'streamSectionCopyModeVisibilities', "possible": "" }, { "name": "detail=stream_scale_x", "field": lang.Width, - "description": "Width of the stream image that is output after processing.", + "description": lang["fieldTextStreamScaleX"], "default": "", "fieldType": "number", "numberMin": "1", "example": "640", - "form-group-class": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", + uiVisibilityConditions: 'streamSectionCopyModeVisibilities', "possible": "" }, { "name": "detail=stream_scale_y", "field": lang.Height, - "description": "Height of the stream image that is output after processing.", + "description": lang["fieldTextStreamScaleY"], "default": "", "fieldType": "number", "numberMin": "1", "example": "480", - "form-group-class": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", + uiVisibilityConditions: 'streamSectionCopyModeVisibilities', "possible": "" }, { "name": "detail=stream_rotate", "field": lang["Rotate"], - "description": "Change the viewing angle of the video stream.", + "description": lang["fieldTextStreamRotate"], "default": "", "example": "", "fieldType": "select", - "form-group-class": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", + uiVisibilityConditions: 'streamSectionCopyModeVisibilities', "possible": [ { "name": lang["No Rotation"], @@ -1039,17 +1091,19 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, "name": "detail=signal_check", "field": lang["Check Signal Interval"], - "description": "How often your client will check the stream to see if it is alive. This is calculated in minutes.", + "description": lang["fieldTextSignalCheck"], "default": "0", "example": "", "possible": "" }, { + isAdvanced: true, "name": "detail=signal_check_log", "field": lang["Log Signal Event"], - "description": "This is for the client side only. It will display in the log thread when client side signal checks occur.", + "description": lang["fieldTextSignalCheckLog"], "default": "0", "example": "", "fieldType": "select", @@ -1065,18 +1119,20 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, "name": "detail=stream_vf", "field": lang["Video Filter"], - "description": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.", + "description": lang["fieldTextStreamVf"], "default": "", "example": "", - "form-group-class": "h_st_input h_st_mjpeg h_st_b64 h_st_hls h_st_gif h_st_flv h_st_mp4 h_st_h265 h_hls_v_input h_hls_v_libx264 h_hls_v_libx265 h_hls_v_h264_nvenc h_hls_v_hevc_nvenc h_hls_v_no", + uiVisibilityConditions: 'streamSectionCopyModeVisibilities', "possible": "" - }, + }, { + isAdvanced: true, "name": "detail=tv_channel", "field": lang["TV Channel"], - "description": "This monitor will have TV Channel features enabled. You will be able to view it in your TV Channel list.", + "description": lang["fieldTextTvChannel"], "default": "", "selector": "h_tvc", "fieldType": "select", @@ -1093,18 +1149,20 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, "name": "detail=tv_channel_id", "field": lang["TV Channel ID"], - "description": "A Custom ID for the Channel.", + "description": lang["fieldTextTvChannelId"], "default": "", "example": "", "form-group-class": "h_tvc_input h_tvc_1", "possible": "" }, { + isAdvanced: true, "name": "detail=tv_channel_group_title", "field": lang["TV Channel Group"], - "description": "A Custom Group for the Channel.", + "description": lang["fieldTextTvChannelGroupTitle"], "default": "", "example": "", "form-group-class": "h_tvc_input h_tvc_1", @@ -1124,7 +1182,7 @@ module.exports = function(s,config,lang){ "name": "detail=stream_timestamp", "selector":"h_stm", "field": lang.Enabled, - "description": "A clock that is burned onto the frames of the video stream.", + "description": lang["fieldTextStreamTimestamp"], "default": "0", "example": "", "fieldType": "select", @@ -1143,7 +1201,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_timestamp_font", "field": "Font Path", - "description": "Font File to style your timestamp.", + "description": lang["fieldTextStreamTimestampFont"], "default": "/usr/share/fonts/truetype/freefont/FreeSans.ttf", "example": "", "form-group-class": "h_stm_input h_stm_1", @@ -1153,7 +1211,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_timestamp_font_size", "field": "Font Size", - "description": "Font size in pt.", + "description": lang["fieldTextStreamTimestampFontSize"], "default": "10", "example": "", "form-group-class": "h_stm_input h_stm_1", @@ -1163,7 +1221,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_timestamp_color", "field": "Text Color", - "description": "Timstamp text color.", + "description": lang["fieldTextStreamTimestampColor"], "default": "white", "example": "", "form-group-class": "h_stm_input h_stm_1", @@ -1173,7 +1231,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_timestamp_box_color", "field": "Text Box Color", - "description": "Timstamp backdrop color.", + "description": lang["fieldTextStreamTimestampBoxColor"], "default": "0x00000000@1", "example": "", "form-group-class": "h_stm_input h_stm_1", @@ -1183,7 +1241,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_timestamp_x", "field": "Position X", - "description": "Horiztonal Position of Timestamp", + "description": lang["fieldTextStreamTimestampX"], "default": "(w-tw)/2", "example": "", "form-group-class": "h_stm_input h_stm_1", @@ -1193,7 +1251,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_timestamp_y", "field": "Position Y", - "description": "Vertical Position of Timestamp", + "description": lang["fieldTextStreamTimestampY"], "default": "0", "example": "", "form-group-class": "h_stm_input h_stm_1", @@ -1212,7 +1270,7 @@ module.exports = function(s,config,lang){ { "name": "detail=stream_watermark", "field": lang.Enabled, - "description": "An image that is burned onto the frames of the video stream.", + "description": lang["fieldTextStreamWatermark"], "default": "0", "example": "", "fieldType": "select", @@ -1232,7 +1290,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_watermark_location", "field": lang['Image Location'], - "description": "Image Location that will be used as Watermark.", + "description": lang["fieldTextStreamWatermarkLocation"], "default": "0", "example": "/usr/share/watermark.logo", "form-group-class": "h_wat_input h_wat_1", @@ -1242,7 +1300,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=stream_watermark_position", "field": lang['Image Position'], - "description": "An image that is burned onto the frames of the video stream.", + "description": lang["fieldTextStreamWatermarkPosition"], "default": "0", "example": "", "fieldType": "select", @@ -1272,8 +1330,568 @@ module.exports = function(s,config,lang){ "name": "Stream Channels", "color": "blue", "id": "monSectionStreamChannels", + "section-class": "pb-0", "emptyDiv": true }, + "Substream": { + "name": lang['Substream'], + "color": "blue", + "isSection": true, + "id": "monSectionSubstream", + "blockquote": lang.substreamText, + "blockquoteClass": 'global_tip', + "info": [ + { + isAdvanced: true, + "name": lang['Connection'], + "color": "orange", + id: "monSectionSubstreamInput", + "blockquote": lang.substreamConnectionText, + "blockquoteClass": 'global_tip', + isSection: true, + isFormGroupGroup: true, + "info": [ + { + name:'detail-substream-input=type', + field:lang['Input Type'], + default:'h264', + attribute:'selector="h_i_SUBSTREAM_FIELDS"', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": "H.264 / H.265 / H.265+", + "value": "h264", + selected: true, + }, + { + "name": "JPEG", + "value": "jpeg" + }, + { + "name": "MJPEG", + "value": "mjpeg" + }, + { + "name": "HLS (.m3u8)", + "value": "hls" + }, + { + "name": "MPEG-4 (.mp4 / .ts)", + "value": "mp4" + }, + { + "name": "Local", + "value": "local" + }, + { + "name": "Raw", + "value": "raw" + } + ] + }, + { + name:'detail-substream-input=stream_flv_type', + field:lang['Loop Stream'], + class:'h_i_SUBSTREAM_FIELDS_input h_i_SUBSTREAM_FIELDS_mp4', + hidden:true, + default:'0', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.HTTP, + "value": "http", + }, + { + "name": lang.Websocket, + "value": "ws", + } + ] + }, + { + name:'detail-substream-input=fulladdress', + field:lang['Full URL Path'], + placeholder:'Example : rtsp://admin:password@123.123.123.123/stream/1', + type:'text', + }, + { + name:'detail-substream-input=sfps', + field:lang['Monitor Capture Rate'], + placeholder:'', + type:'text', + }, + { + name:'detail-substream-input=aduration', + field:lang['Analyzation Duration'], + placeholder:'Example : 1000000', + type:'text', + }, + { + name:'detail-substream-input=probesize', + field:lang['Probe Size'], + placeholder:'Example : 1000000', + type:'text', + }, + { + name:'detail-substream-input=stream_loop', + field:lang['Loop Stream'], + class:'h_i_SUBSTREAM_FIELDS_input h_i_SUBSTREAM_FIELDS_mp4 h_i_SUBSTREAM_FIELDS_raw', + hidden:true, + default:'0', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.No, + "value": "0", + }, + { + "name": lang.Yes, + "value": "1", + selected: true, + } + ] + }, + { + name:'detail-substream-input=rtsp_transport', + field:lang['RTSP Transport'], + class:'h_i_SUBSTREAM_FIELDS_input h_i_SUBSTREAM_FIELDS_h264', + default:'', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.Auto, + "value": "", + "info": lang["fieldTextDetailSubstreamInputRtspTransportAuto"], + selected: true, + }, + { + "name": "TCP", + "value": "tcp", + "info": lang["fieldTextDetailSubstreamInputRtspTransportTCP"] + }, + { + "name": "UDP", + "value": "udp", + "info": lang["fieldTextDetailSubstreamInputRtspTransportUDP"] + } + ] + }, + { + name:'detail-substream-input=accelerator', + field:lang['Accelerator'], + attribute:'selector="h_accel_SUBSTREAM_FIELDS"', + default:'0', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.No, + "value": "0", + selected: true, + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }, + { + name:'detail-substream-input=hwaccel', + field:lang['hwaccel'], + class:'h_accel_SUBSTREAM_FIELDS_input h_accel_SUBSTREAM_FIELDS_1', + hidden:true, + default:'', + "fieldType": "select", + type:'selector', + possible: s.listOfHwAccels + }, + { + name:'detail-substream-input=hwaccel_vcodec', + field:lang['hwaccel_vcodec'], + class:'h_accel_SUBSTREAM_FIELDS_input h_accel_SUBSTREAM_FIELDS_1', + hidden:true, + default:'auto', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.Auto + '('+lang.Recommended+')', + "value": "", + selected: true, + }, + { + "name": lang.NVIDIA, + "optgroup": [ + { + "name": lang.h264_cuvid, + "value": "h264_cuvid" + }, + { + "name": lang.hevc_cuvid, + "value": "hevc_cuvid" + }, + { + "name": lang.mjpeg_cuvid, + "value": "mjpeg_cuvid" + }, + { + "name": lang.mpeg4_cuvid, + "value": "mpeg4_cuvid" + }, + ] + }, + { + "name": lang["Quick Sync Video"], + "optgroup": [ + { + "name": lang.h264_qsv, + "value": "h264_qsv" + }, + { + "name": lang.hevc_qsv, + "value": "hevc_qsv" + }, + { + "name": lang.mpeg2_qsv, + "value": "mpeg2_qsv" + }, + ] + }, + { + "name": lang['Raspberry Pi'], + "optgroup": [ + { + "name": lang.h264_mmal, + "value": "h264_mmal" + }, + { + "name": lang.mpeg2_mmal, + "value": "mpeg2_mmal" + }, + { + "name": lang["MPEG-4 (Raspberry Pi)"], + "value": "mpeg4_mmal" + } + ] + }, + ] + }, + { + name:'detail-substream-input=hwaccel_device', + field:lang['hwaccel_device'], + class:'h_accel_SUBSTREAM_FIELDS_input h_accel_SUBSTREAM_FIELDS_1', + hidden:true, + placeholder:'Example : /dev/dri/video0', + type:'text', + }, + { + name:'detail-substream-input=cust_input', + field:lang['Input Flags'], + type:'text', + }, + ] + }, + { + "name": lang['Output'], + "color": "blue", + id: "monSectionSubstreamOutput", + "blockquote": lang.substreamOutputText, + "blockquoteClass": 'global_tip', + isSection: true, + isFormGroupGroup: true, + "info": [ + { + "field": lang["Stream Type"], + "name": `detail-substream-output="stream_type"`, + "description": lang["fieldTextDetailSubstreamOutputStreamType"], + "default": "hls", + "selector": "h_st_channel_SUBSTREAM_FIELDS", + "fieldType": "select", + "attribute": `triggerChange="#monSectionChannelSUBSTREAM_FIELDS [detail-substream-output=stream_vcodec]" triggerChangeIgnore="b64,mjpeg"`, + "possible": [ + { + "name": lang.Poseidon, + "value": "mp4", + }, + { + "name": lang['MJPEG'], + "value": "mjpeg", + }, + { + "name": lang['FLV'], + "value": "flv", + }, + { + "name": lang['HLS (includes Audio)'], + "value": "hls", + selected: true, + } + ] + }, + { + "field": lang['# of Allow MJPEG Clients'], + "name": `detail-substream-output="stream_mjpeg_clients"`, + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg", + "placeholder": "20", + }, + { + "field": lang['Video Codec'], + "name": `detail-substream-output="stream_vcodec"`, + "description": lang["fieldTextDetailSubstreamOutputStreamVcodec"], + "default": "copy", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + "fieldType": "select", + "selector": "h_hls_v_channel_SUBSTREAM_FIELDS", + "possible": [ + { + "name": lang.Auto, + "value": "no", + "info": lang["fieldTextDetailSubstreamOutputStreamVcodecAuto"], + selected: true, + }, + { + "name": "libx264", + "value": "libx264", + "info": lang["fieldTextDetailSubstreamOutputStreamVcodecLibx264"] + }, + { + "name": "libx265", + "value": "libx265", + "info": lang["fieldTextDetailSubstreamOutputStreamVcodecLibx265"] + }, + { + "name": lang.copy, + "value": "copy", + "info": lang["fieldTextDetailSubstreamOutputStreamVcodecCopy"] + }, + { + "name": lang['Hardware Accelerated'], + "optgroup": [ + { + "name": "H.264 VA-API (Intel HW Accel)", + "value": "h264_vaapi" + }, + { + "name": "H.265 VA-API (Intel HW Accel)", + "value": "hevc_vaapi" + }, + { + "name": "H.264 NVENC (NVIDIA HW Accel)", + "value": "h264_nvenc" + }, + { + "name": "H.265 NVENC (NVIDIA HW Accel)", + "value": "hevc_nvenc" + }, + { + "name": "H.264 (Quick Sync Video)", + "value": "h264_qsv" + }, + { + "name": "H.265 (Quick Sync Video)", + "value": "hevc_qsv" + }, + { + "name": "MPEG2 (Quick Sync Video)", + "value": "mpeg2_qsv" + }, + { + "name": "H.264 (Quick Sync Video)", + "value": "h264_qsv" + }, + { + "name": "H.265 (Quick Sync Video)", + "value": "hevc_qsv" + }, + { + "name": "MPEG2 (Quick Sync Video)", + "value": "mpeg2_qsv" + }, + { + "name": "H.264 openMAX (Raspberry Pi)", + "value": "h264_omx" + } + ] + }, + ] + }, + { + "field": lang["Audio Codec"], + "name": `detail-substream-output="stream_acodec"`, + "description": lang["fieldTextDetailSubstreamOutputStreamAcodec"], + "default": "", + "example": "", + "fieldType": "select", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + "possible": [ + { + "name": lang.Auto, + "info": lang["fieldTextDetailSubstreamOutputStreamAcodecAuto"], + "value": "", + selected: true, + }, + { + "name": lang["No Audio"], + "info": lang["fieldTextDetailSubstreamOutputStreamAcodecNoAudio"], + "value": "no" + }, + { + "name": "libvorbis", + "info": lang["fieldTextDetailSubstreamOutputStreamAcodecLibvorbis"], + "value": "libvorbis" + }, + { + "name": "libopus", + "info": lang["fieldTextDetailSubstreamOutputStreamAcodecLibopus"], + "value": "libopus" + }, + { + "name": "libmp3lame", + "info": lang["fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame"], + "value": "libmp3lame" + }, + { + "name": "aac", + "info": lang["fieldTextDetailSubstreamOutputStreamAcodecAac"], + "value": "aac" + }, + { + "name": "ac3", + "info": lang["fieldTextDetailSubstreamOutputStreamAcodecAc3"], + "value": "ac3" + }, + { + "name": "copy", + "info": lang["fieldTextDetailSubstreamOutputStreamAcodecCopy"], + "value": "copy" + } + ] + }, + { + "name": "detail-substream-output=hls_time", + "field": lang["HLS Segment Length"], + "description": lang["fieldTextDetailSubstreamOutputHlsTime"], + "default": "2", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls", + }, + { + "name": "detail-substream-output=hls_list_size", + "field": lang["HLS List Size"], + "description": lang["fieldTextDetailSubstreamOutputHlsListSize"], + "default": "2", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls", + }, + { + "name": "detail-substream-output=preset_stream", + "field": lang["HLS Preset"], + "description": lang["fieldTextDetailSubstreamOutputPresetStream"], + "example": "ultrafast", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_hls", + }, + { + "name": "detail-substream-output=stream_quality", + "field": lang.Quality, + "description": lang["fieldTextDetailSubstreamOutputStreamQuality"], + "default": "15", + "example": "1", + // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + "possible": "1-23" + }, + { + "name": "detail-substream-output=stream_v_br", + "field": lang["Video Bit Rate"], + "placeholder": "", + "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + }, + { + "name": "detail-substream-output=stream_a_br", + "field": lang["Audio Bit Rate"], + "placeholder": "128k", + "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + }, + { + "name": "detail-substream-output=stream_fps", + "field": lang['Frame Rate'], + "description": lang["fieldTextDetailSubstreamOutputStreamFps"], + // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + }, + { + "name": "detail-substream-output=stream_scale_x", + "field": lang.Width, + "description": lang["fieldTextDetailSubstreamOutputStreamScaleX"], + "fieldType": "number", + "numberMin": "1", + "example": "640", + // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + }, + { + "name": "detail-substream-output=stream_scale_y", + "field": lang.Height, + "description": lang["fieldTextDetailSubstreamOutputStreamScaleY"], + "fieldType": "number", + "numberMin": "1", + "example": "480", + // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + }, + { + "name": "detail-substream-output=stream_rotate", + "field": lang["Rotate"], + "description": lang["fieldTextDetailSubstreamOutputStreamRotate"], + "fieldType": "select", + // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + "possible": [ + { + "name": lang["No Rotation"], + "value": "no" + }, + { + "name": lang["180 Degrees"], + "value": "2,transpose=2" + }, + { + "name": lang["90 Counter Clockwise and Vertical Flip (default)"], + "value": "0" + }, + { + "name": lang["90 Clockwise"], + "value": "1" + }, + { + "name": lang["90 Clockwise and Vertical Flip"], + "value": "2" + }, + { + "name": lang["90 Clockwise and Vertical Flip"], + "value": "3" + } + ] + }, + { + isAdvanced: true, + "name": "detail-substream-output=svf", + "field": lang["Video Filter"], + "description": lang["fieldTextDetailSubstreamOutputSvf"], + // "form-group-class-pre-layer": "h_hls_v_channel_SUBSTREAM_FIELDS_input h_hls_v_channel_SUBSTREAM_FIELDS_libx264 h_hls_v_channel_SUBSTREAM_FIELDS_libx265 h_hls_v_channel_SUBSTREAM_FIELDS_h264_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_hevc_nvenc h_hls_v_channel_SUBSTREAM_FIELDS_no", + "form-group-class": "h_st_channel_SUBSTREAM_FIELDS_input h_st_channel_SUBSTREAM_FIELDS_mjpeg h_st_channel_SUBSTREAM_FIELDS_hls h_st_channel_SUBSTREAM_FIELDS_rtmp h_st_channel_SUBSTREAM_FIELDS_jsmpeg h_st_channel_SUBSTREAM_FIELDS_flv h_st_channel_SUBSTREAM_FIELDS_mp4 h_st_channel_SUBSTREAM_FIELDS_h264", + }, + { + "name": "detail-substream-output=cust_stream", + "field": lang["Stream Flags"], + }, + ] + }, + ] + }, "JPEG API": { "name": lang['JPEG API'], "headerTitle": `${lang['JPEG API']} ${lang.Snapshot} (cgi-bin)`, @@ -1285,7 +1903,7 @@ module.exports = function(s,config,lang){ { "name": "detail=snap", "field": lang.Enabled, - "description": "Get the latest frame in JPEG.", + "description": lang["fieldTextSnap"], "default": "0", "example": "", "fieldType": "select", @@ -1332,6 +1950,7 @@ module.exports = function(s,config,lang){ "possible": "" }, { + isAdvanced: true, hidden: true, "name": "detail=snap_vf", "field": lang['Video Filter'], @@ -1356,7 +1975,7 @@ module.exports = function(s,config,lang){ // { // "name": "height", // "field": lang.Height, - // "description": "Height of the stream image.", + // "description": lang["fieldTextRecordScaleY"], // "default": "640", // "example": "1280", // "possible": "" @@ -1364,7 +1983,7 @@ module.exports = function(s,config,lang){ // { // "name": "width", // "field": lang.Width, - // "description": "Width of the stream image.", + // "description": lang["fieldTextRecordScaleX"], // "default": "480", // "example": "720", // "possible": "" @@ -1372,7 +1991,7 @@ module.exports = function(s,config,lang){ { "name": "ext", "field": lang["Record File Type"], - "description": "The file type for your recorded video file.", + "description": lang["fieldTextExt"], "default": "MP4", "example": "", "selector": "h_f", @@ -1381,19 +2000,19 @@ module.exports = function(s,config,lang){ { "name": "MP4", "value": "mp4", - "info": "This file type is playable is almost all modern web browsers, that includes mobile. The filesize just tends to be larger unless you lower the quality." + "info": lang["fieldTextExtMP4"] }, { "name": "WebM", "value": "webm", - "info": "Small filesize, low client compatibility. Good for uploading to sites like YouTube." + "info": lang["fieldTextExtWebM"] } ] }, { "name": "detail=vcodec", "field": lang["Video Codec"], - "description": "Video codec for recording.", + "description": lang["fieldTextVcodec"], "default": "copy", "example": "", "selector": "h_vc", @@ -1496,16 +2115,17 @@ module.exports = function(s,config,lang){ { "name": "detail=crf", "field": lang.Quality, - "description": "Low number means higher quality. Higher number means less quality.", + "description": lang["fieldTextCrf"], "default": "15", "example": "1", "form-group-class": "h_vc_input h_vc_libvpx h_vc_libvpx-vp9 h_vc_libx264 h_vc_libx265 h_vc_hevc_nvenc h_vc_h264_nvenc h_vc_h264_vaapi h_vc_hevc_vaapi h_vc_h264_qsv h_vc_hevc_qsv h_vc_mpeg2_qsv h_vc_default h_vc_none", "possible": "1-23" }, { + isAdvanced: true, "name": "detail=preset_record", "field": lang.Preset, - "description": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.", + "description": lang["fieldTextPresetRecord"], "default": "", "example": "ultrafast", "form-group-class": "h_vc_input h_vc_libvpx h_vc_libvpx-vp9 h_vc_libx264 h_vc_libx265 h_vc_hevc_nvenc h_vc_h264_nvenc h_vc_h264_vaapi h_vc_hevc_vaapi h_vc_h264_qsv h_vc_hevc_qsv h_vc_mpeg2_qsv h_vc_default h_vc_none", @@ -1514,7 +2134,7 @@ module.exports = function(s,config,lang){ { "name": "detail=acodec", "field": lang['Audio Codec'], - "description": "Audio codec for recording.", + "description": lang["fieldTextAcodec"], "default": "0", "example": "", "fieldType": "select", @@ -1570,7 +2190,7 @@ module.exports = function(s,config,lang){ { "name": "fps", "field": lang["Video Record Rate"], - "description": "The speed in which frames are recorded to files, Frames Per Second. Be aware there is no default. This can lead to large files. Best to set this camera-side.", + "description": lang["fieldTextFps"], "default": "", "example": "2", "form-group-class": "h_vc_input h_vc_libvpx h_vc_libvpx-vp9 h_vc_libx264 h_vc_libx265 h_vc_hevc_nvenc h_vc_h264_nvenc h_vc_h264_vaapi h_vc_hevc_vaapi h_vc_h264_qsv h_vc_hevc_qsv h_vc_mpeg2_qsv h_vc_default h_vc_none", @@ -1597,7 +2217,7 @@ module.exports = function(s,config,lang){ { "name": "detail=cutoff", "field": lang['Recording Segment Interval'], - "description": "In minutes. When to slice off and start a new video file.", + "description": lang["fieldTextCutoff"], "default": "15", "example": "60", "attribute": `triggerChange="#add_monitor [detail=vcodec]"`, @@ -1606,7 +2226,7 @@ module.exports = function(s,config,lang){ { "name": "detail=rotate", "field": lang["Rotate"], - "description": "Change the recording angle of the video stream.", + "description": lang["fieldTextRotate"], "default": "copy", "example": "", "selector": "h_vc", @@ -1640,9 +2260,10 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, "name": "detail=vf", "field": lang['Record Video Filter'], - "description": "Place FFMPEG video filters in this box to affect the recording portion. No spaces.", + "description": lang["fieldTextVf"], "default": "", "example": "", "form-group-class": "h_vc_input h_vc_libvpx h_vc_libvpx-vp9 h_vc_libx264 h_vc_libx265 h_vc_hevc_nvenc h_vc_h264_nvenc h_vc_h264_vaapi h_vc_hevc_vaapi h_vc_h264_qsv h_vc_hevc_qsv h_vc_mpeg2_qsv h_vc_default h_vc_none", @@ -1663,7 +2284,7 @@ module.exports = function(s,config,lang){ "name": "detail=timestamp", "selector":"h_rtm", "field": lang.Enabled, - "description": "A clock that is burned onto the frames of the recorded video.", + "description": lang["fieldTextTimestamp"], "default": "0", "example": "", "fieldType": "select", @@ -1682,7 +2303,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=timestamp_font", "field": "Font Path", - "description": "Font File to style your timestamp.", + "description": lang["fieldTextTimestampFont"], "default": "/usr/share/fonts/truetype/freefont/FreeSans.ttf", "example": "", "form-group-class": "h_rtm_input h_rtm_1", @@ -1692,7 +2313,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=timestamp_font_size", "field": "Font Size", - "description": "Font size in pt.", + "description": lang["fieldTextTimestampFontSize"], "default": "10", "example": "", "form-group-class": "h_rtm_input h_rtm_1", @@ -1702,7 +2323,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=timestamp_color", "field": "Text Color", - "description": "Timstamp text color.", + "description": lang["fieldTextTimestampColor"], "default": "white", "example": "", "form-group-class": "h_rtm_input h_rtm_1", @@ -1712,7 +2333,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=timestamp_box_color", "field": "Text Box Color", - "description": "Timstamp backdrop color.", + "description": lang["fieldTextTimestampBoxColor"], "default": "0x00000000@1", "example": "", "form-group-class": "h_rtm_input h_rtm_1", @@ -1722,7 +2343,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=timestamp_x", "field": "Position X", - "description": "Horiztonal Position of Timestamp", + "description": lang["fieldTextTimestampX"], "default": "(w-tw)/2", "example": "", "form-group-class": "h_rtm_input h_rtm_1", @@ -1732,7 +2353,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=timestamp_y", "field": "Position Y", - "description": "Vertical Position of Timestamp", + "description": lang["fieldTextTimestampY"], "default": "0", "example": "", "form-group-class": "h_rtm_input h_rtm_1", @@ -1752,7 +2373,7 @@ module.exports = function(s,config,lang){ { "name": "detail=watermark", "field": lang.Enabled, - "description": "An image that is burned onto the frames of the recorded video.", + "description": lang["fieldTextWatermark"], "default": "0", "example": "", "fieldType": "select", @@ -1772,7 +2393,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=watermark_location", "field": lang['Image Location'], - "description": "Image Location that will be used as Watermark.", + "description": lang["fieldTextWatermarkLocation"], "default": "0", "example": "/usr/share/watermark.logo", "form-group-class": "h_wat_input h_wat_1", @@ -1782,7 +2403,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=watermark_position", "field": lang['Image Position'], - "description": "An image that is burned onto the frames of the recorded video.", + "description": lang["fieldTextWatermarkPosition"], "default": "0", "example": "", "fieldType": "select", @@ -1818,7 +2439,7 @@ module.exports = function(s,config,lang){ { "name": "detail=record_timelapse", "field": lang.Enabled, - "description": "Create a JPEG based timelapse.", + "description": lang["fieldTextRecordTimelapse"], "default": "0", "example": "", "fieldType": "select", @@ -1838,7 +2459,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=record_timelapse_mp4", "field": lang.Enabled, - "description": "Create an MP4 file at the end of each day for the timelapse.", + "description": lang["fieldTextRecordTimelapseMp4"], "default": "0", "example": "", "fieldType": "select", @@ -1857,9 +2478,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=record_timelapse_fps", "field": lang['Creation Interval'], - "description": "", "default": "900", - "example": "", "form-group-class": "h_rec_ti_input h_rec_ti_1", "fieldType": "select", "possible": [ @@ -1875,6 +2494,10 @@ module.exports = function(s,config,lang){ "name": `.5 ${lang.minutes}`, "value": "30" }, + { + "name": `1 ${lang.minute}`, + "value": "60" + }, { "name": `5 ${lang.minutes}`, "value": "300" @@ -1905,31 +2528,20 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=record_timelapse_scale_x", "field": lang['Image Width'], - "description": "", - "default": "", - "example": "", "form-group-class": "h_rec_ti_input h_rec_ti_1", - "possible": "" }, { hidden: true, "name": "detail=record_timelapse_scale_y", "field": lang['Image Height'], - "description": "", - "default": "", - "example": "", "form-group-class": "h_rec_ti_input h_rec_ti_1", - "possible": "" }, { + isAdvanced: true, hidden: true, "name": "detail=record_timelapse_vf", "field": lang['Video Filter'], - "description": "", - "default": "", - "example": "", "form-group-class": "h_rec_ti_input h_rec_ti_1", - "possible": "" }, ] }, @@ -1945,7 +2557,7 @@ module.exports = function(s,config,lang){ { "name": "detail=record_timelapse_watermark", "field": lang.Enabled, - "description": "An image that is burned onto the frames of the recorded video.", + "description": lang["fieldTextRecordTimelapseWatermark"], "default": "0", "example": "", "fieldType": "select", @@ -1965,7 +2577,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=record_timelapse_watermark_location", "field": lang['Image Location'], - "description": "Image Location that will be used as Watermark.", + "description": lang["fieldTextRecordTimelapseWatermarkLocation"], "default": "0", "example": "/usr/share/watermark.logo", "form-group-class": "h_wat_timelapse_input h_wat_timelapse_1", @@ -1975,7 +2587,7 @@ module.exports = function(s,config,lang){ hidden: true, "name": "detail=record_timelapse_watermark_position", "field": lang['Image Position'], - "description": "An image that is burned onto the frames of the recorded video.", + "description": lang["fieldTextRecordTimelapseWatermarkPosition"], "default": "0", "example": "", "fieldType": "select", @@ -2001,106 +2613,10 @@ module.exports = function(s,config,lang){ }, ] }, - "Custom": { - "name": lang.Custom, - "color": "navy", - "isSection": true, - isAdvanced: true, - "id": "monSectionCustom", - "info": [ - { - "name": "detail=cust_input", - "field": lang['Input Flags'], - "description": "Custom Flags that bind to the Input of the FFMPEG process.", - "default": "", - "example": "", - "possible": "" - }, - // { - // hidden: true, - // "name": "detail=cust_rtmp", - // "field": lang['RTMP Stream Flags'], - // "description": "Custom Flags that bind to the RTMP stream.", - // "default": "", - // "example": "", - // "form-group-class": "h_rtmp_input h_rtmp_1", - // "possible": "" - // }, - { - "name": "detail=cust_stream", - "field": lang["Stream Flags"], - "description": "Custom Flags that bind to the Stream (client side view) of the FFMPEG process.", - "default": "", - "example": "", - "form-group-class": "", - "possible": "" - }, - { - hidden: true, - "name": "detail=cust_snap", - "field": "Snapshot Flags", - "description": "Custom Flags that bind to the Snapshots.", - "default": "", - "example": "", - "form-group-class": "h_sn_input h_sn_1", - "possible": "" - }, - { - hidden: true, - "name": "detail=cust_record", - "field": lang["Recording Flags"], - "description": "Custom Flags that bind to the recording of the FFMPEG process.", - "default": "", - "example": "", - "form-group-class": "h_m_input h_m_record", - "possible": "" - }, - { - hidden: true, - "name": "detail=cust_detect", - "field": lang["Detector Flags"], - "description": "Custom Flags that bind to the stream Detector uses for analyzation.", - "default": "", - "example": "", - "form-group-class": "shinobi-detector", - "possible": "" - }, - { - hidden: true, - "name": "detail=cust_detect_object", - "field": lang["Object Detector Flags"], - "description": "Custom Flags that bind to the stream Detector uses for analyzation.", - "default": "", - "example": "", - "form-group-class": "shinobi-detector", - "possible": "" - }, - { - hidden: true, - "name": "detail=cust_sip_record", - "field": lang['Traditional Recording Flags'], - "description": "Custom Flags that bind to the output that the Event-Based Recordings siphon from.", - "default": "", - "example": "", - "form-group-class": "h_rec_mtd_input h_rec_mtd_sip", - "possible": "" - }, - { - "name": "detail=custom_output", - "field": "Output Method", - "description": "Add a custom output like JPEG frames or send data straight to another server.", - "default": "", - "example": "", - "form-group-class": "", - "possible": "" - } - ] - }, "Detector": { - "name": lang['Global Detector Settings'], - "headerTitle": `${lang['Global Detector Settings']} ${lang['Primary Engine']} : Pixel Array ${lang['Not Connected']}`, + "name": lang['Detector Settings'], + "headerTitle": `${lang['Detector Settings']} ${lang['Primary Engine']} : Pixel Array ${lang['Not Connected']}`, "color": "orange", - isAdvanced: true, "isSection": true, "input-mapping":"detector", "id": "monSectionDetector", @@ -2108,10 +2624,21 @@ module.exports = function(s,config,lang){ "attribute": `triggerChange="#add_monitor [detail=detector_record_method]"`, "blockquote": `${lang.DetectorText}\n

`, "info": [ + { + "fieldType": "btn", + "class": `btn-primary open-region-editor`, + "btnContent": `   ${lang['Region Editor']}`, + "description": "", + "default": "", + "example": "", + "form-group-class-pre-pre-layer": "h_det_input h_det_1", + "form-group-class-pre-layer": "form-group", + "possible": "" + }, { "name": "detail=detector", "field": lang.Enabled, - "description": "This will add another output in the FFMPEG command for the motion detector.", + "description": lang["fieldTextDetector"], "default": "0", "example": "", "fieldType": "select", @@ -2127,98 +2654,11 @@ module.exports = function(s,config,lang){ } ] }, - { - "name": "detail=detector_http_api", - "field": lang["Allow API Trigger"], - "description": "Do you want to allow HTTP triggers to this camera?", - "default": "1", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": `${lang.Always} (${lang.Default})`, - "value": "1" - }, - { - "name": lang[`When Detector is On`], - "value": "2" - }, - { - "name": lang[`When Detector is Off`], - "value": "3" - }, - { - "name": lang.Never, - "value": "0" - } - ] - }, - { - hidden: true, - "name": "detail=detector_send_frames", - "field": lang["Send Frames"], - "description": "Push frames to the connected plugin to be analyzed.", - "default": "0", - "example": "", - "selector": "h_det_fra", - "form-group-class": "h_det_input h_det_1", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - hidden: true, - "form-group-class": "h_det_input h_det_1", - "name": "detail=detector_fps", - "field": lang["Detector Rate"], - "description": "How many frames a second to send to the motion detector; 2 is the default.", - "default": "2", - "example": "", - "possible": "" - }, - { - hidden: true, - "form-group-class": "h_det_input h_det_1", - "name": "detail=detector_scale_x", - "field": lang["Feed-in Image Width"], - "description": "Width of the image being detected. Smaller sizes take less CPU.", - "default": "", - "example": "640", - "possible": "" - }, - { - hidden: true, - "form-group-class": "h_det_input h_det_1", - "name": "detail=detector_scale_y", - "field": lang["Feed-in Image Height"], - "description": "Height of the image being detected. Smaller sizes take less CPU.", - "default": "", - "example": "480", - "possible": "" - }, - { - hidden: true, - "name": "detail=detector_lock_timeout", - "field": lang['Allow Next Trigger'], - "description": "Lockout for when the next trigger is allowed, to avoid overloading the database and receiving clients. Measured in milliseconds.", - "default": "2000", - "example": "", - "form-group-class": "h_det_input h_det_1", - "possible": "" - }, { hidden: true, "name": "detail=detector_save", - "field": lang["Save Events to SQL"], - "description": "Save Motion Events in SQL. This will allow display of motion over video during the time motion events occured in the Power Viewer.", + "field": lang["Save Events"], + "description": lang["fieldTextDetectorSave"], "default": "1", "example": "", "form-group-class": "h_det_input h_det_1", @@ -2235,10 +2675,53 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, + "name": "detail=use_detector_filters", + "field": lang['Event Filters'], + "description": lang.fieldTextEventFilters, + "default": "0", + "example": "", + "selector": "h_det_fil", + "fieldType": "select", + "form-group-class-pre-layer": "h_det_input h_det_1", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + isAdvanced: true, + "name": "detail=use_detector_filters_object", + "field": lang['Filter for Objects only'], + "description": "", + "default": "0", + "example": "", + "fieldType": "select", + "form-group-class": "h_det_fil_input h_det_fil_1", + "form-group-class-pre-layer": "h_det_input h_det_1", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + isAdvanced: true, hidden: true, "name": "detail=detector_record_method", "field": lang['How to Record'], - "description": "There are multiple ways to begin recording when an event occurs, like motion. Traditional Recording is the most user-friendly.", + "description": lang["fieldTextDetectorRecordMethod"], "selector": "h_rec_mtd", "default": "sip", "example": "", @@ -2246,11 +2729,11 @@ module.exports = function(s,config,lang){ "fieldType": "select", "possible": [ { - "name": lang['Traditional (Watch-Only, Includes Buffer)'], + "name": lang['Event-Based Recording (For Watch-Only Mode)'], "value": "sip" }, { - "name": lang['Delete Motionless Videos (Record)'], + "name": lang['Delete Motionless Videos (For Record Mode)'], "value": "del" } ] @@ -2277,16 +2760,12 @@ module.exports = function(s,config,lang){ ] }, { - hidden: true, - "name": "detail=detector_trigger_record_fps", - "field": lang['Recording FPS Change on Start'], - "description": "", - "placeholder": lang['Blank for No Change'], - "default": "", - "example": "", + "name": "detail=detector_buffer_seconds_before", + "field": lang['Buffer Time from Event'], + "description": lang["fieldTextBufferTimeFromEvent"], + "default": "5", "form-group-class": "h_det_input h_det_1", - "form-group-class-pre-layer": "h_rec_mtd_input h_rec_mtd_hot h_rec_mtd_sip", - "possible": "" + "form-group-class-pre-layer": "h_rec_mtd_input h_rec_mtd_sip", }, { hidden: true, @@ -2299,24 +2778,12 @@ module.exports = function(s,config,lang){ "form-group-class-pre-layer": "h_rec_mtd_input h_rec_mtd_hot h_rec_mtd_sip", "possible": "" }, - { - hidden: true, - "name": "detail=detector_send_video_length", - "field": lang["Notification Video Length"], - "description": "In seconds. The length of the video that gets sent to your Notification service, like Email or Discord.", - "default": "10", - "example": "", - "form-group-class": "h_det_input h_det_1", - "form-group-class-pre-layer": "h_rec_mtd_input h_rec_mtd_hot h_rec_mtd_sip", - "possible": "" - }, { hidden: true, "name": "detail=watchdog_reset", "field": lang["Timeout Reset on Next Event"], - "description": "If there is an overlap in trigger record should it reset.", + "description": lang["fieldTextWatchdogReset"], "default": "1", - "example": "", "fieldType": "select", "form-group-class": "h_det_input h_det_1", "form-group-class-pre-layer": "h_rec_mtd_input h_rec_mtd_hot h_rec_mtd_sip", @@ -2332,12 +2799,11 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, hidden: true, "name": "detail=detector_delete_motionless_videos", "field": lang['Delete Motionless Video'], - "description": "", "default": "0", - "example": "", "form-group-class": "h_det_input h_det_1", "form-group-class-pre-layer": "h_rec_mtd_input h_rec_mtd_del", "fieldType": "select", @@ -2353,6 +2819,122 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, + "name": "detail=detector_http_api", + "field": lang["Allow API Trigger"], + "description": lang["fieldTextDetectorHttpApi"], + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": `${lang.Always} (${lang.Default})`, + "value": "1" + }, + { + "name": lang[`When Detector is On`], + "value": "2" + }, + { + "name": lang[`When Detector is Off`], + "value": "3" + }, + { + "name": lang.Never, + "value": "0" + } + ] + }, + { + isAdvanced: true, + hidden: true, + "name": "detail=detector_send_frames", + "field": lang["Send Frames"], + "description": lang["fieldTextDetectorSendFrames"], + "default": "0", + "example": "", + "selector": "h_det_fra", + "form-group-class": "h_det_input h_det_1", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + isAdvanced: true, + hidden: true, + "form-group-class": "h_det_input h_det_1", + "name": "detail=detector_fps", + "field": lang["Detector Rate"], + "description": lang["fieldTextDetectorFps"], + "default": "2", + "example": "", + "possible": "" + }, + { + isAdvanced: true, + hidden: true, + "form-group-class": "h_det_input h_det_1", + "name": "detail=detector_scale_x", + "field": lang["Feed-in Image Width"], + "description": lang["fieldTextDetectorScaleX"], + "default": "", + "example": "640", + "possible": "" + }, + { + isAdvanced: true, + hidden: true, + "form-group-class": "h_det_input h_det_1", + "name": "detail=detector_scale_y", + "field": lang["Feed-in Image Height"], + "description": lang["fieldTextDetectorScaleY"], + "default": "", + "example": "480", + "possible": "" + }, + { + isAdvanced: true, + hidden: true, + "name": "detail=detector_lock_timeout", + "field": lang['Allow Next Trigger'], + "description": lang["fieldTextDetectorLockTimeout"], + "default": "2000", + "example": "", + "form-group-class": "h_det_input h_det_1", + "possible": "" + }, + { + isAdvanced: true, + "name": "detail=detector_send_video_length", + "field": lang["Notification Video Length"], + "description": lang["fieldTextDetectorSendVideoLength"], + "default": "10" + }, + { + isAdvanced: true, + "name": "detail=snap_seconds_inward", + "field": lang['Delay for Snapshot'], + "description": lang[lang["fieldTextSnapSecondsInward"]], + "default": "0" + }, + { + hidden: true, + "name": "detail=cords", + }, + { + hidden: true, + "name": "detail=detector_filters", + }, + { + isAdvanced: true, hidden: true, "name": "detail=det_multi_trig", "field": lang['Trigger Camera Groups'], @@ -2374,6 +2956,7 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, hidden: true, id: "monSectionDetectorGroupMulti", "name": lang['Trigger Camera Groups'], @@ -2398,6 +2981,490 @@ module.exports = function(s,config,lang){ }, ], }, + { + hidden: true, + "name": lang['Motion Detection'], + "headerTitle": `${lang['Motion Detection']} ${lang['Primary Engine']} : Pixel Array ${lang['Not Connected']}`, + "color": "orange", + id: "monSectionDetectorMotion", + isSection: true, + isFormGroupGroup: true, + "section-class": "h_det_input h_det_1", + "info": [ + { + "name": "detail=detector_pam", + "field": lang["Use Built-In"], + "description": lang["fieldTextDetectorPam"], + "selector": "h_det_pam", + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + // { + // "name": "detail=detector_show_matrix", + // "field": lang["Show Matrices"], + // "description": "Outline which pixels are detected as changed in one matrix.", + // "default": "0", + // "example": "", + // "fieldType": "select", + // "form-group-class": "h_det_pam_input h_det_pam_1", + // "possible": [ + // { + // "name": lang.No, + // "value": "0" + // }, + // { + // "name": lang.Yes, + // "value": "1" + // } + // ] + // }, + { + "name": "detail=detector_sensitivity", + "field": lang['Minimum Change'], + "description": "The motion confidence rating must exceed this value to be seen as a trigger. This number correlates directly to the confidence rating returned by the motion detector. This option was previously named \"Indifference\".", + "default": "10", + "example": "10", + "possible": "" + }, + { + "name": "detail=detector_max_sensitivity", + "field": lang["Maximum Change"], + "description": "The motion confidence rating must be lower than this value to be seen as a trigger. Leave blank for no maximum. This option was previously named \"Max Indifference\".", + "default": "", + "example": "75", + "possible": "" + }, + { + isAdvanced: true, + "name": "detail=detector_threshold", + "field": lang["Trigger Threshold"], + "description": lang["fieldTextDetectorThreshold"], + "default": "1", + "example": "3", + "possible": "Any non-negative integer." + }, + { + isAdvanced: true, + "name": "detail=detector_color_threshold", + "field": lang["Color Threshold"], + "description": lang["fieldTextDetectorColorThreshold"], + "default": "9", + "example": "9", + "possible": "Any non-negative integer." + }, + { + isAdvanced: true, + "name": "detail=inverse_trigger", + "field": lang["Inverse Trigger"], + "description": lang["fieldTextInverseTrigger"], + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "name": "detail=detector_frame", + "field": lang["Full Frame Detection"], + "description": lang["fieldTextDetectorFrame"], + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "name": "detail=detector_motion_tile_mode", + "field": lang['Accuracy Mode'], + "selector": "h_det_tile_mode", + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "form-group-class": "h_det_tile_mode_input h_det_tile_mode_1", + "name": "detail=detector_tile_size", + "field": lang["Tile Size"], + "description": lang.fieldTextTileSize, + "default": "20", + }, + { + isAdvanced: true, + "name": "detail=detector_noise_filter", + "field": lang['Noise Filter'], + "description": lang["fieldTextDetectorNoiseFilter"], + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + isAdvanced: true, + "name": "detail=detector_noise_filter_range", + "field": lang["Noise Filter Range"], + "description": lang["fieldTextDetectorNoiseFilterRange"], + "default": "6", + "example": "9", + "possible": "Any non-negative integer." + }, + ] + }, + { + "name": lang['Object Detection'], + "color": "orange", + id: "monSectionDetectorObject", + headerTitle: `${lang['Object Detection']} ${lang['Not Connected']}`, + isFormGroupGroup: true, + isSection: true, + "input-mapping": "detector_object", + "section-class": "h_det_input h_det_1", + "info": [ + { + "name": "detail=detector_use_detect_object", + "field": lang.Enabled, + "description": lang["fieldTextDetectorUseDetectObject"], + "default": "0", + "example": "", + "selector": "h_casc", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + isAdvanced: true, + "name": "detail=detector_send_frames_object", + "field": lang["Send Frames"], + "description": lang["fieldTextDetectorSendFramesObject"], + "default": "1", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + isAdvanced: true, + hidden: true, + "name": "detail=detector_obj_count_in_region", + "field": lang["Count Objects only inside Regions"], + "description": lang["fieldTextDetectorObjCountInRegion"], + "default": "0", + "example": "", + "form-group-class": "h_det_count_input h_det_count_1", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "name": "detail=detector_obj_region", + "field": lang['Require Object to be in Region'], + "description": "", + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + isAdvanced: true, + "name": "detail=detector_use_motion", + "field": lang['Check for Motion First'], + "description": "", + "default": "1", + "example": "", + "selector": "h_det_mot_fir", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + hidden: true, + "name": "detail=detector_fps_object", + "field": lang['Frame Rate'], + "description": "", + "default": "2", + "example": "", + "form-group-class": "h_casc_input h_casc_1", + "possible": "" + }, + { + isAdvanced: true, + hidden: true, + "name": "detail=detector_scale_x_object", + "field": lang['Image Width'], + "description": "", + "default": "1280", + "form-group-class": "h_casc_input h_casc_1", + "fieldType": "number", + "numberMin": "1", + "possible": "" + }, + { + isAdvanced: true, + hidden: true, + "name": "detail=detector_scale_y_object", + "field": lang['Image Height'], + "description": "", + "default": "720", + "form-group-class": "h_casc_input h_casc_1", + "fieldType": "number", + "numberMin": "1", + "possible": "" + }, + ] + }, + { + isAdvanced: true, + hidden: true, + "name": lang['Event-Based Recording'], + "input-mapping": "detector_sip_buffer", + "color": "orange", + id: "monSectionDetectorTraditionalRecording", + isSection: true, + isFormGroupGroup: true, + "section-class": "h_det_input h_det_1", + "info": [ + { + "name": "detail=detector_buffer_vcodec", + "field": lang['HLS Video Encoder'], + "description": "", + "default": "0", + "example": "", + "selector": "h_buff", + "fieldType": "select", + "possible": [ + { + "name": "Auto", + "value": "auto" + }, + { + "name": "libx264", + "value": "libx264" + }, + { + "name": "H.264 VA-API (Intel HW Accel)", + "value": "h264_vaapi" + }, + { + "name": "H.265 VA-API (Intel HW Accel)", + "value": "hevc_vaapi" + }, + { + "name": lang.copy, + "value": "copy" + } + ] + }, + { + "name": "detail=detector_buffer_acodec", + "field": lang['HLS Audio Encoder'], + "description": "", + "default": "no", + "fieldType": "select", + "possible": [ + { + "name": lang['No Audio'], + "value": "no" + }, + { + "name": "Auto", + "value": "auto" + }, + { + "name": "aac", + "value": "aac" + }, + { + "name": "ac3", + "value": "ac3" + }, + { + "name": "libmp3lame", + "value": "libmp3lame" + }, + { + "name": lang.copy, + "value": "copy" + } + ] + }, + { + "name": "detail=detector_buffer_fps", + "field": lang['Frame Rate'], + "description": "", + "default": "30", + "example": "", + "form-group-class": "h_buff_input h_buff_libx264 h_buff_h264_vaapi h_buff_hevc_vaapi", + "possible": "" + }, + { + "name": "detail=event_record_scale_x", + "field": lang.Width, + "description": lang["fieldTextEventRecordScaleX"], + "default": "", + "fieldType": "number", + "numberMin": "1", + "example": "640", + "form-group-class": "h_buff_input h_buff_libx264 h_buff_h264_vaapi h_buff_hevc_vaapi", + "possible": "" + }, + { + "name": "detail=event_record_scale_y", + "field": lang.Height, + "description": lang["fieldTextEventRecordScaleY"], + "default": "", + "fieldType": "number", + "numberMin": "1", + "example": "480", + "form-group-class": "h_buff_input h_buff_libx264 h_buff_h264_vaapi h_buff_hevc_vaapi", + "possible": "" + }, + { + name: 'detail=event_record_aduration', + field: lang['Analyzation Duration'], + default: '1000', + }, + { + name: 'detail=event_record_probesize', + field: lang['Probe Size'], + default: '32', + }, + { + "fieldType": "div", + // style: `width:100%;background:#eceaea;border-radius:5px;color:#333;font-family:monospace`, + divContent: `
` + }, + ] + }, + { + hidden: true, + "name": lang['Audio Detector'], + "color": "orange", + id: "monSectionAudioDetector", + isSection: true, + isFormGroupGroup: true, + "section-class": "h_det_input h_det_1", + "info": [ + { + "name": "detail=detector_audio", + "field": lang.Enabled, + "description": lang["fieldTextDetectorAudio"], + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "name": "detail=detector_audio_min_db", + "field": lang['Minimum dB'], + "description": "", + "default": "5", + "example": "", + "possible": "" + }, + { + "name": "detail=detector_audio_max_db", + "field": lang['Maximum dB'], + "description": "", + "default": "", + "example": "", + "possible": "" + } + ] + }, { hidden: true, "name": lang['Webhook'], @@ -2411,7 +3478,7 @@ module.exports = function(s,config,lang){ { "name": "detail=detector_webhook", "field": "Webhook", - "description": "Send a GET request to a URL with some values from the event.", + "description": lang["fieldTextDetectorWebhook"], "default": "0", "example": "", "selector": "h_det_web", @@ -2431,7 +3498,7 @@ module.exports = function(s,config,lang){ { "name": "detail=detector_webhook_timeout", "field": lang['Allow Next Webhook'], - "description": "This value is a timer to allow the next running of your Webhook. This value is in minutes.", + "description": lang["fieldTextDetectorWebhookTimeout"], "default": "10", "example": "", "form-group-class": "h_det_web_input h_det_web_1", @@ -2508,7 +3575,7 @@ module.exports = function(s,config,lang){ { "name": "detail=detector_command", "field": lang['Command'], - "description": "The command that will run. This is the equivalent of running a shell command from terminal.", + "description": lang["fieldTextDetectorCommand"], "default": "", "form-group-class": "h_det_com_input h_det_com_1", "example": "/home/script.sh {{MONITOR_ID}} {{GROUP_KEY}} {{CONFIDENCE}}", @@ -2518,7 +3585,7 @@ module.exports = function(s,config,lang){ { "name": "detail=detector_command_timeout", "field": lang['Allow Next Command'], - "description": "This value is a timer to allow the next running of your script. This value is in minutes.", + "description": lang["fieldTextDetectorCommandTimeout"], "default": "10", "example": "", "form-group-class": "h_det_com_input h_det_com_1", @@ -2527,246 +3594,6 @@ module.exports = function(s,config,lang){ }, ] }, - { - "name": "detail=snap_seconds_inward", - "field": lang['Delay for Snapshot'], - "description": lang['in seconds'], - "form-group-class": "h_det_input h_det_1", - "default": "0", - }, - { - "name": "detail=detector_mail", - "field": lang['Email on Trigger'], - "description": "Recieve an email of an image during a motion event to the master account for the camera group. You must setup SMTP details in conf.json.", - "default": "0", - "example": "", - "selector": "h_det_email", - "fieldType": "select", - "form-group-class-pre-layer": "h_det_input h_det_1", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_mail_timeout", - "field": lang['Allow Next Email'], - "description": "The amount of time until a trigger is allowed to send another email with motion details and another image.", - "default": "10", - "example": "", - "form-group-class-pre-layer": "h_det_input h_det_1", - "possible": "" - }, - { - "name": "detail=use_detector_filters", - "field": lang['Event Filters'], - "description": "", - "default": "0", - "example": "", - "selector": "h_det_fil", - "fieldType": "select", - "form-group-class-pre-layer": "h_det_input h_det_1", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=use_detector_filters_object", - "field": lang['Filter for Objects only'], - "description": "", - "default": "0", - "example": "", - "fieldType": "select", - "form-group-class": "h_det_fil_input h_det_fil_1", - "form-group-class-pre-layer": "h_det_input h_det_1", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - hidden: true, - "name": "detail=cords", - }, - { - hidden: true, - "name": "detail=detector_filters", - }, - { - "fieldType": "btn", - "class": `btn-danger open-region-editor`, - "btnContent": `   ${lang['Region Editor']}`, - "description": "", - "default": "", - "example": "", - "form-group-class-pre-pre-layer": "h_det_input h_det_1", - "form-group-class-pre-layer": "form-group", - "possible": "" - }, - { - hidden: true, - "name": lang['Motion Detection'], - "headerTitle": `${lang['Motion Detection']} ${lang['Primary Engine']} : Pixel Array ${lang['Not Connected']}`, - "color": "orange", - id: "monSectionDetectorMotion", - isSection: true, - isAdvanced: true, - isFormGroupGroup: true, - "section-class": "h_det_input h_det_1", - "info": [ - { - "name": "detail=detector_pam", - "field": lang["Use Built-In"], - "description": "Use Kevin Godell's Motion Detector. This is built into Shinobi and requires no other configuration to activate.", - "selector": "h_det_pam", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - // { - // "name": "detail=detector_show_matrix", - // "field": lang["Show Matrices"], - // "description": "Outline which pixels are detected as changed in one matrix.", - // "default": "0", - // "example": "", - // "fieldType": "select", - // "form-group-class": "h_det_pam_input h_det_pam_1", - // "possible": [ - // { - // "name": lang.No, - // "value": "0" - // }, - // { - // "name": lang.Yes, - // "value": "1" - // } - // ] - // }, - { - "name": "detail=detector_sensitivity", - "field": lang['Minimum Change'], - "description": "The motion confidence rating must exceed this value to be seen as a trigger. This number correlates directly to the confidence rating returned by the motion detector. This option was previously named \"Indifference\".", - "default": "10", - "example": "10", - "possible": "" - }, - { - "name": "detail=detector_max_sensitivity", - "field": lang["Maximum Change"], - "description": "The motion confidence rating must be lower than this value to be seen as a trigger. Leave blank for no maximum. This option was previously named \"Max Indifference\".", - "default": "", - "example": "75", - "possible": "" - }, - { - "name": "detail=detector_threshold", - "field": lang["Trigger Threshold"], - "description": "Minimum number of detections to fire a motion event. Detections must be within the detector the threshold divided by detector fps seconds. For example, if detector fps is 2 and trigger threshold is 3, then three detections must occur within 1.5 seconds to trigger a motion event. This threshold is per detection region.", - "default": "1", - "example": "3", - "possible": "Any non-negative integer." - }, - { - "name": "detail=detector_color_threshold", - "field": lang["Color Threshold"], - "description": "The amount of difference allowed in a pixel before it is considered motion.", - "default": "9", - "example": "9", - "possible": "Any non-negative integer." - }, - { - "name": "detail=inverse_trigger", - "field": lang["Inverse Trigger"], - "description": "To trigger outside specified regions. Will not trigger with Full Frame Detection enabled.", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_frame", - "field": lang["Full Frame Detection"], - "description": "This will read the entire frame for pixel differences. This is the same as creating a region that covers the entire screen.", - "default": "1", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_noise_filter", - "field": lang['Noise Filter'], - "description": "Attempt to filter grain or repeated motion at a particular indifference.", - "default": "1", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_noise_filter_range", - "field": lang["Noise Filter Range"], - "description": "The amount of difference allowed in a pixel before it is considered motion.", - "default": "6", - "example": "9", - "possible": "Any non-negative integer." - }, - ] - }, { hidden: true, "name": lang['\"No Motion"\ Detector'], @@ -2780,7 +3607,7 @@ module.exports = function(s,config,lang){ { "name": "detail=detector_notrigger", "field": lang.Enabled, - "description": "Check if motion has occured on an interval. If motion has occurred the check will be reset.", + "description": lang["fieldTextDetectorNotrigger"], "default": "0", "example": "", "fieldType": "select", @@ -2796,27 +3623,17 @@ module.exports = function(s,config,lang){ ] }, { - "name": "detail=detector_notrigger_mail", - "field": lang['Email'], - "description": "If motion has not been detected after the timeout period you will recieve an email.", - "default": "0", + "name": "detail=detector_notrigger_timeout", + "field": lang.Timeout, + "description": lang["fieldTextDetectorNotriggerTimeout"], + "default": "10", "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] + "possible": "" }, { "name": "detail=detector_notrigger_discord", "field": lang['No Trigger'], - "description": "If motion has not been detected after the timeout period you will recieve an Discord notification.", + "description": lang["fieldTextDetectorNotriggerDiscord"], "default": "0", "example": "", "fieldType": "select", @@ -2831,18 +3648,10 @@ module.exports = function(s,config,lang){ } ] }, - { - "name": "detail=detector_notrigger_timeout", - "field": lang.Timeout, - "description": "Timeout is calculated in minutes.", - "default": "10", - "example": "", - "possible": "" - }, { "name": "detail=detector_notrigger_webhook", "field": "Webhook", - "description": "Send a GET request to a URL with some values from the event.", + "description": lang["fieldTextDetectorNotriggerWebhook"], "default": "0", "example": "", "selector": "h_det_web_notrig", @@ -2913,7 +3722,7 @@ module.exports = function(s,config,lang){ { "name": "detail=detector_notrigger_command", "field": lang['Command'], - "description": "The command that will run. This is the equivalent of running a shell command from terminal.", + "description": lang["fieldTextDetectorNotriggerCommand"], "default": "", "form-group-class": "h_det_com_notrig_input h_det_com_notrig_1", "example": "/home/script.sh {{MONITOR_ID}} {{GROUP_KEY}} {{CONFIDENCE}}", @@ -2922,7 +3731,7 @@ module.exports = function(s,config,lang){ { "name": "detail=detector_notrigger_command_timeout", "field": lang['Allow Next Command'], - "description": "This value is a timer to allow the next running of your script. This value is in minutes.", + "description": lang["fieldTextDetectorNotriggerCommandTimeout"], "default": "10", "example": "", "form-group-class": "h_det_com_notrig_input h_det_com_notrig_1", @@ -2930,413 +3739,6 @@ module.exports = function(s,config,lang){ }, ] }, - { - hidden: true, - "name": lang['Audio Detector'], - "color": "orange", - id: "monSectionAudioDetector", - isSection: true, - isAdvanced: true, - isFormGroupGroup: true, - "section-class": "h_det_input h_det_1", - "info": [ - { - "name": "detail=detector_audio", - "field": lang.Enabled, - "description": "Check if Audio has occured at a certiain decible. Decible reading may not be accurate to real-world measurement.", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_audio_min_db", - "field": lang['Minimum dB'], - "description": "", - "default": "5", - "example": "", - "possible": "" - }, - { - "name": "detail=detector_audio_max_db", - "field": lang['Maximum dB'], - "description": "", - "default": "", - "example": "", - "possible": "" - } - ] - }, - { - "name": lang['Object Detection'], - "color": "orange", - id: "monSectionDetectorObject", - headerTitle: `${lang['Object Detection']} ${lang['Not Connected']}`, - isFormGroupGroup: true, - isSection: true, - "input-mapping": "detector_object", - "section-class": "h_det_input h_det_1", - "info": [ - { - "name": "detail=detector_use_detect_object", - "field": lang.Enabled, - "description": "Create frames for sending to any connected Plugin.", - "default": "0", - "example": "", - "selector": "h_casc", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_send_frames_object", - "field": lang["Send Frames"], - "description": "Push frames to the connected plugin to be analyzed.", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - hidden: true, - "name": "detail=detector_obj_count_in_region", - "field": lang["Count Objects only inside Regions"], - "description": "Count Objects only inside Regions.", - "default": "0", - "example": "", - "form-group-class": "h_det_count_input h_det_count_1", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_obj_region", - "field": lang['Require Object to be in Region'], - "description": "", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_use_motion", - "field": lang['Check for Motion First'], - "description": "", - "default": "0", - "example": "", - "selector": "h_det_mot_fir", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - hidden: true, - "name": "detail=detector_fps_object", - "field": lang['Frame Rate'], - "description": "", - "default": "2", - "example": "", - "form-group-class": "h_casc_input h_casc_1", - "possible": "" - }, - { - hidden: true, - "name": "detail=detector_scale_x_object", - "field": lang['Image Width'], - "description": "", - "default": "", - "example": "", - "form-group-class": "h_casc_input h_casc_1", - "fieldType": "number", - "numberMin": "1", - "possible": "" - }, - { - hidden: true, - "name": "detail=detector_scale_y_object", - "field": lang['Image Height'], - "description": "", - "default": "", - "example": "", - "form-group-class": "h_casc_input h_casc_1", - "fieldType": "number", - "numberMin": "1", - "possible": "" - }, - ] - }, - { - hidden: true, - "name": lang['License Plate Detector'], - "color": "orange", - id: "monSectionLisencePlateDetector", - headerTitle: `${lang['Object Detection']} ${lang['Plugin']} : ${lang['Not Connected']}`, - isSection: true, - isAdvanced: true, - isFormGroupGroup: true, - "section-pre-pre-class": "h_det_input h_det_1", - "section-pre-class": "h_casc_input h_casc_1", - "section-class": "shinobi-detector-opencv shinobi-detector-openalpr shinobi-detector_plug", - "info": [ - { - "name": "detail=detector_lisence_plate", - "field": lang.Enabled, - "description": "Enable License Plate Recognition. OpenALPR plugin has this always enabled.", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, - { - "name": "detail=detector_lisence_plate_country", - "field": lang['Country of Plates'], - "description": "Choose the type of plates to recognize. Only US and EU are supported at this time.", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": "US", - "value": "us" - }, - { - "name": "EU", - "value": "eu" - } - ] - }, - ] - }, - { - hidden: true, - "name": lang['OpenCV Cascades'], - "color": "orange", - id: "monSectionOpenCVCascades", - headerTitle: `${lang['OpenCV Cascades']}  `, - blockquote: lang.opencvCascadesText, - isSection: true, - isAdvanced: true, - isFormGroupGroup: true, - "section-pre-pre-class": "h_det_input h_det_1", - "section-pre-class": "h_casc_input h_casc_1", - "section-class": "shinobi-detector-opencv shinobi-detector-openalpr shinobi-detector_plug", - "info": [ - { - "fieldType": "div", - id: "detector_cascade_list", - style: "max-height: 300px;overflow: auto;" - } - ] - }, - { - hidden: true, - "name": lang['Traditional Recording'], - "input-mapping": "detector_sip_buffer", - "color": "orange", - id: "monSectionDetectorTraditionalRecording", - isSection: true, - isAdvanced: true, - isFormGroupGroup: true, - "section-class": "h_det_input h_det_1", - "info": [ - { - "name": "detail=detector_buffer_vcodec", - "field": lang['HLS Video Encoder'], - "description": "", - "default": "0", - "example": "", - "selector": "h_buff", - "fieldType": "select", - "possible": [ - { - "name": "Auto", - "value": "auto" - }, - { - "name": "libx264", - "value": "libx264" - }, - { - "name": "H.264 VA-API (Intel HW Accel)", - "value": "h264_vaapi" - }, - { - "name": "H.265 VA-API (Intel HW Accel)", - "value": "hevc_vaapi" - }, - { - "name": lang.copy, - "value": "copy" - } - ] - }, - { - "name": "detail=detector_buffer_acodec", - "field": lang['HLS Audio Encoder'], - "description": "", - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang['No Audio'], - "value": "no" - }, - { - "name": "Auto", - "value": "auto" - }, - { - "name": "aac", - "value": "aac" - }, - { - "name": "ac3", - "value": "ac3" - }, - { - "name": "libmp3lame", - "value": "libmp3lame" - }, - { - "name": lang.copy, - "value": "copy" - } - ] - }, - { - "name": "detail=detector_buffer_fps", - "field": lang['Frame Rate'], - "description": "", - "default": "30", - "example": "", - "form-group-class": "h_buff_input h_buff_libx264 h_buff_h264_vaapi h_buff_hevc_vaapi", - "possible": "" - }, - { - "name": "detail=event_record_scale_x", - "field": lang.Width, - "description": "Width of the Event-based Recording image that is output after processing.", - "default": "", - "fieldType": "number", - "numberMin": "1", - "example": "640", - "form-group-class": "h_buff_input h_buff_libx264 h_buff_h264_vaapi h_buff_hevc_vaapi", - "possible": "" - }, - { - "name": "detail=event_record_scale_y", - "field": lang.Height, - "description": "Height of the Event-based Recording image that is output after processing.", - "default": "", - "fieldType": "number", - "numberMin": "1", - "example": "480", - "form-group-class": "h_buff_input h_buff_libx264 h_buff_h264_vaapi h_buff_hevc_vaapi", - "possible": "" - }, - { - "name": "detail=detector_buffer_hls_time", - "field": lang['HLS Segment Length'], - "description": "How long each video segment should be, in seconds. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.", - "default": "2", - "example": "", - "possible": "" - }, - { - "name": "detail=detector_buffer_hls_list_size", - "field": lang['HLS List Size'], - "description": "The number of segments maximum before deleting old segments automatically.", - "default": "10", - "example": "", - "possible": "" - }, - { - "name": "detail=detector_buffer_start_number", - "field": lang['HLS Start Number'], - "description": "", - "default": "0", - "example": "", - "form-group-class": "h_buff_input h_buff_libx264 h_buff_h264_vaapi h_buff_hevc_vaapi", - "possible": "" - }, - { - "name": "detail=detector_buffer_live_start_index", - "field": lang['HLS Live Start Index'], - "description": "", - "default": "-3", - "example": "", - "form-group-class": "h_buff_input h_buff_libx264 h_buff_h264_vaapi h_buff_hevc_vaapi", - "possible": "" - }, - { - "field": lang['Buffer Preview'], - id: "monEditBufferPreview", - "fieldType": "div", - "style": "width:100%;height:300px;background:#eceaea;border-radius:5px;color:#333;font-family:monospace" - }, - ] - }, ] }, "Control": { @@ -3365,6 +3767,7 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, "name": "detail=control_base_url", "field": lang['Custom Base URL'], "description": "", @@ -3401,6 +3804,7 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, "name": "detail=control_digest_auth", "field": lang['Digest Authentication'], "description": "", @@ -3434,8 +3838,12 @@ module.exports = function(s,config,lang){ "value": "0" }, { - "name": lang.Yes, + "name": lang.Timed, "value": "1" + }, + { + "name": lang['On Release'], + "value": "2" } ] }, @@ -3454,13 +3862,13 @@ module.exports = function(s,config,lang){ "description": "", "default": "0.1", "example": "", - "form-group-class": "h_cs_input h_cs_1", + "form-group-class": "h_control_call_input h_control_call_ONVIF", "possible": "" }, { "name": "detail=detector_ptz_follow", "field": lang['PTZ Tracking'], - "description": "Follow the largest detected object with PTZ? Requires an Object Detector running or matrices provided with events.", + "description": lang["fieldTextDetectorPtzFollow"], "default": "0", "example": "", "selector": "h_det_tracking", @@ -3477,6 +3885,7 @@ module.exports = function(s,config,lang){ ] }, { + isAdvanced: true, "name": "detail=detector_ptz_follow_target", "field": lang['PTZ Tracking Target'], "description": "", @@ -3485,25 +3894,25 @@ module.exports = function(s,config,lang){ "form-group-class": "h_det_tracking_input h_det_tracking_1", "possible": "" }, - { - "name": "detail=detector_obj_count", - "field": lang["Count Objects"], - "description": "Count detected objects.", - "default": "0", - "example": "", - "selector": "h_det_count", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, + // { + // "name": "detail=detector_obj_count", + // "field": lang["Count Objects"], + // "description": lang["fieldTextDetectorObjCount"], + // "default": "0", + // "example": "", + // "selector": "h_det_count", + // "fieldType": "select", + // "possible": [ + // { + // "name": lang.No, + // "value": "0" + // }, + // { + // "name": lang.Yes, + // "value": "1" + // } + // ] + // }, { "name": "detail=control_url_center", "field": lang['Center'], @@ -3528,7 +3937,7 @@ module.exports = function(s,config,lang){ "description": "", "default": "/", "example": "", - "form-group-class-pre-layer": "h_cs_input h_cs_1", + "form-group-class-pre-layer": "h_cs_input h_cs_1 h_cs_2", "form-group-class": "h_control_call_input h_control_call_GET h_control_call_PUT h_control_call_POST", "possible": "" }, @@ -3547,7 +3956,7 @@ module.exports = function(s,config,lang){ "description": "", "default": "/", "example": "", - "form-group-class-pre-layer": "h_cs_input h_cs_1", + "form-group-class-pre-layer": "h_cs_input h_cs_1 h_cs_2", "form-group-class": "h_control_call_input h_control_call_GET h_control_call_PUT h_control_call_POST", "possible": "" }, @@ -3566,7 +3975,7 @@ module.exports = function(s,config,lang){ "description": "", "default": "/", "example": "", - "form-group-class-pre-layer": "h_cs_input h_cs_1", + "form-group-class-pre-layer": "h_cs_input h_cs_1 h_cs_2", "form-group-class": "h_control_call_input h_control_call_GET h_control_call_PUT h_control_call_POST", "possible": "" }, @@ -3585,7 +3994,7 @@ module.exports = function(s,config,lang){ "description": "", "default": "/", "example": "", - "form-group-class-pre-layer": "h_cs_input h_cs_1", + "form-group-class-pre-layer": "h_cs_input h_cs_1 h_cs_2", "form-group-class": "h_control_call_input h_control_call_GET h_control_call_PUT h_control_call_POST", "possible": "" }, @@ -3622,7 +4031,7 @@ module.exports = function(s,config,lang){ "description": "", "default": "/", "example": "", - "form-group-class-pre-layer": "h_cs_input h_cs_1", + "form-group-class-pre-layer": "h_cs_input h_cs_1 h_cs_2", "form-group-class": "h_control_call_input h_control_call_GET h_control_call_PUT h_control_call_POST", "possible": "" }, @@ -3641,14 +4050,15 @@ module.exports = function(s,config,lang){ "description": "", "default": "/", "example": "", - "form-group-class-pre-layer": "h_cs_input h_cs_1", + "form-group-class-pre-layer": "h_cs_input h_cs_1 h_cs_2", "form-group-class": "h_control_call_input h_control_call_GET h_control_call_PUT h_control_call_POST", "possible": "" }, { + isAdvanced: true, "name": "detail=control_invert_y", "field": lang["Invert Y-Axis"], - "description": "For When your camera is mounted upside down or uses inverted vertical controls.", + "description": lang["fieldTextControlInvertY"], "default": "0", "example": "", "fieldType": "select", @@ -3674,7 +4084,7 @@ module.exports = function(s,config,lang){ isAdvanced: true, "info": [ { - "fieldType": 'ul', + "fieldType": 'div', "id": "monitor_groups", "class": "mdl-list" }, @@ -3689,10 +4099,11 @@ module.exports = function(s,config,lang){ "name": lang['Copy Settings'], "color": "orange", isSection: true, + "box-wrapper-class": "row", "info": [ { "id": "copy_settings", - "field": lang['Copy to Settings'], + "field": lang['Copy to Selected Monitor(s)'], "description": "", "default": "0", "example": "", @@ -3717,6 +4128,7 @@ module.exports = function(s,config,lang){ "fieldType": "select", "attribute": `copy="field=mode"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3729,13 +4141,32 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Stream Channels'], + "field": lang['Compress Completed Videos'], + "default": "0", + "fieldType": "select", + "attribute": `copy="field=detail=auto_compress_videos"`, + "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "field": lang['Stream Channels'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="field=detail=stream_channels"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3748,13 +4179,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Connection Settings'], + "field": lang['Connection'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionConnection"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3767,13 +4199,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Input Settings'], + "field": lang['Input'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionInput"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3786,13 +4219,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Timelapse Settings'], + "field": lang['Timelapse'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionTimelapse"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3805,13 +4239,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Stream Settings'], + "field": lang['Stream'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionStream,#monSectionStreamTimestamp,#monSectionStreamWatermark"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3824,13 +4259,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy JPEG API Settings'], + "field": lang['JPEG API'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionJPEGAPI"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3843,13 +4279,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Recording Settings'], + "field": lang['Recording'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionRecording,#monSectionRecordingTimestamp,#monSectionRecordingWatermark"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3862,13 +4299,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Detector Settings'], + "field": lang['Detector Settings'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionDetector,#monSectionDetectorBuffer,#monSectionLisencePlateDetector,#monSectionNoMotionDetector"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3881,13 +4319,34 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Custom Settings'], + "field": lang['Control'], + "description": "", + "default": "0", + "example": "", + "fieldType": "select", + "attribute": `copy="#monSectionControl"`, + "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "field": lang['Custom'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionCustom"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3900,13 +4359,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Group Settings'], + "field": lang['Grouping'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionGrouping"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3919,13 +4379,14 @@ module.exports = function(s,config,lang){ ] }, { - "field": lang['Copy Logging Settings'], + "field": lang['Logging'], "description": "", "default": "0", "example": "", "fieldType": "select", "attribute": `copy="#monSectionLogging"`, "form-group-class": "h_copy_settings_input h_copy_settings_1", + "form-group-class-pre-layer": "col-md-6", "possible": [ { "name": lang.No, @@ -3965,32 +4426,13 @@ module.exports = function(s,config,lang){ isAdvanced: true, "isSection": true, "id": "monSectionNotifications", - "selector": "h_det", - "attribute": `triggerChange="#add_monitor [detail=detector_record_method]"`, - "blockquote": `${lang.DetectorText}\n

`, "info": [ { "name": lang.Methods, "color": "blue", isFormGroupGroup: true, "info": [ - { - "name": "detail=notify_email", - "field": lang.Email, - "default": "0", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, + ], }, { @@ -4026,20 +4468,114 @@ module.exports = function(s,config,lang){ "value": "1" } ] - }, + } ] }, + "Custom": { + "name": lang.Custom, + "color": "navy", + "isSection": true, + isAdvanced: true, + "id": "monSectionCustom", + "info": [ + { + "name": "detail=cust_input", + "field": lang['Input Flags'], + "description": lang["fieldTextCustInput"], + "default": "", + "example": "", + "possible": "" + }, + // { + // hidden: true, + // "name": "detail=cust_rtmp", + // "field": lang['RTMP Stream Flags'], + // "description": "Custom Flags that bind to the RTMP stream.", + // "default": "", + // "example": "", + // "form-group-class": "h_rtmp_input h_rtmp_1", + // "possible": "" + // }, + { + "name": "detail=cust_stream", + "field": lang["Stream Flags"], + "description": lang["fieldTextCustStream"], + "default": "", + "example": "", + "form-group-class": "", + "possible": "" + }, + { + hidden: true, + "name": "detail=cust_snap", + "field": "Snapshot Flags", + "description": lang["fieldTextCustSnap"], + "default": "", + "example": "", + "form-group-class": "h_sn_input h_sn_1", + "possible": "" + }, + { + hidden: true, + "name": "detail=cust_record", + "field": lang["Recording Flags"], + "description": lang["fieldTextCustRecord"], + "default": "", + "example": "", + "form-group-class": "h_m_input h_m_record", + "possible": "" + }, + { + hidden: true, + "name": "detail=cust_detect", + "field": lang["Detector Flags"], + "description": lang["fieldTextCustDetect"], + "default": "", + "example": "", + "form-group-class": "shinobi-detector", + "possible": "" + }, + { + hidden: true, + "name": "detail=cust_detect_object", + "field": lang["Object Detector Flags"], + "description": lang["fieldTextCustDetectObject"], + "default": "", + "example": "", + "form-group-class": "shinobi-detector", + "possible": "" + }, + { + hidden: true, + "name": "detail=cust_sip_record", + "field": lang['Event-Based Recording Flags'], + "description": lang["fieldTextCustSipRecord"], + "default": "", + "example": "", + "form-group-class": "h_rec_mtd_input h_rec_mtd_sip", + "possible": "" + }, + { + "name": "detail=custom_output", + "field": "Output Method", + "description": lang["fieldTextCustomOutput"], + "default": "", + "example": "", + "form-group-class": "", + "possible": "" + } + ] + }, "Logging": { "name": lang.Logging, "color": "green", id: "monSectionLogging", - isAdvanced: true, isSection: true, "info": [ { "name": "detail=loglevel", "field": lang['Log Level'], - "description": "The amount of data to provide while doing the job.", + "description": lang["fieldTextLoglevel"], "default": "0", "example": "", "fieldType": "select", @@ -4047,29 +4583,29 @@ module.exports = function(s,config,lang){ { "name": lang.Silent, "value": "quiet", - "info": "None. This will silence all logging." + "info": lang["fieldTextLoglevelSilent"] }, { "name": lang.Fatal, "value": "fatal", - "info": "Display only fatal errors." + "info": lang["fieldTextLoglevelFatal"] }, { "name": lang['on Error'], "value": "error", - "info": "Display all important errors. Note : this doesn't always show important information." + "info": lang["fieldTextLoglevelOnError"] }, { "name": lang['All Warnings'], "value": "warning", - "info": "Display all warnings. Use this if you can't find out what's wrong with your camera." + "info": lang["fieldTextLoglevelAllWarnings"] } ] }, { "name": "detail=sqllog", "field": lang["Save Log in SQL"], - "description": "Use this with caution as FFMPEG likes to throw up superfluous data at times which can lead to a lot of database rows.", + "description": lang["fieldTextSqllog"], "default": "0", "example": "", "fieldType": "select", @@ -4077,12 +4613,12 @@ module.exports = function(s,config,lang){ { "name": lang.No, "value": "0", - "info": "No is the default." + "info": lang["fieldTextSqllogNo"] }, { "name": lang.Yes, "value": "1", - "info": "Do this if you are having recurring issues only." + "info": lang["fieldTextSqllogYes"] } ] }, @@ -4197,7 +4733,7 @@ module.exports = function(s,config,lang){ { "name": "detail=factorAuth", "field": lang.Enabled, - "description": "Enable a secondary requirement for login through one of the enabled methods.", + "description": lang["fieldTextFactorAuth"], "default": "0", "example": "", "fieldType": "select", @@ -4212,24 +4748,6 @@ module.exports = function(s,config,lang){ } ] }, - { - "name": "detail=factor_mail", - "field": lang.Email, - "description": "Send 2-Factor Authentication codes to the email address of the account.", - "default": "1", - "example": "", - "fieldType": "select", - "possible": [ - { - "name": lang.No, - "value": "0" - }, - { - "name": lang.Yes, - "value": "1" - } - ] - }, ] }, "Profile": { @@ -4239,7 +4757,7 @@ module.exports = function(s,config,lang){ { "name": "mail", "field": lang.Email, - "description": "The login for accounts. The main account holder's email address will get notifications.", + "description": lang["fieldTextMail"], "default": "", "example": "ccio@m03.ca", "possible": "" @@ -4248,7 +4766,7 @@ module.exports = function(s,config,lang){ "name": "pass", "field": lang.Password, "fieldType": "password", - "description": "Leave blank to keep the same password during settings modification.", + "description": lang["fieldTextPass"], "fieldType": "password", "default": "", "example": "", @@ -4258,7 +4776,7 @@ module.exports = function(s,config,lang){ "name": "password_again", "field": lang["Password Again"], "fieldType": "password", - "description": "Must match Password field if you desire to change it.", + "description": lang["fieldTextPasswordAgain"], "default": "", "example": "", "possible": "" @@ -4266,7 +4784,7 @@ module.exports = function(s,config,lang){ { "name": "detail=size", "field": lang["Max Storage Amount"], - "description": "The amount of disk space Shinobi will allow to be consumed before purging. This value is read in megabytes.", + "description": lang["fieldTextSize"], "default": "10000", "example": "600000", "possible": "Up to 95% of your maximum storage space if only one master account exists.", @@ -4276,21 +4794,21 @@ module.exports = function(s,config,lang){ { "name": "detail=size_video_percent", "field": lang["Video Share"], - "description": "Percent of Max Storage Amount the videos can record to.", + "description": lang["fieldTextSizeVideoPercent"], "default": "90", "notForSubAccount": true, }, { "name": "detail=size_timelapse_percent", "field": lang["Timelapse Frames Share"], - "description": "Percent of Max Storage Amount the timelapse frames can record to.", + "description": lang["fieldTextSizeTimelapsePercent"], "default": "5", "notForSubAccount": true, }, { "name": "detail=size_filebin_percent", "field": lang["FileBin Share"], - "description": "Percent of Max Storage Amount the FileBin archive can use.", + "description": lang["fieldTextSizeFilebinPercent"], "default": "5", "notForSubAccount": true, }, @@ -4307,7 +4825,7 @@ module.exports = function(s,config,lang){ { "name": "detail=days", "field": lang["Number of Days to keep"] + ' ' + lang['Videos'], - "description": "The number of days to keep videos before purging.", + "description": lang["fieldTextDays"], "default": "5", "example": "30", "possible": "", @@ -4317,17 +4835,25 @@ module.exports = function(s,config,lang){ { "name": "detail=event_days", "field": lang["Number of Days to keep"] + ' ' + lang['Events'], - "description": "The number of days to keep events before purging.", + "description": lang["fieldTextEventDays"], "default": "10", "example": "30", "possible": "", "notForSubAccount": true, "evaluation": "details.edit_event_days !== '0'" }, + { + "name": "detail=timelapseFrames_days", + "field": lang["Number of Days to keep"] + ' ' + lang['Timelapse'], + "description": lang["fieldTextEventDays"], + "default": "60", + "notForSubAccount": true, + "evaluation": "details.edit_timelapseFrames_days !== '0'" + }, { "name": "detail=log_days", "field": lang["Number of Days to keep"] + ' ' + lang['Logs'], - "description": "The number of days to keep logs before purging.", + "description": lang["fieldTextLogDays"], "default": "10", "example": "30", "possible": "", @@ -4337,7 +4863,7 @@ module.exports = function(s,config,lang){ { "name": "detail=lang", "field": lang["Dashboard Language"], - "description": 'The primary language of text elements. For complete translation add your language in conf.json e.g:"language": "en_CA",', + "description": lang["fieldTextLang"], "default": "en_CA", "example": "", "fieldType": "select", @@ -4346,7 +4872,7 @@ module.exports = function(s,config,lang){ { "name": "detail=audio_note", "field": lang["Notification Sound"], - "description": "Sound when information bubble appears.", + "description": lang["fieldTextAudioNote"], "default": "", "example": "", "fieldType": "select", @@ -4355,7 +4881,7 @@ module.exports = function(s,config,lang){ { "name": "detail=audio_alert", "field": lang["Alert Sound"], - "description": "Sound when Event occurs.", + "description": lang["fieldTextAudioAlert"], "default": "", "example": "", "fieldType": "select", @@ -4364,7 +4890,7 @@ module.exports = function(s,config,lang){ { "name": "detail=audio_delay", "field": lang["Alert Sound Delay"], - "description": "Delay until next time an Event can start an Alert. Measured in seconds.", + "description": lang["fieldTextAudioDelay"], "default": "1", "example": "", "possible": "" @@ -4372,7 +4898,7 @@ module.exports = function(s,config,lang){ { "name": "detail=event_mon_pop", "field": lang["Popout Monitor on Event"], - "description": "When an Event occurs popout the monitor stream.", + "description": lang["fieldTextEventMonPop"], "default": "en_CA", "example": "", "fieldType": "select", @@ -4445,12 +4971,17 @@ module.exports = function(s,config,lang){ "Uploaders": { "name": lang["Uploaders"], "color": "forestgreen", - "blocks": s.uploaderFields + "info": [] }, "Preferences": { "name": lang.Preferences, "color": "navy", "info": [ + { + "field": lang['Clock Format'], + "name": "detail=clock_date_format", + "placeholder": "$DAYNAME $DAY $MONTHNAME $YEAR", + }, { "field": lang.CSS, "name": "detail=css", @@ -4461,6 +4992,12 @@ module.exports = function(s,config,lang){ "example": "", "possible": "" }, + { + "field": lang.hlsOptions, + "name": "localStorage=hlsOptions", + fieldType:"textarea", + "placeholder": "{}", + }, { "field": lang['Force Monitors Per Row'], "form-group-class":"st_force_mon_rows_input st_force_mon_rows_1", @@ -4568,11 +5105,22 @@ module.exports = function(s,config,lang){ "blockquoteClass": "global_tip", "blockquote": lang.onvifdeviceManagerGlobalTip, "info": [ + { + "field": lang["Monitor"], + "fieldType": "select", + "class": "monitors_list", + "possible": [] + }, { "fieldType": "btn", "class": `btn-warning onvif-device-reboot`, "btnContent": `   ${lang['Reboot Camera']}`, }, + { + "fieldType": "div", + "class": "p-2", + "divContent": `
`,
+                     }
                  ]
              },
              "Network": {
@@ -4746,17 +5294,17 @@ module.exports = function(s,config,lang){
                            {
                               "name": lang.On,
                               "value": "ON",
-                              "info": "Enable Ir cut fiter. Typically Day mode."
+                              "info": lang["fieldTextIrCutFilterOn"]
                            },
                            {
                               "name": lang.Off,
                               "value": "OFF",
-                              "info": "Disable Ir cut fiter. Typically Night mode."
+                              "info": lang["fieldTextIrCutFilterOff"]
                            },
                            {
                               "name": lang.Auto,
                               "value": "AUTO",
-                              "info": "Ir cut filter is automatically activated by the device."
+                              "info": lang["fieldTextIrCutFilterAuto"]
                            },
                        ]
                      },
@@ -5232,7 +5780,7 @@ module.exports = function(s,config,lang){
                         "field": lang['Allowed IPs'],
                         "default": `0.0.0.0`,
                         "placeholder": `0.0.0.0 ${lang['for Global Access']}`,
-                        "description": lang['Separate with commas, no spaces'],
+                        "description": lang[lang["fieldTextIp"]],
                         "fieldType": "text"
                      },
                      {
@@ -5373,23 +5921,26 @@ module.exports = function(s,config,lang){
                 "name": lang["Regions"],
                 "headerTitle": ` 
                   
-    -    + +
`, "color": "orange", "section-pre-class": "col-md-6", "section-class": "where", + "box-wrapper-class": "row", "info": [ { "field": lang["Monitor"], "id": "region_editor_monitors", "fieldType": "select", + "form-group-class": "col-md-6", }, { "id": "regions_list", "field": lang["Regions"], "fieldType": "select", - "possible": [] + "possible": [], + "form-group-class": "col-md-6", }, { "name": "name", @@ -5398,28 +5949,47 @@ module.exports = function(s,config,lang){ { "name": "sensitivity", "field": lang['Minimum Change'], + "form-group-class": "col-md-6", }, { "name": "max_sensitivity", "field": lang['Maximum Change'], + "form-group-class": "col-md-6", }, { "name": "threshold", "field": lang['Trigger Threshold'], + "form-group-class": "col-md-6", }, { "name": "color_threshold", "field": lang['Color Threshold'], + "form-group-class": "col-md-6", }, { + hidden: true, id: "regions_points", "fieldType": "table", "class": 'table table-striped', }, { - "fieldType": "btn", - "class": `btn-info toggle-region-still-image`, - "btnContent": `   ${lang['Live Stream Toggle']}`, + "class": 'col-md-12', + "fieldType": 'div', + info: [ + { + "fieldType": "btn", + attribute: "href=#", + "class": `btn-info toggle-region-still-image`, + "btnContent": `   ${lang['Live Stream Toggle']}`, + }, + { + "fieldType": "btn", + forForm: true, + attribute: "href=#", + "class": `btn-success`, + "btnContent": `   ${lang['Save']}`, + }, + ] }, ] }, @@ -5427,6 +5997,7 @@ module.exports = function(s,config,lang){ "name": lang["Points"], "color": "orange", "section-pre-class": "col-md-6", + "style": "overflow:auto", "blockquoteClass": "global_tip", "blockquote": lang.RegionNote, "info": [ @@ -5434,7 +6005,7 @@ module.exports = function(s,config,lang){ "fieldType": "div", class: "canvas_holder", divContent: `
- `, +
`, } ] } @@ -5443,6 +6014,21 @@ module.exports = function(s,config,lang){ "Schedules": { "section": "Schedules", "blocks": { + "Info": { + "name": lang["Monitor States and Schedules"], + "color": "blue", + "section-pre-class": "col-md-12", + "blockquoteClass": "global_tip", + "blockquote": lang.MonitorStatesText, + "info": [ + { + "fieldType": "btn", + "attribute": `page-open="monitorStates"`, + "class": `btn-primary`, + "btnContent": `   ${lang["Monitor States"]}`, + }, + ] + }, "Schedules": { "name": lang["Schedules"], "color": "orange", @@ -5574,8 +6160,23 @@ module.exports = function(s,config,lang){ "Monitor States": { "section": "Monitor States", "blocks": { + "Info": { + "name": lang["Monitor States and Schedules"], + "color": "blue", + "section-pre-class": "col-md-12", + "blockquoteClass": "global_tip", + "blockquote": lang.MonitorStatesText, + "info": [ + { + "fieldType": "btn", + "attribute": `page-open="schedules"`, + "class": `btn-primary`, + "btnContent": `   ${lang["Schedules"]}`, + }, + ] + }, "Monitor States": { - "name": lang["Monitor States"], + noHeader: true, "color": "green", "section-pre-class": "col-md-6", "info": [ @@ -5641,48 +6242,68 @@ module.exports = function(s,config,lang){ "name": lang["Search Settings"], "color": "green", "section-pre-class": "col-md-4", + isFormGroupGroup: true, + "noHeader": true, + "noDefaultSectionClasses": true, "info": [ { - "field": lang["Monitor"], - "fieldType": "select", - "class": "dark monitors_list", - "possible": [] - }, - { - "id": "timelapsejpeg_date", - "field": lang.Date, - }, - { - "id": "timelapseJpegFps", - "field": lang["Frame Rate"], - "fieldType": "range", - "min": "1", - "max": "30", - }, - { - "fieldType": "btn-group", - "btns": [ + isFormGroupGroup: true, + "noHeader": true, + "info": [ { - "fieldType": "btn", - "class": `btn-primary playPause playPauseText`, - "btnContent": `${lang['Play']}`, + "field": lang["Monitor"], + "fieldType": "select", + "class": "monitors_list", + "possible": [] }, { - "fieldType": "btn", - "class": `btn-success download_mp4`, - "btnContent": `${lang['Download']}`, + "id": "timelapsejpeg_date", + "field": lang.Date, }, - ], + { + "id": "timelapseJpegFps", + "field": lang["Frame Rate"], + "fieldType": "range", + "min": "1", + "max": "30", + }, + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `btn-primary playPause playPauseText`, + "btnContent": ` ${lang['Play']}`, + }, + { + "fieldType": "btn", + "class": `btn-success download_mp4`, + "btnContent": `${lang['Download']}`, + }, + ], + } + ] }, { - "fieldType": "div", - "class": "frameIcons mt-3 row scroll-style-6", + isFormGroupGroup: true, + "headerTitle": ` + ${lang['Delete selected']} +
+ +
`, + "info": [ + { + "fieldType": "form", + "class": "frameIcons mt-3 mb-0 row scroll-style-6", + } + ] } ] }, "Watch": { - "name": lang['Watch'], + noHeader: true, "color": "blue", + style: 'padding:0!important', "section-pre-class": "col-md-8", "info": [ { @@ -5731,6 +6352,23 @@ module.exports = function(s,config,lang){ hidden:true, "name": "id", }, + { + "name": "enabled", + "field": lang.Enabled, + "fieldType": "select", + "default": "1", + "possible": [ + { + "name": "No", + "value": "0", + }, + { + "name": lang.Yes, + "value": "1", + "selected": true + } + ] + }, { "name": "filter_name", "field": lang['Filter Name'], @@ -5762,6 +6400,7 @@ module.exports = function(s,config,lang){ "Action for Selected": { "name": lang["Action for Selected"], "color": "red", + "blockquote": lang.eventFilterActionText, "section-class": "actions", "info": [ { @@ -5769,153 +6408,80 @@ module.exports = function(s,config,lang){ "field": "Drop Event", "fieldType": "select", "form-group-class": "actions-row", - "description": "Make the event do nothing, as if it never happened.", - "default": "No", + "description": lang["fieldTextActionsHalt"], + "default": "0", "possible": [ { "name": "No", "value": "0", - "info": "Allow other functions to continue.", "selected": true }, { - "name": "Yes", + "name": lang.Yes, "value": "1", - "info": "Use Traditional Recording, Hotswap, or Delete Motionless with their currently set options in the Global Detection Settings section." } ] }, { "name": "actions=save", - "field": "Save Events to SQL", + "field": lang['Save Events'], "fieldType": "select", - "description": "Save Motion Events in SQL. This will allow display of motion over video during the time motion events occured in the Power Viewer.", "default": "Yes", "form-group-class": "actions-row", "possible": [ { - "name": "Default", + "name": lang['Original Choice'], "value": "", - "info": "Use values set in Global Detector Settings.", "selected": true }, { - "name": "No", - "value": "0", - "info": "Finish the current 10 minute order." - }, - { - "name": "Yes", + "name": lang.Yes, "value": "1", - "info": "Reset the timer" } ] }, { - "name": "actions=mail", - "field": "Email on Trigger", - "fieldType": "select", - "form-group-class": "actions-row", - "description": "Recieve an email of an image during a motion event to the master account for the camera group. You must setup SMTP details in conf.json.", - "default": "No", - "example": "1", - "possible": [ - { - "name": "Default", - "value": "", - "info": "Use values set in Global Detector Settings.", - "selected": true - }, - { - "name": "No", - "value": "0", - "info": "No Email." - }, - { - "name": "Yes", - "value": "1", - "info": "Send Email." - } - ] + "name": "actions=indifference", + "field": "Modify Indifference", + "description": lang["fieldTextActionsIndifference"], + "form-group-class": "actions-row", }, { "name": "actions=webhook", - "field": "Webhook on Trigger", + "field": lang['Legacy Webhook'], "fieldType": "select", "form-group-class": "actions-row", - "description": "Send a GET request during an event to the URL specified. Webhook location can be specified in the Global Detector Settings for the Monitor.", - "default": "No", + "default": "", "example": "1", "possible": [ { - "name": "Default", + "name": lang['Original Choice'], "value": "", - "info": "Use values set in Global Detector Settings.", "selected": true }, { - "name": "No", - "value": "0", - "info": "No Webhook." - }, - { - "name": "Yes", + "name": lang.Yes, "value": "1", - "info": "Send Webhook." } ] }, - { - "name": "actions=discord", - "field": "Discord Alert on Trigger", - "fieldType": "select", - "form-group-class": "actions-row", - "description": "Recieve a Discord Notification with an image or video during an event to the Discord channel specified. Discord Bot and Channel settings can be changed in your Account Settings.", - "default": "No", - "example": "1", - "possible": [ - { - "name": "Default", - "value": "", - "info": "Use values set in Global Detector Settings.", - "selected": true - }, - { - "name": "No", - "value": "0", - "info": "No Alert." - }, - { - "name": "Yes", - "value": "1", - "info": "Get a Message to Discord." - } - ] - }, { "name": "actions=command", "field": "Detector Command", "fieldType": "select", "form-group-class": "actions-row", - "description": "You may use this to trigger a script on command.", + "description": lang["fieldTextActionsCommand"], "default": "No", "form-group-class": "actions-row", "possible": [ { - "name": "Default", + "name": lang['Original Choice'], "value": "", - "info": "Use values set in Global Detector Settings.", "selected": true }, { - "name": "No", - "value": "0", - "info": "No script will run." - }, - { - "name": "Yes", + "name": lang.Yes, "value": "1", - "info": "Trigger the script that is set in the Command option. Command is only visible when selecting this option." } ] }, @@ -5923,34 +6489,21 @@ module.exports = function(s,config,lang){ "name": "actions=record", "field": "Use Record Method", "fieldType": "select", - "description": "Use Traditional Recording, Hotswap, or Delete Motionless with their currently set options in the Global Detection Settings section.", - "default": "No", + "description": lang["fieldTextActionsRecord"], + "default": "", "form-group-class": "actions-row", "possible": [ { - "name": "Default", + "name": lang['Original Choice'], "value": "", - "info": "Use values set in Global Detector Settings.", "selected": true }, { - "name": "No", - "value": "0", - "info": "No Traditional Recording, Hotswap, or Delete Motionless." - }, - { - "name": "Yes", + "name": lang.Yes, "value": "1", - "info": "Use Traditional Recording, Hotswap, or Delete Motionless with their currently set options in the Global Detection Settings section." } ] }, - { - "name": "actions=indifference", - "field": "Modify Indifference", - "description": "Modify minimum indifference required for event.", - "form-group-class": "actions-row", - }, ] }, } @@ -5967,7 +6520,7 @@ module.exports = function(s,config,lang){ { "name": "ip", "field": lang['IP Address'], - "description": lang['Range or Single'], + "description": lang[lang["fieldTextIp"]], "example": "10.1.100.1-10.1.100.254", }, { @@ -6015,6 +6568,17 @@ module.exports = function(s,config,lang){ } ] }, + "Other Devices": { + "name": lang['Other Devices'], + "color": "danger", + "section-pre-class": "col-md-12", + "info": [ + { + "fieldType": "div", + "class": "onvif_result_error row", + } + ] + }, } }, "Camera Probe": { @@ -6267,5 +6831,1871 @@ module.exports = function(s,config,lang){ }, } }, - } + "Monitor Settings Additional Input Map": { + "section": "Monitor Settings Additional Input Map", + "blocks": { + "Connection" : { + "id": `monSectionMap$[TEMP_ID]`, + "name": `${lang['Input Map']} $[NUMBER]`, + "section-class": "input-map", + "color": "orange", + "isSection": true, + "info": [ + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `btn-danger delete mb-2`, + "btnContent": `${lang['Delete']}`, + } + ], + }, + { + name:'map-detail=type', + field:lang['Input Type'], + default:'h264', + attribute:'selector="h_i_$[TEMP_ID]"', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": "H.264 / H.265 / H.265+", + "value": "h264" + }, + { + "name": "JPEG", + "value": "jpeg" + }, + { + "name": "MJPEG", + "value": "mjpeg" + }, + { + "name": "HLS (.m3u8)", + "value": "hls" + }, + { + "name": "MPEG-4 (.mp4 / .ts)", + "value": "mp4" + }, + { + "name": "Local", + "value": "local" + }, + { + "name": "Raw", + "value": "raw" + } + ] + }, + { + name:'map-detail=fulladdress', + field:lang['Full URL Path'], + placeholder:'Example : rtsp://admin:password@123.123.123.123/stream/1', + type:'text', + }, + { + name:'map-detail=sfps', + field:lang['Monitor Capture Rate'], + placeholder:'', + type:'text', + }, + { + name:'map-detail=aduration', + field:lang['Analyzation Duration'], + placeholder:'Example : 1000000', + type:'text', + }, + { + name:'map-detail=probesize', + field:lang['Probe Size'], + placeholder:'Example : 1000000', + type:'text', + }, + { + name:'map-detail=stream_loop', + field:lang['Loop Stream'], + "form-group-class":'h_i_$[TEMP_ID]_input h_i_$[TEMP_ID]_mp4 h_i_$[TEMP_ID]_raw', + hidden:true, + default:'0', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.No, + "value": "0", + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }, + { + name:'map-detail=rtsp_transport', + field:lang['RTSP Transport'], + "form-group-class":'h_i_$[TEMP_ID]_input h_i_$[TEMP_ID]_h264', + default:'', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.Auto, + "value": "", + "info": lang["fieldTextMapRtspTransportAuto"] + }, + { + "name": "TCP", + "value": "tcp", + "info": lang["fieldTextMapRtspTransportTCP"] + }, + { + "name": "UDP", + "value": "udp", + "info": lang["fieldTextMapRtspTransportUDP"] + } + ] + }, + { + name:'map-detail=accelerator', + field:lang['Accelerator'], + selector:'h_accel_$[TEMP_ID]', + default:'0', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.No, + "value": "0", + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }, + { + name:'map-detail=hwaccel', + field:lang['hwaccel'], + "form-group-class":'h_accel_$[TEMP_ID]_input h_accel_$[TEMP_ID]_1', + hidden:true, + default:'', + "fieldType": "select", + type:'selector', + possible: s.listOfHwAccels + }, + { + name:'map-detail=hwaccel_vcodec', + field:lang['hwaccel_vcodec'], + "form-group-class":'h_accel_$[TEMP_ID]_input h_accel_$[TEMP_ID]_1', + hidden:true, + default:'auto', + "fieldType": "select", + type:'selector', + possible:[ + { + "name": lang.Auto + '('+lang.Recommended+')', + "value": "" + }, + { + "name": lang.NVIDIA, + "optgroup": [ + { + "name": lang.h264_cuvid, + "value": "h264_cuvid" + }, + { + "name": lang.hevc_cuvid, + "value": "hevc_cuvid" + }, + { + "name": lang.mjpeg_cuvid, + "value": "mjpeg_cuvid" + }, + { + "name": lang.mpeg4_cuvid, + "value": "mpeg4_cuvid" + }, + ] + }, + { + "name": lang["Quick Sync Video"], + "optgroup": [ + { + "name": lang.h264_qsv, + "value": "h264_qsv" + }, + { + "name": lang.hevc_qsv, + "value": "hevc_qsv" + }, + { + "name": lang.mpeg2_qsv, + "value": "mpeg2_qsv" + }, + ] + }, + { + "name": lang['Raspberry Pi'], + "optgroup": [ + { + "name": lang.h264_mmal, + "value": "h264_mmal" + }, + { + "name": lang.mpeg2_mmal, + "value": "mpeg2_mmal" + }, + { + "name": lang["MPEG-4 (Raspberry Pi)"], + "value": "mpeg4_mmal" + } + ] + }, + ] + }, + { + name:'map-detail=hwaccel_device', + field:lang['hwaccel_device'], + "form-group-class":'h_accel_$[TEMP_ID]_input h_accel_$[TEMP_ID]_1', + hidden:true, + placeholder:'Example : /dev/dri/video0', + type:'text', + }, + { + name:'map-detail=cust_input', + field:lang['Input Flags'], + type:'text', + }, + ] + } + } + }, + "Monitor Settings Additional Stream Channel": { + "section": "Monitor Settings Additional Stream Channel", + "blocks": { + "Stream" : { + "id": `monSectionChannel$[TEMP_ID]`, + "name": `${lang["Stream Channel"]} $[NUMBER]`, + "color": "blue", + "input-mapping": "stream_channel-$[NUMBER]", + "isSection": true, + "section-class": "stream-channel", + "info": [ + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `btn-danger delete mb-2`, + "btnContent": `${lang['Delete']}`, + } + ], + }, + { + "field": lang["Stream Type"], + "name": `channel-detail="stream_type"`, + "description": lang["fieldTextChannelStreamType"], + "default": "mp4", + "selector": "h_st_channel_$[TEMP_ID]", + "fieldType": "select", + "attribute": `triggerChange="#monSectionChannel$[TEMP_ID] [channel-detail=stream_vcodec]" triggerChangeIgnore="b64,mjpeg"`, + "possible": [ + { + "name": lang.Poseidon, + "value": "mp4", + "info": lang["fieldTextChannelStreamTypePoseidon"] + }, + { + "name": lang["RTMP Stream"], + "value": "rtmp", + }, + { + "name": lang['MJPEG'], + "value": "mjpeg", + "info": lang["fieldTextChannelStreamTypeMJPEG"] + }, + { + "name": lang['FLV'], + "value": "flv", + "info": lang["fieldTextChannelStreamTypeFLV"] + }, + { + "name": lang['HLS (includes Audio)'], + "value": "hls", + "info": lang["fieldTextChannelStreamTypeHLS(includesAudio)"] + } + ] + }, + { + "field": lang['Server URL'], + "name": `channel-detail="rtmp_server_url"`, + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_rtmp", + "example": "rtmp://live-api.facebook.com:80/rtmp/", + }, + { + "field": lang['Stream Key'], + "name": `channel-detail="rtmp_stream_key"`, + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_rtmp", + "example": "1111111111?ds=1&a=xxxxxxxxxx", + }, + { + "field": lang['# of Allow MJPEG Clients'], + "name": `channel-detail="stream_mjpeg_clients"`, + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg", + "placeholder": "20", + }, + { + "field": lang['Video Codec'], + "name": `channel-detail="stream_vcodec"`, + "description": lang["fieldTextChannelStreamVcodec"], + "default": "no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + "fieldType": "select", + "selector": "h_hls_v_channel_$[TEMP_ID]", + "possible": [ + { + "name": lang.Auto, + "value": "no", + "info": lang["fieldTextChannelStreamVcodecAuto"] + }, + { + "name": "libx264", + "value": "libx264", + "info": lang["fieldTextChannelStreamVcodecLibx264"] + }, + { + "name": "libx265", + "value": "libx265", + "info": lang["fieldTextChannelStreamVcodecLibx265"] + }, + { + "name": lang.copy, + "value": "copy", + "info": lang["fieldTextChannelStreamVcodecCopy"] + }, + { + "name": lang['Hardware Accelerated'], + "optgroup": [ + { + "name": "H.264 VA-API (Intel HW Accel)", + "value": "h264_vaapi" + }, + { + "name": "H.265 VA-API (Intel HW Accel)", + "value": "hevc_vaapi" + }, + { + "name": "H.264 NVENC (NVIDIA HW Accel)", + "value": "h264_nvenc" + }, + { + "name": "H.265 NVENC (NVIDIA HW Accel)", + "value": "hevc_nvenc" + }, + { + "name": "H.264 (Quick Sync Video)", + "value": "h264_qsv" + }, + { + "name": "H.265 (Quick Sync Video)", + "value": "hevc_qsv" + }, + { + "name": "MPEG2 (Quick Sync Video)", + "value": "mpeg2_qsv" + }, + { + "name": "H.264 (Quick Sync Video)", + "value": "h264_qsv" + }, + { + "name": "H.265 (Quick Sync Video)", + "value": "hevc_qsv" + }, + { + "name": "MPEG2 (Quick Sync Video)", + "value": "mpeg2_qsv" + }, + { + "name": "H.264 openMAX (Raspberry Pi)", + "value": "h264_omx" + } + ] + }, + ] + }, + { + "field": lang["Audio Codec"], + "name": `channel-detail="stream_acodec"`, + "description": lang["fieldTextChannelStreamAcodec"], + "default": "0", + "example": "", + "fieldType": "select", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + "possible": [ + { + "name": lang.Auto, + "info": lang["fieldTextChannelStreamAcodecAuto"], + "value": "" + }, + { + "name": lang["No Audio"], + "info": lang["fieldTextChannelStreamAcodecNoAudio"], + "value": "no" + }, + { + "name": "libvorbis", + "info": lang["fieldTextChannelStreamAcodecLibvorbis"], + "value": "libvorbis" + }, + { + "name": "libopus", + "info": lang["fieldTextChannelStreamAcodecLibopus"], + "value": "libopus" + }, + { + "name": "libmp3lame", + "info": lang["fieldTextChannelStreamAcodecLibmp3lame"], + "value": "libmp3lame" + }, + { + "name": "aac", + "info": lang["fieldTextChannelStreamAcodecAac"], + "value": "aac" + }, + { + "name": "ac3", + "info": lang["fieldTextChannelStreamAcodecAc3"], + "value": "ac3" + }, + { + "name": "copy", + "info": lang["fieldTextChannelStreamAcodecCopy"], + "value": "copy" + } + ] + }, + { + "name": "channel-detail=hls_time", + "field": lang["HLS Segment Length"], + "description": lang["fieldTextChannelHlsTime"], + "default": "2", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_hls", + }, + { + "name": "channel-detail=hls_list_size", + "field": lang["HLS List Size"], + "description": lang["fieldTextChannelHlsListSize"], + "default": "2", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_hls", + }, + { + "name": "channel-detail=preset_stream", + "field": lang["HLS Preset"], + "description": lang["fieldTextChannelPresetStream"], + "example": "ultrafast", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_hls", + }, + { + "name": "channel-detail=stream_quality", + "field": lang.Quality, + "description": lang["fieldTextChannelStreamQuality"], + "default": "15", + "example": "1", + "form-group-class-pre-layer": "h_hls_v_channel_$[TEMP_ID]_input h_hls_v_channel_$[TEMP_ID]_libx264 h_hls_v_channel_$[TEMP_ID]_libx265 h_hls_v_channel_$[TEMP_ID]_h264_nvenc h_hls_v_channel_$[TEMP_ID]_hevc_nvenc h_hls_v_channel_$[TEMP_ID]_no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_jsmpeg h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + "possible": "1-23" + }, + { + "name": "channel-detail=stream_v_br", + "field": lang["Video Bit Rate"], + "placeholder": "", + "form-group-class-pre-layer": "h_hls_v_channel_$[TEMP_ID]_input h_hls_v_channel_$[TEMP_ID]_libx264 h_hls_v_channel_$[TEMP_ID]_libx265 h_hls_v_channel_$[TEMP_ID]_h264_nvenc h_hls_v_channel_$[TEMP_ID]_hevc_nvenc h_hls_v_channel_$[TEMP_ID]_no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_jsmpeg h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + }, + { + "name": "channel-detail=stream_a_br", + "field": lang["Audio Bit Rate"], + "placeholder": "128k", + "form-group-class-pre-layer": "h_hls_v_channel_$[TEMP_ID]_input h_hls_v_channel_$[TEMP_ID]_libx264 h_hls_v_channel_$[TEMP_ID]_libx265 h_hls_v_channel_$[TEMP_ID]_h264_nvenc h_hls_v_channel_$[TEMP_ID]_hevc_nvenc h_hls_v_channel_$[TEMP_ID]_no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_jsmpeg h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + }, + { + "name": "channel-detail=stream_fps", + "field": lang['Frame Rate'], + "description": lang["fieldTextChannelStreamFps"], + "form-group-class-pre-layer": "h_hls_v_channel_$[TEMP_ID]_input h_hls_v_channel_$[TEMP_ID]_libx264 h_hls_v_channel_$[TEMP_ID]_libx265 h_hls_v_channel_$[TEMP_ID]_h264_nvenc h_hls_v_channel_$[TEMP_ID]_hevc_nvenc h_hls_v_channel_$[TEMP_ID]_no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_jsmpeg h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + }, + { + "name": "channel-detail=stream_scale_x", + "field": lang.Width, + "description": lang["fieldTextChannelStreamScaleX"], + "fieldType": "number", + "numberMin": "1", + "example": "640", + "form-group-class-pre-layer": "h_hls_v_channel_$[TEMP_ID]_input h_hls_v_channel_$[TEMP_ID]_libx264 h_hls_v_channel_$[TEMP_ID]_libx265 h_hls_v_channel_$[TEMP_ID]_h264_nvenc h_hls_v_channel_$[TEMP_ID]_hevc_nvenc h_hls_v_channel_$[TEMP_ID]_no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_jsmpeg h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + }, + { + "name": "channel-detail=stream_scale_y", + "field": lang.Height, + "description": lang["fieldTextChannelStreamScaleY"], + "fieldType": "number", + "numberMin": "1", + "example": "480", + "form-group-class-pre-layer": "h_hls_v_channel_$[TEMP_ID]_input h_hls_v_channel_$[TEMP_ID]_libx264 h_hls_v_channel_$[TEMP_ID]_libx265 h_hls_v_channel_$[TEMP_ID]_h264_nvenc h_hls_v_channel_$[TEMP_ID]_hevc_nvenc h_hls_v_channel_$[TEMP_ID]_no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_jsmpeg h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + }, + { + "name": "channel-detail=stream_rotate", + "field": lang["Rotate"], + "description": lang["fieldTextChannelStreamRotate"], + "fieldType": "select", + "form-group-class-pre-layer": "h_hls_v_channel_$[TEMP_ID]_input h_hls_v_channel_$[TEMP_ID]_libx264 h_hls_v_channel_$[TEMP_ID]_libx265 h_hls_v_channel_$[TEMP_ID]_h264_nvenc h_hls_v_channel_$[TEMP_ID]_hevc_nvenc h_hls_v_channel_$[TEMP_ID]_no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_jsmpeg h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + "possible": [ + { + "name": lang["No Rotation"], + "value": "no" + }, + { + "name": lang["180 Degrees"], + "value": "2,transpose=2" + }, + { + "name": lang["90 Counter Clockwise and Vertical Flip (default)"], + "value": "0" + }, + { + "name": lang["90 Clockwise"], + "value": "1" + }, + { + "name": lang["90 Clockwise and Vertical Flip"], + "value": "2" + }, + { + "name": lang["90 Clockwise and Vertical Flip"], + "value": "3" + } + ] + }, + { + isAdvanced: true, + "name": "channel-detail=svf", + "field": lang["Video Filter"], + "description": lang["fieldTextChannelSvf"], + "form-group-class-pre-layer": "h_hls_v_channel_$[TEMP_ID]_input h_hls_v_channel_$[TEMP_ID]_libx264 h_hls_v_channel_$[TEMP_ID]_libx265 h_hls_v_channel_$[TEMP_ID]_h264_nvenc h_hls_v_channel_$[TEMP_ID]_hevc_nvenc h_hls_v_channel_$[TEMP_ID]_no", + "form-group-class": "h_st_channel_$[TEMP_ID]_input h_st_channel_$[TEMP_ID]_mjpeg h_st_channel_$[TEMP_ID]_hls h_st_channel_$[TEMP_ID]_rtmp h_st_channel_$[TEMP_ID]_jsmpeg h_st_channel_$[TEMP_ID]_flv h_st_channel_$[TEMP_ID]_mp4 h_st_channel_$[TEMP_ID]_h264", + }, + { + "name": "channel-detail=cust_stream", + "field": lang["Stream Flags"], + }, + ] + } + } + }, + "Monitor Stream Window": { + "section": "Monitor Stream Window", + // gridBlockClass: "", + // streamBlockPreHtml: ` + // `, + streamBlockHudHtml: `
+
+
+
+
+
`, + streamBlockHudControlsHtml: ` + + + ${lang['Trigger Event']} + `, + gridBlockAfterContentHtml: `
+
+
+
+
+
+
+
+ $QUICKLINKS +
+
$MONITOR_NAME
+
+
+
+
+
+
+
+
`, + quickLinks: { + "Options": { + "label": lang['Options'], + "class": "default toggle-live-grid-monitor-menu", + "icon": "bars" + }, + "Fullscreen": { + "label": lang['Fullscreen'], + "class": "default toggle-live-grid-monitor-fullscreen", + "icon": "arrows-alt" + }, + "Monitor Settings": { + "label": lang['Monitor Settings'], + "class": "default open-monitor-settings", + "icon": "wrench", + eval: `isSubAccount && permissionCheck('monitor_edit',monitorId)`, + }, + "Toggle Substream": { + "label": lang['Toggle Substream'], + "class": "warning toggle-monitor-substream", + "icon": "eye" + }, + "Snapshot": { + "label": lang['Snapshot'], + "class": "primary snapshot-live-grid-monitor", + "icon": "camera" + }, + "Videos List": { + "label": lang['Videos List'], + "class": "default open-videosTable", + "icon": "film", + eval: `isSubAccount && permissionCheck('video_view',monitorId)`, + }, + "Reconnect Stream": { + "label": lang['Reconnect Stream'], + "class": "success signal reconnect-live-grid-monitor", + "icon": "plug" + }, + "Control": { + "label": lang['Control'], + "class": "default toggle-live-grid-monitor-ptz-controls", + "icon": "arrows", + eval: `monitor.details.control === '1'`, + }, + "Show Logs": { + "label": lang['Show Logs'], + "class": "warning toggle-live-grid-monitor-logs", + "icon": "exclamation-triangle" + }, + "Close": { + "label": lang['Close'], + "class": "danger close-live-grid-monitor", + "icon": "times" + } + }, + links: { + "Mute Audio": { + "label": lang['Mute Audio'], + "attr": `system="monitorMuteAudioSingle" mid="$MONITOR_ID"`, + "class": "primary", + "icon": '$MONITOR_MUTE_ICON' + }, + "Snapshot": { + "label": lang['Snapshot'], + "class": "primary snapshot-live-grid-monitor", + "icon": "camera" + }, + "Show Logs": { + "label": lang['Show Logs'], + "class": "warning toggle-live-grid-monitor-logs", + "icon": "exclamation-triangle" + }, + "Toggle Substream": { + "label": lang['Toggle Substream'], + "class": "warning toggle-monitor-substream", + "icon": "eye" + }, + "Control": { + "label": lang['Control'], + "class": "default toggle-live-grid-monitor-ptz-controls", + "icon": "arrows", + eval: `monitor.details.control === '1'`, + }, + "Reconnect Stream": { + "label": lang['Reconnect Stream'], + "class": "success signal reconnect-live-grid-monitor", + "icon": "plug" + }, + "Pop": { + "label": lang['Pop'], + "class": "default run-live-grid-monitor-pop", + "icon": "external-link" + }, + "Zoom In": { + "label": lang['Zoom In'], + "attr": `monitor="zoomStreamWithMouse"`, + "class": "default", + "icon": "search-plus" + }, + // "Calendar": { + // "label": lang['Calendar'], + // "attr": `monitor="calendar"`, + // "class": "default ", + // "icon": "calendar" + // }, + // "Power Viewer": { + // "label": lang['Power Viewer'], + // "attr": `monitor="powerview"`, + // "class": "default", + // "icon": "map-marker" + // }, + "Time-lapse": { + "label": lang['Time-lapse'], + "attr": `monitor="timelapseJpeg"`, + "class": "default", + "icon": "angle-double-right", + eval: `isSubAccount && permissionCheck('video_view',monitorId)`, + }, + // "Video Grid": { + // "label": "Video Grid", + // "attr": `monitor="video_grid"`, + // "class": "default", + // "icon": "th" + // }, + "Videos List": { + "label": lang['Videos List'], + "class": "default open-videosTable", + "icon": "film", + eval: `isSubAccount && permissionCheck('video_view',monitorId)`, + }, + "Monitor Settings": { + "label": lang['Monitor Settings'], + "class": "default open-monitor-settings", + "icon": "wrench", + eval: `isSubAccount && permissionCheck('monitor_edit',monitorId)`, + }, + "Fullscreen": { + "label": lang['Fullscreen'], + "class": "default toggle-live-grid-monitor-fullscreen", + "icon": "arrows-alt" + }, + "Close": { + "label": lang['Close'], + "class": "danger close-live-grid-monitor", + "icon": "times" + } + } + }, + "Monitor Options": { + "section": "Monitor Options", + "dropdownClass": `${Theme.isDark ? 'dropdown-menu-dark' : ''} ${mainBackgroundColor}` + }, + "SideMenu": { + "section": "SideMenu", + showMonitors: true, + "blocks": { + "Container1": { + // "id": "sidebarMenu", + "class": `col-md-3 col-lg-2 d-md-block ${mainBackgroundColor} sidebar collapse`, + "links": [ + { + icon: 'home', + label: lang.Home, + pageOpen: 'initial', + }, + { + icon: 'th', + label: lang['Live Grid'] + `   + 0`, + pageOpen: 'liveGrid', + addUl: true, + ulItems: [ + { + label: lang['Open All Monitors'], + class: 'open-all-monitors cursor-pointer', + color: 'orange', + }, + { + label: lang['Close All Monitors'], + class: 'close-all-monitors cursor-pointer', + color: 'red', + }, + { + label: lang['Remember Positions'], + class: 'cursor-pointer', + attributes: 'shinobi-switch="monitorOrder" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', + color: 'grey', + }, + { + label: lang['Mute Audio'], + class: 'cursor-pointer', + attributes: 'shinobi-switch="monitorMuteAudio" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', + color: 'grey', + }, + { + label: lang['JPEG Mode'], + class: 'cursor-pointer', + attributes: 'shinobi-switch="jpegMode" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', + color: 'grey', + }, + { + label: lang['Stream in Background'], + class: 'cursor-pointer', + attributes: 'shinobi-switch="backgroundStream" ui-change-target=".dot" on-class="dot-grey" off-class="dot-green"', + color: 'grey', + }, + { + label: lang[`Original Aspect Ratio`], + class: 'cursor-pointer', + attributes: 'shinobi-switch="dontMonStretch" ui-change-target=".dot" on-class="dot-grey" off-class="dot-green"', + color: 'grey', + }, + { + label: lang[`Hide Detection on Stream`], + class: 'cursor-pointer', + attributes: 'shinobi-switch="dontShowDetection" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', + color: 'grey', + }, + ] + }, + { + icon: 'video-camera', + label: `${lang.Monitors}   + `, + pageOpen: 'monitorsList', + }, + { + icon: 'film', + label: `${lang['Videos']}`, + pageOpen: 'videosTableView', + addUl: true, + ulItems: [ + { + label: lang[`Save Compressed Video on Completion`], + class: 'cursor-pointer', + attributes: 'shinobi-switch="saveCompressedVideo" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', + color: 'grey', + }, + ] + }, + { + icon: 'map-marker', + label: `${lang['Power Viewer']}`, + pageOpen: 'powerVideo', + }, + { + icon: 'calendar', + label: `${lang['Calendar']}`, + pageOpen: 'calendarView', + }, + { + icon: 'fast-forward', + label: `${lang['Time-lapse']}`, + pageOpen: 'timelapseViewer', + addUl: true, + ulItems: [ + { + label: lang[`Save Built Video on Completion`], + class: 'cursor-pointer', + attributes: 'shinobi-switch="timelapseSaveBuiltVideo" ui-change-target=".dot" on-class="dot-green" off-class="dot-grey"', + color: 'grey', + }, + ] + }, + { + icon: 'file-o', + label: `${lang['FileBin']}`, + pageOpen: 'fileBinView', + }, + { + divider: true, + }, + { + icon: 'wrench', + label: `${lang['Monitor Settings']}`, + pageOpen: 'monitorSettings', + addUl: true, + eval: `!$user.details.sub || $user.details.monitor_create !== 0`, + }, + { + icon: 'grav', + label: `${lang['Region Editor']}`, + pageOpen: 'regionEditor', + eval: `!$user.details.sub`, + }, + { + icon: 'filter', + label: `${lang['Event Filters']}`, + pageOpen: 'eventFilters', + eval: `!$user.details.sub`, + }, + { + icon: 'align-right', + label: `${lang['Monitor States']}`, + pageOpen: 'monitorStates', + eval: `!$user.details.sub`, + }, + { + icon: 'clock-o', + label: `${lang['Schedules']}`, + pageOpen: 'schedules', + eval: `!$user.details.sub`, + }, + { + icon: 'exclamation-triangle', + label: `${lang['Logs']}`, + pageOpen: 'logViewer', + eval: `!$user.details.sub || $user.details.view_logs !== 0`, + }, + { + divider: true, + }, + { + icon: 'gears', + label: `${lang['Account Settings']}`, + pageOpen: 'accountSettings', + eval: `!$user.details.sub || $user.details.user_change !== 0`, + addUl: true, + }, + { + icon: 'group', + label: `${lang.subAccountManager}`, + pageOpen: 'subAccountManager', + addUl: true, + eval: `!$user.details.sub`, + }, + { + icon: 'key', + label: `${lang['API Keys']}`, + pageOpen: 'apiKeys', + }, + { + divider: true, + }, + { + icon: 'search', + label: `${lang['ONVIF Scanner']}`, + pageOpen: 'onvifScanner', + addUl:true, + eval: `!$user.details.sub || $user.details.monitor_create !== 0`, + }, + { + icon: 'opera', + label: `${lang['ONVIF Device Manager']}`, + pageOpen: 'onvifDeviceManager', + eval: `!$user.details.sub || $user.details.monitor_create !== 0`, + }, + { + icon: 'eyedropper', + label: `${lang['FFprobe']}`, + pageOpen: 'cameraProbe', + }, + { + icon: 'compass', + label: `${lang['ShinobiHub']}`, + pageOpen: 'configFinder', + addUl: true, + eval: `!$user.details.sub || $user.details.monitor_create !== 0`, + }, + { + divider: true, + }, + { + icon: 'info-circle', + label: `${lang['Help']}`, + pageOpen: 'helpWindow', + }, + // { + // icon: 'exclamation-circle', + // label: `${lang['Events']}`, + // pageOpen: 'eventListWithPics', + // }, + { + icon: 'sign-out', + label: `${lang['Logout']}`, + class: 'logout', + }, + ] + }, + "SideMenuBeforeList": { + "name": "SideMenuBeforeList", + "color": "grey", + "noHeader": true, + "noDefaultSectionClasses": true, + "section-class": "px-3", + "info": [ + { + isFormGroupGroup: true, + "noHeader": true, + "noDefaultSectionClasses": true, + "section-class": "card btn-default text-white px-3 py-2 mb-3 border-0", + info: [ + { + "fieldType": "div", + "id": `clock`, + "style": `cursor:pointer`, + "attribute": `data-target="#time-hours"`, + "divContent": `
+ + ` + }, + ] + }, + { + "id": "indicator-bars", + isFormGroupGroup: true, + "noHeader": true, + "noDefaultSectionClasses": true, + "section-class": "card text-white bg-gradient-blue px-3 py-2 mb-3 border-0", + info: [ + { + "fieldType": "indicatorBar", + "icon": "square", + "name": "cpu", + "label": ` ${lang.CPU} : `, + }, + { + "fieldType": "indicatorBar", + "icon": "microchip", + "name": "ram", + "label": ` ${lang.MB} ${lang.RAM}`, + }, + { + id: 'disk-indicator-bars', + isFormGroupGroup: true, + "noHeader": true, + "noDefaultSectionClasses": true, + "section-class": "disk-indicator-bars", + info: [ + { + "fieldType": "indicatorBar", + "icon": "hdd-o", + "name": "disk", + "bars": 3, + "color0": "info", + "title0": lang["Video Share"], + "color1": "danger", + "title1": lang["Timelapse Frames Share"], + "color2": "warning", + "title2": lang["FileBin Share"], + "label": ``, + }, + ] + }, + { + "fieldType": "indicatorBar", + "percent": 0, + "color": 'warning', + "indicatorPercentClass": 'activeCameraCount', + "icon": "video-camera", + "name": "activeCameraCount", + "label": lang['Active Monitors'], + }, + ] + } + ] + }, + "SideMenuAfterList": { + "name": "SideMenuAfterList", + "noHeader": true, + "noDefaultSectionClasses": true, + "info": [] + } + } + }, + "Home": { + "section": "Home", + "blocks": { + // "Container1": { + // "name": "Container1", + // "color": "grey", + // "noHeader": true, + // "noDefaultSectionClasses": true, + // "section-class": "col-md-3 pt-3", + // "info": [ + // { + // "fieldType": "div", + // "class": `card ${mainBackgroundColor} mb-3`, + // "divContent": `
+ //
${lang['Live Grid']}
+ //

${lang.liveGridDescription}

+ // ${lang.Open} + //
` + // }, + // { + // "fieldType": "div", + // "class": `card ${mainBackgroundColor} mb-3`, + // "divContent": `
+ //
${lang['Account Settings']}
+ //

${lang.accountSettingsDescription}

+ // ${lang.Open} + //
` + // }, + // ] + // }, + "Container4": { + "name": "Container4", + "color": "grey", + "noHeader": true, + "noDefaultSectionClasses": true, + "section-class": "col-lg-12", + "info": [ + { + ejs: 'web/pages/blocks/home/recentVideos', + }, + ] + } + } + }, + "Power Viewer": { + "section": lang["Power Viewer"], + "blocks": { + "Video Playback": { + id: "powerVideoVideoPlayback", + noHeader: true, + noDefaultSectionClasses: true, + "color": "green", + "section-pre-class": "col-md-8 search-parent", + "info": [ + { + "id": "powerVideoMonitorViews", + "fieldType": "div", + }, + { + "id": "powerVideoMonitorControls", + "color": "blue", + noHeader: true, + isSection: true, + isFormGroupGroup: true, + 'section-class': 'text-center', + "info": [ + { + "fieldType": "btn-group", + "normalWidth": true, + "btns": [ + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="toggleZoom" title="${lang['Zoom In']}"`, + "btnContent": ``, + }, + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="toggleMute" title="${lang['Zoom In']}"`, + "btnContent": ``, + }, + ], + }, + { + "fieldType": "btn-group", + "normalWidth": true, + "btns": [ + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="previousVideoAll" title="${lang['Previous Video']}"`, + "btnContent": ``, + }, + { + "fieldType": "btn", + "class": `btn-danger btn-sm`, + "attribute": `powerVideo-control="playAll" title="${lang['Play']}"`, + "btnContent": ``, + }, + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="pauseAll" title="${lang['Pause']}"`, + "btnContent": ``, + }, + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="nextVideoAll" title="${lang['Next Video']}"`, + "btnContent": ``, + }, + ], + }, + { + "fieldType": "btn-group", + "style": "font-family: monospace;", + "normalWidth": true, + "btns": [ + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="playSpeedAll" data-speed="1"`, + "btnContent": `1`, + }, + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="playSpeedAll" data-speed="5"`, + "btnContent": `5`, + }, + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="playSpeedAll" data-speed="10"`, + "btnContent": `10`, + }, + { + "fieldType": "btn", + "class": `btn-default btn-sm`, + "attribute": `powerVideo-control="playSpeedAll" data-speed="15"`, + "btnContent": `15`, + }, + ], + }, + ] + }, + ] + }, + "Container2": { + id: "powerVideoTabs", + noHeader: true, + "section-pre-class": "col-md-4", + attribute: `tab-chooser-parent`, + "info": [ + { + "field": lang['Monitors'], + "id": "powerVideoMonitorsList", + "fieldType": "form", + "style": "overflow-y:auto;max-height:200px;", + }, + { + "id": "powerVideoDateRange", + "field": lang['Date Range'], + }, + { + "id": "powerVideoVideoLimit", + "field": lang['Video Limit'] + ` (${lang['Per Monitor']})`, + "placeholder": "0", + }, + { + "id": "powerVideoEventLimit", + "field": lang['Event Limit'] + ` (${lang['Per Monitor']})`, + "placeholder": "500", + }, + { + id:'powerVideoSet', + field: lang['Video Set'], + default:'h264', + "fieldType": "select", + possible:[ + { + "name": lang.Local, + "value": "local" + }, + { + "name": lang.Cloud, + "value": "cloud" + }, + ] + }, + ] + }, + "Time Strip": { + id: "powerVideoTimelineStripsContainer", + noHeader: true, + "color": "bg-gradient-blue text-white", + "section-pre-class": "col-md-12", + "info": [ + { + "id": "powerVideoTimelineStrips", + "fieldType": "div", + "divContent": `
${lang['Select a Monitor']}
`, + }, + ] + }, + } + }, + "Calendar": { + "section": "Calendar", + "blocks": { + "Search Settings": { + "name": lang["Search Settings"], + "color": "green", + "section-pre-class": "col-md-4", + "info": [ + { + "field": lang["Monitor"], + "fieldType": "select", + "class": "monitors_list", + "possible": [] + }, + { + "class": "date_selector", + "field": lang.Date, + }, + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `btn-success fill refresh-data mb-3`, + "icon": `refresh`, + "btnContent": `${lang['Refresh']}`, + }, + ], + } + ] + }, + "Calendar": { + noHeader: true, + "section-pre-class": "col-md-8", + "info": [ + { + "fieldType": "div", + "id": "calendar_draw_area", + "divContent": "" + } + ] + }, + } + }, + "FileBin": { + "section": "FileBin", + "blocks": { + "Search Settings": { + "name": lang["Search Settings"], + "color": "green", + "section-pre-class": "col-md-4", + "info": [ + { + "field": lang["Monitor"], + "fieldType": "select", + "class": "monitors_list", + "possible": [] + }, + { + "class": "date_selector", + "field": lang.Date, + }, + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `btn-success fill refresh-data mb-3`, + "icon": `refresh`, + "btnContent": `${lang['Refresh']}`, + }, + ], + }, + { + "fieldType": "div", + "id": "fileBin_preview_area", + "divContent": "" + } + ] + }, + "FileBin": { + noHeader: true, + "section-pre-class": "col-md-8", + "info": [ + { + "fieldType": "table", + "attribute": `data-classes="table table-striped"`, + "id": "fileBin_draw_area", + "divContent": "" + } + ] + }, + } + }, + "Videos Table": { + "section": "Videos Table", + "blocks": { + "Search Settings": { + "name": lang["Search Settings"], + "color": "green", + "section-pre-class": "col-md-4", + "info": [ + { + "field": lang["Monitor"], + "fieldType": "select", + "class": "monitors_list", + "possible": [] + }, + { + "id": "videosTable_tag_search", + "field": lang["Search Object Tags"], + "example": "person", + }, + { + "class": "date_selector", + "field": lang.Date, + }, + { + id:'videosTable_cloudVideos', + field: lang['Video Set'], + default:'local', + "fieldType": "select", + possible:[ + { + "name": lang.Local, + "value": "local" + }, + { + "name": lang.Archive, + "value": "archive" + }, + { + "name": lang.Cloud, + "value": "cloud" + }, + ] + }, + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `btn-success fill refresh-data mb-3`, + "icon": `refresh`, + "btnContent": `${lang['Refresh']}`, + }, + ], + }, + { + "fieldType": "div", + "id": "videosTable_preview_area", + "divContent": "" + }, + ] + }, + "theTable": { + noHeader: true, + "section-pre-class": "col-md-8", + "info": [ + { + "fieldType": "table", + "attribute": `data-classes="table table-striped"`, + "id": "videosTable_draw_area", + "divContent": "" + } + ] + }, + } + }, + "Admin Account Settings": { + "section": "Admin Account Settings", + "blocks": { + "Editor": { + "name": lang["Admin Account Settings"], + "color": "blue", + "info": [ + { + "name": "mail", + "field": lang.Email, + }, + { + "name": "ke", + "field": lang['Group Key'], + }, + { + "name": "pass", + "type": "password", + "field": lang['Password'], + "fieldType": "password", + }, + { + "name": "password_again", + "type": "password", + "field": lang['Password Again'], + "fieldType": "password", + }, + { + "field": lang['2-Factor Authentication'], + "name": "detail=factorAuth", + "default":'0', + "fieldType": "select", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "name": "detail=size", + "field": lang['Max Storage Amount'], + "default": '10000' + }, + { + "name": "detail=days", + "field": `${lang['Number of Days to keep']} ${lang['Videos']}`, + "default": '5' + }, + { + "name": "detail=event_days", + "field": `${lang['Number of Days to keep']} ${lang['Events']}`, + "default": '10' + }, + { + "name": "detail=log_days", + "field": `${lang['Number of Days to keep']} ${lang['Logs']}`, + "default": '10' + }, + // { + // "name": "detail=log_timelapseFrames", + // "field": `${lang['Number of Days to keep']} ${lang['Timelapse Frames']}`, + // "default": '10' + // }, + { + "name": "detail=max_camera", + "field": lang['Max Number of Cameras'], + "placeholder": lang['Leave blank for unlimited'] + }, + { + "field": lang.Permissions, + "name": "detail=permissions", + "default":'1', + "fieldType": "select", + selector:'more_perms', + possible: [ + { + "name": lang['All Privileges'], + "value": "all" + }, + { + "name": lang.Limited, + "value": "limited" + }, + ] + }, + { + "field": lang['Can edit Max Storage'], + "name": "detail=edit_size", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Can edit Max Days'], + "name": "detail=edit_days", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Can edit how long to keep Events'], + "name": "detail=edit_event_days", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Can edit how long to keep Logs'], + "name": "detail=edit_log_days", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + // NEEDS TO MOVE DESIGNATED FILES AFTER TESTING > + { + "field": lang['Can use Amazon S3'], + "name": "detail=use_aws_s3", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Can use Wasabi Hot Cloud Storage'], + "name": "detail=use_whcs", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Can use SFTP'], + "name": "detail=use_sftp", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Can use WebDAV'], + "name": "detail=use_webdav", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Can use Discord Bot'], + "name": "detail=use_discordbot", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Can use LDAP'], + "name": "detail=use_ldap", + "default":'1', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Use Global Amazon S3 Video Storage'], + "name": "detail=aws_use_global", + "default":'0', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Use Global Wasabi Hot Cloud Storage Video Storage'], + "name": "detail=whcs_use_global", + "default":'0', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Use Global Backblaze B2 Video Storage'], + "name": "detail=b2_use_global", + "default":'0', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + { + "field": lang['Use Global WebDAV Video Storage'], + "name": "detail=webdav_use_global", + "default":'0', + "fieldType": "select", + "form-group-class":"more_perms_input more_perms_limited", + possible: [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + }, + ] + }, + // NEEDS TO MOVE DESIGNATED FILES AFTER TESTING /> + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `submit btn-success fill`, + "btnContent": `${lang['Save']}`, + }, + ], + }, + ] + }, + } + }, + "Super User Preferences": { + "section": "Super User Preferences", + "blocks": { + "Editor": { + noHeader: true, + "color": "grey", + "noDefaultSectionClasses": true, + "box-wrapper-class": "row", + "info": [ + { + isFormGroupGroup: true, + "noHeader": true, + "section-pre-class": "col-md-6", + info: [ + { + "name": "mail", + "field": lang.Email, + }, + { + "name": "pass", + "type": "password", + "fieldType": "password", + "field": lang['Password'], + }, + { + "name": "pass_again", + "type": "password", + "fieldType": "password", + "field": lang['Password Again'], + }, + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `submit btn-success`, + "btnContent": `${lang['Save']}`, + }, + ], + }, + ] + }, + { + isFormGroupGroup: true, + "name": lang["API Keys"], + "section-pre-class": "col-md-6", + info: [ + { + "fieldType": "btn-group", + "btns": [ + { + "fieldType": "btn", + "class": `new-token btn-success`, + "btnContent": `${lang['Add']}`, + }, + ], + }, + { + "id": "super-tokens", + "fieldType": "div", + }, + ] + }, + ] + } + } + }, + }) } diff --git a/languages/ar.json b/languages/ar.json index a1f3fef4..db8a43db 100644 --- a/languages/ar.json +++ b/languages/ar.json @@ -1,497 +1,1651 @@ { - "\"No Motion\" Detector": "\"لا الحركة\" كاشف", - "# of Allow MJPEG Clients": "# of Allow MJPEG Clients 0 for infinite", - "180 Degrees": "180 درجة", - "2-Factor Authentication": "2-عامل التوثيق", - "90 Clockwise": "90 اتجاه عقارب الساعة", - "90 Clockwise and Vertical Flip": "90 اتجاه عقارب الساعة العمودي الوجه", - "90 Counter Clockwise and Vertical Flip (default)": "90 عكس اتجاه عقارب الساعة و انعكاس عمودي (الافتراضي)", - "API": "API", - "API Key Added": "مفتاح API وأضاف", - "API Key Deleted": "مفتاح API حذف", - "API Keys": "مفاتيح API", - "APIKeyAddedText": "يمكنك استخدام هذا المفتاح الآن.", - "APIKeyDeletedText": "تم حذف مفتاح. وسوف لم يعد يعمل.", - "ASC": "ASC", - "Account Info": "معلومات الحساب", - "AccountEditText1": "لا يمكن تحريرها. تحديث الصفحة إذا كانت المشكلة لا تزال.", - "Accounts": "حسابات", - "Action for Selected": "العمل المحدد", - "Add": "إضافة", - "Add Monitor": "إضافة مراقبة", - "Add New": "إضافة جديدة", - "Admin": "المشرف", - "Advanced": "المتقدمة", - "Again": "مرة أخرى", - "All Monitors": "جميع الشاشات", - "All Monitors and Privileges": "جميع شاشات والامتيازات", - "All Warnings": "كل التحذيرات", - "Allow Next Command": "تسمح الأمر التالي في دقائق", - "Allow Next Email": "تسمح المقبل البريد الإلكتروني في غضون دقائق", - "Allow Next Trigger": "تسمح الزناد التالي في ميلي ثانية", - "Allowed IPs": "يسمح IPs", - "Analyzation Duration": "Analyzation مدة", - "Archive": "أرشيف", - "Audio Codec": "ترميز الصوت", - "Authenticate": "المصادقة", - "Auto": "السيارات", - "Autosave": "الحفظ التلقائي", - "Base64 over Websocket": "Base64 على Websocket", - "Bottom Left": "أسفل اليسار", - "Bottom Right": "أسفل اليمين", - "Browser Console Log": "المتصفح سجل وحدة", - "CPU": "وحدة المعالجة المركزية", - "CPU indicator will not work. Continuing...": "وحدة المعالجة المركزية المؤشر لا يعمل. استمرار...", - "CSS": "CSS أسلوب لوحة القيادة الخاصة بك.", - "Calendar": "التقويم", - "Camera Password": "كاميرا المرور", - "Camera Username": "الكاميرا المستخدم", - "Camera is not recording": "الكاميرا ليست تسجيل", - "CameraNotRecordingText": "الإعدادات قد تكون غير متوافقة. التحقق من الترميز. إعادة تشغيل...", - "Can Control Monitors": "يمكن التحكم في الشاشات", - "Can Delete Videos": "يمكن حذف الفيديو", - "Can Delete Videos and Events": "يمكن حذف الفيديو و الأحداث", - "Can Edit Monitor": "يمكن تحرير رصد", - "Can Get Logs": "يمكن الحصول على سجلات", - "Can Get Monitors": "يمكن الحصول على شاشات", - "Can View Monitor": "يمكن عرض الشاشة", - "Can View Snapshots": "يمكن عرض لقطات", - "Can View Streams": "يمكن عرض الجداول", - "Can View Videos": "يمكن عرض الفيديو", - "Can View Videos and Events": "يمكن عرض أشرطة الفيديو والأحداث", - "Can't Connect": "لا يمكن الاتصال", - "Center": "مركز عنوان URL", - "Chat on Discord": "الدردشة على الفتنة", - "Check": "تحقق", - "Check Signal Interval": "التحقق من إشارة الفاصل الزمني في دقائق", - "Check for Motion First": "تحقق من الحركة الأولى", - "Close": "قريب", - "Closed": "مغلقة", - "Command": "الأمر", - "Command on Trigger": "القيادة على الزناد", - "Complete Stream URL": "كاملة تيار URL", - "Confirm": "تأكيد", - "Connected": "متصل", - "Connection Type": "نوع الاتصال", - "Control": "التحكم", - "Control Error": "السيطرة على خطأ", - "ControlErrorText1": "لم يتم تمكين التحكم", - "Controllable": "يمكن السيطرة عليها", - "Country of Plates": "البلد من لوحات", - "Counts of Motion": "تهم الحركة", - "Current": "الحالي", - "Currently viewing": "حاليا عرض", - "Custom": "مخصص", - "Custom Base URL": "العرف قاعدة URL ترك فارغا لاستخدام المضيف URL", - "DB Lost.. Retrying..": "قاعدة البيانات المفقودة.. إعادة المحاولة..", - "DESC": "DESC", - "Dashboard": "لوحة القيادة", - "Dashboard Language": "لوحة أجهزة القياس في اللغة", - "Dashcam": "Dashcam", - "Dashcam (Streamer v2)": "Dashcam (غاسل v2)", - "Date Range": "نطاق التاريخ", - "Debug": "التصحيح", - "Default": "الافتراضي", - "Delete": "حذف", - "Delete Filter": "حذف الفلتر", - "Delete Matches": "حذف المباريات", - "Delete Monitor": "حذف رصد", - "Delete Motionless Video": "حذف حراك الفيديو", - "Delete Motionless Videos (Record)": "حذف حراك الفيديو (رقم قياسي)", - "Delete Selected Videos": "حذف الفيديو المختار", - "Delete Video": "حذف الفيديو", - "Delete selected": "حذف المحدد", - "DeleteMonitorText": "هل تريد حذف هذا العرض ؟ لا يمكنك استرداد. ملفات هذه الهوية ستبقى في نظام الملفات. إذا اخترت إعادة رصد مع نفس معرف الفيديو و أحداث سوف تصبح مرئية في لوحة القيادة.", - "DeleteSelectedVideosMsg": "هل تريد حذف هذا الفيديو ؟ لا يمكنك استرداد.", - "DeleteVideoMsg": "هل تريد حذف هذا الفيديو ؟ لا يمكنك استرداد.", - "Deleted": "حذف", - "Detect Objects": "الكشف عن الكائنات انظر أدناه", - "Detector": "كاشف", - "Detector Flags": "كشف أعلام", - "Detector Rate": "للكشف عن معدل (FPS)", - "DetectorText": "

عندما المربعات عرض وارتفاع ترد عليك أن مجموعة منهم إلى 640 × 480 أو أقل. هذا وسوف تحسين سرعة القراءة من الإطارات.

", - "Disable Night Vision": "تعطيل للرؤية الليلية عنوان URL", - "Disable Nightvision": "تعطيل رؤية ليلية", - "Disabled": "تعطيل", - "Documentation": "الوثائق", - "Don't show this anymore": "لا تظهر هذا بعد الآن", - "Double Quote Directory": "اقتباس مزدوجة الدليل بعض الدلائل التي تحتوي على مسافات. باستخدام هذا قد يتلف بعض الكاميرات.", - "Down": "أسفل عنوان URL", - "Down Stop": "أسفل وقف عنوان URL", - "Download": "تحميل", - "EU": "الاتحاد الأوروبي", - "Edit": "تحرير", - "Email": "البريد الإلكتروني", - "Email Details": "البريد الإلكتروني التفاصيل", - "Email on No Motion": "البريد الإلكتروني على \"لا الحركة\"", - "Email on Trigger": "البريد الإلكتروني على الزناد رسائل البريد الإلكتروني الذهاب إلى الحساب الرئيسي حامل عنوان تسجيل الدخول.", - "Enable Night Vision": "تمكين الرؤية الليلية عنوان URL", - "Enable Nightvision": "تمكن رؤية ليلية", - "Enabled": "تمكين", - "End": "نهاية", - "End Time": "نهاية الوقت", - "Ended": "انتهت", - "Enlarge": "تكبير", - "Enter this code to proceed": "أدخل هذا الرمز إلى المضي قدما", - "Equal to": "يساوي", - "Error Connecting": "حدث خطأ أثناء الاتصال", - "Event": "الحدث", - "Event Limit": "الحدث الحد", - "EventText1": "أثار الحدث الحركة في", - "EventText2": "لا يمكن أن البريد الإلكتروني صورة الملف لا يمكن الوصول إليها", - "Events": "الأحداث", - "Example": "على سبيل المثال", - "Execute Command": "تنفيذ الأوامر", - "Executed": "أعدم", - "Export": "التصدير", - "FFmpegCantStart": "FFmpeg لم تبدأ", - "FFmpegCantStartText": "تسجيل محرك هذه الكاميرا لا يمكن أن تبدأ. قد يكون هناك شيء خاطئ مع الكاميرا التكوين. إذا كان هناك أي سجلات أخرى من هذا واحد يرجى نشرها في القضايا على جيثب.", - "FFmpegTip": "FFprobe بسيط تيارات متعددة الوسائط محلل. يمكنك استخدامه لانتاج جميع أنواع المعلومات حول المدخلات بما في ذلك مدة, معدل الإطار, حجم الإطار ، إلخ.", - "FFprobe": "التحقيق", - "FactorAuthText1": "كود سوف يكون نشطا فقط لمدة 15 دقيقة. إذا كنت تسجيل الدخول مرة أخرى الموقت سيتم إعادة تعيين إلى 15 دقيقة مع نفس القانون.", - "Fatal": "قاتلة", - "Fatal Maximum Reached": "قاتلة الأقصى التوصل إلى وقف الكاميرا.", - "FatalMaximumReachedText": "JPEG كان خطأ فادح.", - "Feed-in Image Height": "تغذية في ارتفاع الصورة", - "Feed-in Image Width": "تغذية في صورة العرض", - "Fields cannot be empty": "المجالات لا يمكن أن تكون فارغة", - "File Not Exist": "الملف غير موجود", - "File Not Found": "لم يتم العثور على الملف", - "File Type": "نوع الملف", - "FileNotExistText": "لا يمكن حفظ غير متوفرة. شيء ذهب على نحو خاطئ.", - "Filename": "اسم الملف", - "Filesize": "حجم الملف", - "Filter ID": "تصفية معرف", - "Filter Matches": "تصفية المباريات", - "Filter Name": "اسم عامل التصفية", - "FilterMatchesText1": "هذا الفلتر قد استوفت الشروط.", - "FilterMatchesText2": "أشرطة الفيديو وجدت.", - "Filters": "المرشحات", - "Filters Updated": "مرشحات تحديث", - "FiltersUpdatedText": "التغييرات التي تم حفظها وتطبيقها.", - "Find Where": "تجد فيها", - "Fix": "Fix", - "Fix Video": "إصلاح الفيديو", - "FixVideoMsg": "هل تريد إصلاح هذا الفيديو ؟ لا يمكن التراجع عن هذا الإجراء.", - "Font Path": "مسار الخط", - "Font Size": "حجم الخط", - "Force Port": "القوة ميناء", - "Found Devices": "وجدت الأجهزة", - "Frame Rate": "معدل الإطار (FPS)", - "Full Frame Detection": "الإطار الكامل الكشف", - "Fullscreen": "ملء الشاشة", - "Greater Than": "أكبر من", - "Greater Than or Equal to": "أكبر من أو يساوي", - "Group Key": "المجموعة الرئيسية", - "Group Name": "اسم المجموعة", - "Grouping": "تجمع ", - "H.264 / H.265 / H.265+": "H. 264 / H. 265 / H. 265 ", - "HLS (.m3u8)": "HLS (.m3u8)", - "HLS (includes Audio)": "HLS (ويشمل الصوت)", - "HLS Audio Encoder": "HLS ترميز الصوت", - "HLS List Size": "HLS قائمة الحجم", - "HLS Preset": "HLS مسبقا", - "HLS Segment Length": "HLS قطعة طول في ثواني", - "HLS Video Encoder": "HLS ترميز الفيديو", - "HTTP": "HTTP", - "HTTPS": "HTTPS", - "Height": "ارتفاع", - "Help": "مساعدة", - "Hide List": "إخفاء قائمة", - "Hide Notes": "إخفاء الملاحظات", - "Host": "المضيف", - "Hotswap Modes (Watch-Only)": "Hotswap وسائط (مشاهدة فقط)", - "How to Record": "كيفية تسجيل", - "IP Address": "عنوان IP", - "Identity": "الهوية", - "IdentityText1": "هذا هو كيف نظام تحديد البيانات عن هذا التيار. لا يمكنك تغيير الشاشة معرف بمجرد الضغط على حفظ. إذا كنت تريد أن تجعل الشاشة معرف المزيد الإنسان للقراءة قبل المتابعة.", - "IdentityText2": "يمكنك تكرار رصد بتعديل رصد الهوية ثم الضغط على حفظ. هل يمكن استخدام معرف جهاز موجود بالفعل أو أنه سيوفر أكثر من أن الشاشة معلومات قاعدة البيانات.", - "Idle": "الخمول", - "Image Height": "ارتفاع الصورة", - "Image Location": "صورة موقع المسار المطلق أو ترك فارغا لاستخدام العالمية", - "Image Position": "صورة الموقف", - "Image Width": "صورة العرض", - "Import": "استيراد", - "Import Monitor Configuration": "استيراد رصد التكوين", - "ImportMonitorConfigurationText": "عند القيام بذلك سوف overrwrite أي تغييرات في الوقت الراهن لم يتم حفظها. التغييرات المستوردة تطبق فقط عند الضغط على حفظ.", - "In": "في", - "Incorrect Settings Chosen": "إعدادات غير صحيحة المختار", - "Indifference": "اللامبالاة", - "Input": "المدخلات", - "Input Flags": "إدخال الأعلام", - "Input Type": "نوع الإدخال", - "InputText1": "هذا المقطع يحكي شينوبي كيفية تستهلك تيار. للحصول على الأداء الأمثل محاولة ضبط الكاميرا الإعدادات الداخلية. العثور على الخيارات التالية ومجموعة منهم كما هو مبين. العثور على الكاميرا الخاصة بك يمكنك استخدام بنيت في ONVIF الماسح الضوئي من شينوبي. بعض كاميرات ONVIF تتطلب استخدام أداة إدارة إلى تعديل الإعدادات الداخلية. إذا كنت لا يمكن العثور على الكاميرات الخاصة بك يمكنك محاولة ONVIF مدير جهاز ويندوز.", - "InputText2": "", - "InputText3": "إذا كنت بحاجة إلى مساعدة في معرفة ما هو إدخال نوع الكاميرا الخاصة بك يمكنك أن تأخذ نظرة في الكاميرا عناوين قائمة على شينوبي الموقع.", - "Invalid JSON": "صالح سلمان", - "InvalidJSONText": "يرجى التأكد من هذا هو صالح سلمان سلسلة شينوبي رصد التكوين.", - "JPEG": "JPEG", - "JPEG (Auto Enables JPEG API)": "JPEG (السيارات تمكن JPEG API)", - "JPEG API": "JPEG API لقطة (cgi-bin)", - "JPEG Error": "JPEG خطأ", - "JPEG Mode": "وضع JPEG", - "JPEGErrorText": "كان هناك مشكلة في الحصول على البيانات من الكاميرا الخاصة بك.", - "Leave blank for random.": "ترك فارغا عشوائية.", - "Left": "ترك عنوان URL", - "Left Stop": "غادر وقف عنوان URL", - "Less Than": "أقل من", - "Less Than or Equal to": "أقل من أو يساوي", - "Like": "مثل", - "Lisence Plate Detector": "رخصة لوحة كاشف", - "List Toggle": "قائمة تبديل", - "Live Stream Toggle": "بث مباشر تبديل", - "Live View": "عرض لايف", - "Local": "المحلية", - "Log Level": "سجل مستوى", - "Log Signal Event": "سجل إشارة الحدث من جانب العميل فقط", - "Logging": "تسجيل", - "Login": "تسجيل الدخول", - "Logout": "خروج", - "Logs": "سجلات", - "MB": "MB", - "MJPEG": "MJPEG", - "MP4 (copy, libx264, libx265)": "MP4 (نسخ ، libx264, libx265)", - "MPEG-4 (.mp4 / .ts)": "MPEG-4 (.mp4 / .ts)", - "MailError": "البريد خطأ : لا يمكن إرسال البريد الإلكتروني التحقق من conf.json. تخطي أي ملامح الاعتماد على البريدية.", - "Matches": "مباريات", - "Max Storage Amount": "ماكس تخزين كمية في ميغا بايت", - "Mode": "وضع", - "Monitor": "رصد", - "Monitor Added by user": "رصد المضافة من قبل المستخدم.", - "Monitor Capture Rate": "رصد معدل التقاط (FPS)", - "Monitor Groups": "رصد مجموعات", - "Monitor ID": "رصد ID", - "Monitor Idling": "رصد تسكع", - "Monitor Name": "رصد اسم", - "Monitor Settings": "إعدادات الشاشة", - "Monitor Stopped": "رصد توقف", - "Monitor Updated by user": "مراقبة تحديث من قبل المستخدم.", - "Monitor mode changed": "مراقبة الوضع تغير", - "Monitor mode is already": "رصد وضع بالفعل", - "Monitor or Key does not exist.": "مراقبة أو عدم وجود مفتاح.", - "MonitorIdlingText": "مراقبة الدورة وقد أمر إلى الخمول.", - "MonitorStoppedText": "مراقبة الدورة وقد أمر التوقف.", - "Monitors": "شاشات", - "Monitors per row": "شاشات لكل صف على المونتاج", - "Montage": "المونتاج", - "Motion GUI": "الحركة واجهة المستخدم الرسومية", - "Motion Meter": "الحركة متر", - "Name": "اسم", - "No": "لا", - "No Audio": "لا الصوت", - "No Data": "لا توجد بيانات", - "No Events found for this video": "لا الأحداث وجدت هذا الفيديو", - "No Group with this key exists": "أي جماعة مع هذا المفتاح موجود", - "No Monitor Found, Ignoring Request": "لا مراقبة وجدت تجاهل الطلب", - "No Rotation": "لا دوران", - "No such file": "لا يوجد مثل هذا الملف", - "NoMotionEmailText1": "لا الحركة", - "NoMotionEmailText2": "لم يكن هناك أي حركة الكشف على الكاميرا", - "NoVideosFoundForDateRange": "لا أشرطة الفيديو الموجودة في هذا النطاق الزمني. محاولة تحديد بداية التاريخ إلى الوراء.", - "Not Authorized": "لا يؤذن", - "Not Connected": "غير متصل", - "Not Equal to": "لا يساوي", - "Not In": "لا في", - "Not Matches": "لا يطابق", - "Not Permitted": "لا يسمح", - "Not an Administrator Account": "لا حساب مسؤول", - "NotAuthorizedText1": "لا يحق تقديم init الأمر مع \"مصادقة\",\"كه\", \"uid\"", - "Notes": "ملاحظات", - "NotesPlacholder": "التعليقات كنت تريد أن تترك هذه الكاميرات الإعدادات.", - "Number of Days to keep": "عدد أيام الاحتفاظ", - "ONVIF": "ONVIF", - "ONVIF Scanner": "ONVIF الماسح الضوئي", - "ONVIFnote": "اكتشاف ONVIF الأجهزة على الشبكات خارج الخاص بك أو اتركه فارغا لمسح شبكة الاتصال الحالية الخاصة بك.
اسم المستخدم وكلمة المرور يمكن أن تترك فارغة.", - "OpenCV Cascades": "بنسف شلالات", - "Order Streams": "ترتيب الجداول", - "Output Method": "أسلوب الإخراج", - "Password": "كلمة المرور", - "Password Again": "كلمة المرور مرة أخرى", - "Passwords don't match": "كلمات السر لا تتطابق", - "Paste JSON here.": "لصق JSON هنا.", - "Path": "المسار", - "Permissions": "أذونات", - "Points": "نقاط عند إضافة نقاط انقر على حافة المضلع.", - "Port": "ميناء", - "Position X": "موقف X", - "Position Y": "موقف Y", - "Power Video Viewer": "فيديو المشاهد", - "Power Viewer": "السلطة المشاهد", - "Preferences": "تفضيلات", - "Preset": "مسبقا", - "Probe Size": "التحقيق الحجم", - "Process Crashed for Monitor": "عملية تحطمت على رصد", - "Process Unexpected Exit": "عملية خروج غير متوقع", - "Profile": "الملف الشخصي", - "Quality": "جودة 1 عالية ، 23 منخفضة", - "Query": "الاستعلام", - "RAM": "ذاكرة الوصول العشوائي", - "RTSP": "RTSP", - "RTSP Transport": "RTSP النقل", - "Range or Single": "مجموعة أو واحد", - "Rate": "معدل (FPS)", - "Record": "سجل", - "Record File Type": "تسجيل نوع الملف", - "Record Height": "سجل ارتفاع", - "Record Video Filter": "تسجيل الفيديو فلتر", - "Record Width": "سجل العرض", - "Recording": "تسجيل", - "Recording Flags": "تسجيل الأعلام", - "Recording Segment Interval": "تسجيل الجزء الفاصل الزمني في دقائق", - "Recording Timeout": "تسجيل مهلة في دقائق", - "Recording Timestamp": "تسجيل الزمني", - "Recording Watermark": "تسجيل العلامة المائية", - "RecordingText": "فمن المستحسن أن تقوم بتعيين تسجيل نوع الملف إلى WebMMP4 و ترميز الفيديو إلى libvpxنسخ أو libx264 لأن إدخال نوع هو .", - "Refresh List of Cascades": "تحديث قائمة شلالات", - "Region Editor": "المنطقة محرر", - "Region Name": "اسم المنطقة", - "RegionNote": "النقاط يتم حفظها فقط عند الضغط حفظ على إعدادات الشاشة نافذة.", - "Regions": "المناطق", - "Remember Me": "تذكر لي", - "Reset Timer": "إعادة تعيين جهاز ضبط الوقت", - "Restarting Process": "إعادة تشغيل العملية", - "Retry Connection": "إعادة المحاولة اتصال عدد المرات التي يسمح تفشل", - "Retrying...": "إعادة المحاولة...", - "Right": "صحيح عنوان URL", - "Right Stop": "صحيح وقف عنوان URL", - "Rotate": "تدوير", - "Save": "حفظ", - "Save Directory": "حفظ الدليل", - "Save Events to SQL": "حفظ الأحداث إلى SQL", - "Save Log in SQL": "حفظ سجل في SQL وهذا يمكن أن تملأ بسرعة.", - "Save as": "حفظ باسم", - "Saved Filters": "حفظ الفلاتر", - "Scan Settings": "إعدادات المسح الضوئي", - "Search": "البحث", - "Send Frames": "إرسال إطارات دفع الإطارات ليتم تحليلها", - "Separate with commas, no spaces": "منفصلة بفواصل, بدون مسافات", - "Set to Watch Only": "تعيين مشاهدة فقط", - "Settings": "الإعدادات", - "Settings Changed": "تغيير الإعدادات", - "SettingsChangedText": "إعدادات تم حفظها وتطبيقها.", - "Shinobi": "شينوبي", - "Shinobi Streamer": "شينوبي غاسل", - "Show Logs": "تظهر سجلات", - "Silent": "الصمت", - "Simple": "بسيطة", - "Size (mb)": "الحجم (ميجا بايت)", - "Snapshot": "لقطة", - "Snapshot Flags": "لقطة الأعلام", - "Snapshots": "لقطات", - "Sort By": "فرز حسب", - "Start": "تبدأ", - "Start Recording": "بدء التسجيل", - "Start Time": "وقت البدء", - "Started": "بدأت", - "Status Indicator": "مؤشر حالة", - "Stop URL": "وقف URL", - "Stream": "تيار", - "Stream Flags": "تيار الأعلام", - "Stream Timestamp": "تيار الزمني", - "Stream Type": "تيار من نوع", - "Stream Watermark": "تيار مائية", - "Stream to YouTube": "تيار يوتيوب", - "Stream to YouTube Flags": "تيار يوتيوب الأعلام", - "StreamText": "

هذا القسم سوف تعين الابتدائي تيار الأسلوب وإعدادات. هذا التيار سوف يتم عرضها في لوحة القيادة. إذا اخترت استخدام HLS, JPEG, أو MJPEG ثم يمكنك تستهلك تيار من خلال البرامج الأخرى.

استخدام JPEG تيار أساسا إيقاف البث الأساسية يستخدم اللقطة بن للحصول على إطارات.

", - "Streamer": "غاسل", - "Streams": "تيارات", - "Superuser": "الخارق", - "Switch on for Still Image": "التبديل على صورة ثابتة", - "TCP": "TCP", - "Text Box Color": "النص مربع اللون", - "Text Color": "لون النص", - "Time-lapse": "الوقت الفاصل بين", - "Time-lapse Tool": "الوقت الفاصل بين أداة", - "Timeout": "مهلة", - "Timeout Reset on Next Motion": "مهلة إعادة تعيين على الحركة القادمة", - "Toggle Sidebar": "تبديل الشريط الجانبي", - "Top Left": "أعلى اليسار", - "Top Right": "أعلى اليمين", - "Trigger Record": "الزناد سجل", - "Trigger Successful": "الزناد ناجحة", - "UDP": "UDP", - "URL": "URL", - "URL Stop Timeout": "URL وقف مهلة تشغيل إيقاف URL بعد X ثانية", - "US": "لنا", - "Unable to Launch": "قادر على إطلاق", - "UnabletoLaunchText": "الرجاء حفظ رصد جديدة أولا. ثم محاولة إطلاق محرر المنطقة.", - "Up": "حتى عنوان URL", - "Up Stop": "حتى وقف عنوان URL", - "Username": "اسم المستخدم", - "Value": "القيمة", - "Video": "فيديو", - "Video Codec": "ترميز الفيديو", - "Video Filter": "فيديو مرشح", - "Video Finished": "الفيديو النهائي", - "Video Length (minutes) and Motion Count per video": "طول الفيديو (دقيقة) و الحركة الاعتماد في الفيديو", - "Video Record Rate": "الفيديو سجل معدل (FPS)", - "Video Status": "فيديو حالة", - "Video and Time Span (Minutes)": "فيديو من الزمن (دقيقة)", - "Videos": "الفيديو", - "Videos List": "قائمة أشرطة الفيديو", - "Watch": "مشاهدة", - "Watch Only": "مشاهدة فقط", - "WebDAV": "WebDAV", - "WebM (libvpx)": "WebM (libvpx)", - "Webdav Error": "خطأ Webdav", - "WebdavErrorText": "لا يمكن حفظ. هل جعل الكاميرا المجلدات داخل اخترتها حفظ الدليل ؟ ", - "Webhook": "Webhook", - "Webhook URL": "Webhook URL", - "Width": "العرض", - "Yes": "نعم", - "Zoom In": "التكبير في عنوان URL", - "Zoom In Stop": "التكبير في التوقف عن عنوان URL", - "Zoom Out": "تصغير عنوان URL", - "Zoom Out Stop": "تصغير وقف عنوان URL", - "a day": "اليوم", - "a few seconds": "بضع ثوان", - "a minute": "دقيقة", - "a month": "شهر", - "a year": "في السنة", - "aac": "الجميح للسيارات", - "aac (Default)": "الجميح للسيارات (افتراضي)", - "ac3": "ac3", - "ago": "قبل", - "an hour": "ساعة", - "blankPassword": "تترك فارغة إلى الحفاظ على نفس كلمة المرور", - "calendar": "التقويم", - "clientStreamFailedattemptingReconnect": "العميل ctream تحقق فشلت محاولة إعادة الاتصال.", - "confirmDeleteFilter": "هل تريد حذف هذا الفلتر ؟ لا يمكنك استرداد.", - "copy": "نسخ", - "days": "أيام", - "dropBoxSuccess": "النجاح! الملفات المحفوظة إلى دروببوإكس الخاص بك.", - "for Global Access": "من أجل الوصول العالمي", - "hours": "ساعات", - "in": "في", - "libmp3lame": "libmp3lame", - "libopus": "libopus", - "libvorbis (Default)": "libvorbis (الافتراضي)", - "libvpx (Default)": "libvpx (الافتراضي)", - "libvpx-vp9": "libvpx-vp9", - "libx264": "libx264", - "libx264 (Default)": "libx264 (الافتراضي)", - "libx265": "libx265", - "minutes": "دقائق", - "modifyVideoText1": "طريقة لا وجود لها. تحقق للتأكد من أن آخر قيمة URL ليست فارغة.", - "monitorEditFailedMaxReached": "حسابك قد بلغ أقصى عدد من الكاميرات التي يمكن إنشاؤها. التحدث إلى مسؤول إذا كنت ترغب في تغيير هذا.", - "monitorEditText1": "بيانات غير صالحة ، تحقق لمعرفة هذا هو صالح استيراد السلسلة.", - "monitorEditText2": "غير صالحة تفاصيل السلسلة. تحقق لمعرفة ما هو سلسلة JSON و غير منتظم وجوه يتم تمريرها.", - "monitorGetText1": "الطلب غير المكتمل ، وإزالة الماضي مائل في URL أو وضع مقبول القيمة.", - "months": "أشهر", - "noSpecialCharacters": "بدون مسافات أو أحرف خاصة.", - "on": "على", - "on Error": "على خطأ", - "startUpText0": "حجم التحقق من أشرطة الفيديو", - "startUpText1": "الغاية من حجم التحقق من أشرطة الفيديو", - "startUpText2": "جميع المستخدمين فحص الانتظار لإغلاق الملفات المفتوحة وإزالة الملفات على حد المستخدم", - "startUpText3": "في انتظار أن تعطي لم تنته الفيديو تحقق بعض الوقت. 3 ثوان.", - "startUpText4": "بدأت كل مجموعة شاشات لمشاهدة وتسجيل", - "startUpText5": "شينوبي جاهز.", - "superAdminText": "\"السوبر.سلمان\" لا وجود لها. الرجاء إعادة تسمية \"سوبر.العينة.سلمان\" إلى \"السوبر.سلمان\".", - "superAdminTitle": "شينوبي : المشرف المميز", - "total": "مجموع", - "undefined": "غير معرف", - "updateKeyText1": "\"updateKey\" مفقود من \"conf.json\", لا يمكن أن تفعل التحديثات هذه الطريقة حتى يمكنك إضافته.", - "updateKeyText2": "\"updateKey\" غير صحيحة.", - "years": "سنوات" -} + "\"No Motion\" Detector": "\"لا الحركة\" كاشف", + "# of Allow MJPEG Clients": "# من السماح لعملاء MJPEG 0 لـ Infinite ", + "'Already Installing...'": "\"تثبيت بالفعل ...\"", + "180 Degrees": "180 درجة", + "2-Factor Authentication": "2-عامل التوثيق", + "90 Clockwise": "90 اتجاه عقارب الساعة", + "90 Clockwise and Vertical Flip": "90 اتجاه عقارب الساعة العمودي الوجه", + "90 Counter Clockwise and Vertical Flip (default)": "90 عكس اتجاه عقارب الساعة و انعكاس عمودي (الافتراضي)", + "AND": "و", + "API": "API", + "API Key": "مفتاح API", + "API Key Action Failed": "فشل إجراء مفتاح API", + "API Key Added": "مفتاح API وأضاف", + "API Key Deleted": "مفتاح API حذف", + "API Keys": "مفاتيح API", + "APIKeyAddedText": "يمكنك استخدام هذا المفتاح الآن.", + "APIKeyDeletedText": "تم حذف مفتاح. وسوف لم يعد يعمل.", + "ASC": "ASC", + "Accelerator": "مسرع", + "Account Info": "معلومات الحساب", + "Account Information": "معلومات الحساب", + "Account Privileges": "امتيازات الحساب", + "Account Save": "حفظ الحساب", + "Account Settings": "إعدادت الحساب", + "AccountEditText1": "لا يمكن تحريرها. تحديث الصفحة إذا كانت المشكلة لا تزال.", + "Accounts": "حسابات", + "Action for Selected": "العمل المحدد", + "Activated": "مفعل", + "Active Monitors": "شاشات نشطة", + "Add": "إضافة", + "Add All": "إضافة الجميع", + "Add Camera": "أضف الكاميرا", + "Add Cameras": "إضافة الكاميرات", + "Add Channel": "إضافة قناة", + "Add Input Feed": "إضافة تغذية الإدخال", + "Add Map": "أضف الخريطة", + "Add Monitor": "إضافة مراقبة", + "Add New": "إضافة جديدة", + "AddToPreset": "أضف إلى الإعداد المسبق", + "Additional Inputs": "مدخلات إضافية", + "Admin": "المشرف", + "Advanced": "المتقدمة", + "After": "بعد", + "Again": "مرة أخرى", + "Age": "سن", + "Alert Sound": "صوت في حالة تأهب", + "Alert Sound Delay": "تنبيه تأخير الصوت", + "All Logs": "جميع السجلات", + "All Monitors": "جميع الشاشات", + "All Monitors and Privileges": "جميع شاشات والامتيازات", + "All Privileges": "جميع الامتيازات", + "All Warnings": "كل التحذيرات", + "All streams in first feed": "جميع التدفقات في التغذية الأولى", + "Allow API Trigger": "السماح API Trigger", + "Allow Next Alert": "السماح التنبيه التالي", + "Allow Next Command": "تسمح الأمر التالي في دقائق", + "Allow Next Discord Alert": "اسمح تنبيه الخلاف التالي في دقائق ", + "Allow Next Email": "تسمح المقبل البريد الإلكتروني في غضون دقائق", + "Allow Next Trigger": "تسمح الزناد التالي في ميلي ثانية", + "Allow Next Webhook": "السماح التالي webhook", + "Allowed IPs": "يسمح IPs", + "Already Processing": "المعالجة بالفعل", + "Already exists": "موجود مسبقا", + "Alternate Logins": "تسجيلات تسجيلات بديلة", + "Always": "دائماً", + "Amazon S3": "Amazon S3", + "Amazon S3 Upload Error": "خطأ في تحميل Amazon S3", + "Analyzation Duration": "Analyzation مدة", + "AppNotEnabledText": "لم يتم تمكين التطبيق ، وتمكينه في إعدادات حسابك.", + "April": "أبريل", + "Archive": "أرشيف", + "Are you sure?": "هل أنت واثق؟", + "Attach Snapshot": "نعلق لقطة", + "Attach Video Clip": "إرفاق مقطع الفيديو", + "Audio": "صوتي", + "Audio Bit Rate": "معدل البت الصوت", + "Audio Codec": "ترميز الصوت", + "Audio Detection": "اكتشاف الصوت", + "Audio Detector": "كاشف الصوت", + "Audio stream only from first feed": "دفق الصوت فقط من الخلاصة الأولى", + "Audio streams only": "تدفقات الصوت فقط", + "August": "أغسطس", + "Authenticate": "المصادقة", + "Authenticated": "مصادقة", + "Authentication Failed": "المصادقة فشلت", + "Auto": "السيارات", + "Automatic": "تلقائي", + "Automatic Checking Cancelled": "فحص تلقائي تم إلغاؤه", + "Automatic Codec Repair": "إصلاح الترميز التلقائي", + "Automatic Field Fill": "ملء المجال التلقائي", + "Autosave": "الحفظ التلقائي", + "Back": "خلف", + "Backblaze B2": "Backblaze B2", + "Backblaze Error": "خطأ backblaze", + "BacklightCompensation": "وتعويض الإضاءة الخلفية", + "Backup": "دعم", + "Bandwidth": "عرض النطاق", + "Base64 over Websocket": "Base64 على Websocket", + "Basic Authentication": "المصادقة الأساسية", + "Batch": "حزمة", + "Before": "قبل", + "Bind Credentials": "ربط بيانات الاعتماد (كلمة المرور)", + "BitrateLimit": "حد البت", + "Blank for No Change": "فارغة لعدم تغيير", + "Bottom Left": "أسفل اليسار", + "Bottom Right": "أسفل اليمين", + "Brightness": "سطوع", + "Browser Console Log": "المتصفح سجل وحدة", + "Bucket": "دلو", + "Buffer Preview": "معاينة المخزن المؤقت", + "Build": "يبني", + "Build Video": "بناء الفيديو", + "Building": "مبنى", + "CPU": "وحدة المعالجة المركزية", + "CPU indicator will not work. Continuing...": "وحدة المعالجة المركزية المؤشر لا يعمل. استمرار...", + "CPU used by this stream": "وحدة المعالجة المركزية المستخدمة من قبل هذا الدفق", + "CSS": "CSS أسلوب لوحة القيادة الخاصة بك.", + "Calendar": "التقويم", + "Call Method": "طريقة الاتصال", + "Camera Password": "كاميرا المرور", + "Camera Username": "الكاميرا المستخدم", + "Camera is not recording": "الكاميرا ليست تسجيل", + "Camera is not running": "الكاميرا لا تعمل", + "Camera is not streaming": "الكاميرا لا تتدفق", + "CameraNotRecordingText": "الإعدادات قد تكون غير متوافقة. التحقق من الترميز. إعادة تشغيل...", + "Can Authenticate Websocket": "يمكن مصادقة WebSocket", + "Can Change User Settings": "يمكن تغيير إعدادات المستخدم", + "Can Control Monitors": "يمكن التحكم في الشاشات", + "Can Create and Delete Monitors": "يمكن إنشاء وحذف الشاشات", + "Can Delete Videos": "يمكن حذف الفيديو", + "Can Delete Videos and Events": "يمكن حذف الفيديو و الأحداث", + "Can Edit Monitor": "يمكن تحرير رصد", + "Can Get Logs": "يمكن الحصول على سجلات", + "Can Get Monitors": "يمكن الحصول على شاشات", + "Can View Logs": "يمكن عرض سجلات", + "Can View Monitor": "يمكن عرض الشاشة", + "Can View Snapshots": "يمكن عرض لقطات", + "Can View Streams": "يمكن عرض الجداول", + "Can View Videos": "يمكن عرض الفيديو", + "Can View Videos and Events": "يمكن عرض أشرطة الفيديو والأحداث", + "Can edit Max Days": "يمكن تحرير أيام كحد أقصى", + "Can edit Max Storage": "يمكن تحرير الحد الأقصى للتخزين", + "Can edit how long to keep Events": "يمكن تعديل كم من الوقت للحفاظ على الأحداث", + "Can edit how long to keep Logs": "يمكن تعديل كم من الوقت للحفاظ على السجلات", + "Can use Admin Panel": "يمكن استخدام لوحة المسؤول", + "Can use Amazon S3": "يمكن استخدام Amazon S3", + "Can use Discord Bot": "يمكن استخدام روبوت الخلاف", + "Can use LDAP": "يمكن استخدام LDAP", + "Can use SFTP": "يمكن استخدام SFTP", + "Can use Wasabi Hot Cloud Storage": "يمكن استخدام التخزين السحابي الساخن Wasabi", + "Can use WebDAV": "يمكن استخدام WebDav", + "Can't Connect": "لا يمكن الاتصال", + "Cannot watch a monitor that isn't running.": "لا يمكن مشاهدة شاشة لا تعمل.", + "Cards": "البطاقات", + "Carousel in Background": "كاروسيل في الخلفية", + "Center": "مركز عنوان URL", + "Channel": "قناة", + "Channel ID": "معرف القناة", + "Chat on Discord": "الدردشة على الفتنة", + "Check": "تحقق", + "Check Signal Interval": "التحقق من إشارة الفاصل الزمني في دقائق", + "Check for Motion First": "تحقق من الحركة الأولى", + "Check the Channel ID": "تحقق من معرف القناة", + "Check the Recipient ID": "تحقق من معرف المستلم", + "Clear": "واضح", + "Clear Recorder Process": "عملية مسجل واضحة", + "Close": "قريب", + "Close All Monitors": "أغلق جميع الشاشات", + "Closed": "مغلقة", + "Cloud": "سحاب", + "Codec Mismatch": "عدم تطابق الترميز", + "Color Threshold": "عتبة اللون", + "ColorSaturation": "تشبع اللون", + "Command": "الأمر", + "Command on Trigger": "القيادة على الزناد", + "Common Objects": "أشياء مشتركة", + "Complete Stream URL": "كاملة تيار URL", + "Conditions": "الظروف", + "Confidence": "الثقة", + "Confidence of Detection": "ثقة الكشف", + "Configuration": "إعدادات", + "Confirm": "تأكيد", + "Connected": "متصل", + "Connected Users": "المستخدمين المتصلون", + "Connection": "اتصال", + "Connection Type": "نوع الاتصال", + "Connection timed out": "انتهت مدة الاتصال", + "Contains": "يتضمن", + "Contrast": "التباين", + "Control": "التحكم", + "Control Error": "السيطرة على خطأ", + "ControlErrorText1": "لم يتم تمكين التحكم", + "ControlErrorText2": "تحقق من تفاصيل الاتصال الخاصة بك. قد تحتاج إلى توجيه عنوان URL الأساسي إلى المنفذ 8000 أو 80. تحقق من معلومات المصادقة الخاصة بك.", + "Controllable": "يمكن السيطرة عليها", + "Controls and Logs": "الضوابط والسجلات", + "Copied": "نسخ", + "Copied to Clipboard": "نسخ إلى الحافظة", + "Copy": "ينسخ", + "Copy Connection Settings": "نسخ إعدادات الاتصال", + "Copy Custom Settings": "نسخ الإعدادات المخصصة", + "Copy Detector Settings": "نسخ إعدادات كاشف", + "Copy Group Settings": "نسخ إعدادات المجموعة", + "Copy Input Settings": "نسخ إعدادات الإدخال", + "Copy JPEG API Settings": "نسخ إعدادات API JPEG", + "Copy Logging Settings": "نسخ إعدادات التسجيل", + "Copy Mode": "وضع النسخ", + "Copy Recording Settings": "نسخ إعدادات التسجيل", + "Copy Remote Link": "نسخ الرابط البعيد", + "Copy Settings": "نسخ الإعدادات", + "Copy Stream Channel Settings": "نسخ إعدادات قناة الدفق", + "Copy Stream Channels": "نسخ قنوات الدفق", + "Copy Stream Settings": "نسخ إعدادات الدفق", + "Copy Stream URL": "نسخ URL دفق", + "Copy Timelapse Settings": "نسخ إعدادات timelapse", + "Copy to Settings": "انسخ إلى الإعدادات", + "Cores": "النوى", + "Could not create Bucket.": "لا يمكن إنشاء دلو.", + "Count Objects": "عدد الكائنات", + "Count Objects only inside Regions": "عد الكائنات فقط داخل المناطق", + "Country of Plates": "البلد من لوحات", + "Counts of Motion": "تهم الحركة", + "Create Sub-Accounts at /admin": "إنشاء حسابات فرعية في /المسؤول", + "Creating New Account": "إنشاء حساب جديد", + "Creation Interval": "فاصل الخلق", + "Current": "الحالي", + "Currently Active": "نشط حاليا", + "Currently viewing": "حاليا عرض", + "Custom": "مخصص", + "Custom Auto Load": "تحميل تلقائي مخصص", + "Custom Base URL": "العرف قاعدة URL ترك فارغا لاستخدام المضيف URL", + "Custom Endpoint": "نقطة نهاية مخصصة", + "DB Lost.. Retrying..": "قاعدة البيانات المفقودة.. إعادة المحاولة..", + "DESC": "DESC", + "DHCP": "DHCP", + "DNS": "DNS", + "Daily Events": "الأحداث اليومية", + "Dashboard": "لوحة القيادة", + "Dashboard Language": "لوحة أجهزة القياس في اللغة", + "Dashcam": "Dashcam", + "Dashcam (Streamer v2)": "Dashcam (غاسل v2)", + "Database": "قاعدة البيانات", + "Database Not Found": "قاعدة البيانات غير موجودة", + "Database row does not exist": "صف قاعدة البيانات غير موجود", + "Date": "تاريخ", + "Date Added": "تم إضافة التاريخ", + "Date Range": "نطاق التاريخ", + "Date Updated": "تاريخ تحديث", + "Date and Time": "التاريخ و الوقت", + "DateTimeType": "إدارة التاريخ", + "DaylightSavings": "التوقيت الصيفي", + "Days": "أيام", + "Debug": "التصحيح", + "December": "ديسمبر", + "Default": "الافتراضي", + "Delay for Snapshot": "تأخير لقطة", + "Delete": "حذف", + "Delete Camera": "حذف الكاميرا", + "Delete Filter": "حذف الفلتر", + "Delete Logs": "حذف السجلات", + "Delete Matches": "حذف المباريات", + "Delete Monitor": "حذف رصد", + "Delete Monitor State?": "حذف حالة الشاشة", + "Delete Monitor States Preset": "حذف الحالات المسبقة للمراقبة", + "Delete Monitors and Files": "حذف الشاشات والملفات", + "Delete Motionless Video": "حذف حراك الفيديو", + "Delete Motionless Videos (Record)": "حذف حراك الفيديو (رقم قياسي)", + "Delete Region": "حذف المنطقة", + "Delete Schedule": "حذف الجدول الزمني", + "Delete Selected Videos": "حذف الفيديو المختار", + "Delete Timelapse Frame": "حذف إطار timelapse", + "Delete Video": "حذف الفيديو", + "Delete selected": "حذف المحدد", + "DeleteMonitorText": "هل تريد حذف هذا العرض ؟ لا يمكنك استرداد. ملفات هذه الهوية ستبقى في نظام الملفات. إذا اخترت إعادة رصد مع نفس معرف الفيديو و أحداث سوف تصبح مرئية في لوحة القيادة.", + "DeleteMonitorsText": "هل تريد حذف هذه الشاشات؟ لا يمكنك استعادتها. يمكنك اختيار الاحتفاظ بالملفات لهذه المعرفات في نظام الملفات. إذا اخترت إعادة إنشاء شاشة باستخدام أحد المعرفات ، فستصبح مقاطع الفيديو والأحداث مرئية في لوحة القيادة.", + "DeleteSelectedVideosMsg": "هل تريد حذف هذا الفيديو ؟ لا يمكنك استرداد.", + "DeleteThisMsg": "هل تريد حذف هذا؟ لا يمكنك استعادته.", + "DeleteVideoMsg": "هل تريد حذف هذا الفيديو ؟ لا يمكنك استرداد.", + "Deleted": "حذف", + "Deleted Schedule Configuration": "تحذف تكوين الجدول الزمني", + "Deleted State Configuration": "تحذف تكوين الحالة", + "Detect Objects": "الكشف عن الكائنات انظر أدناه", + "Detection": "كشف", + "Detection Engine": "محرك الكشف", + "Detection Event": "حدث الكشف", + "Detector": "كاشف", + "Detector Buffer": "الكاشف العازلة", + "Detector Filters": "مرشحات الكاشف", + "Detector Flags": "كشف أعلام", + "Detector Grouping": "تجميع الكاشف أضف مجموعات في إعدادات ", + "Detector Rate": "للكشف عن معدل (FPS)", + "Detector Recording Complete": "تسجيل الكشف الكامل", + "Detector Recording Process Exited Prematurely. Restarting.": "عملية تسجيل الكاشف خرجت قبل الأوان. إعادة التشغيل.", + "DetectorText": "

عندما المربعات عرض وارتفاع ترد عليك أن مجموعة منهم إلى 640 × 480 أو أقل. هذا وسوف تحسين سرعة القراءة من الإطارات.

", + "Died": "مات", + "Digest Authentication": "ملخص صحة البيانات", + "Disable": "إبطال", + "Disable Night Vision": "تعطيل للرؤية الليلية عنوان URL", + "Disable Nightvision": "تعطيل رؤية ليلية", + "Disabled": "تعطيل", + "Discord": "خلاف", + "Discord Alert on Trigger": "تنبيه الخلاف على الزناد", + "Discord Bot": "بوت الخلاف", + "Discord on No Motion": "خلاف على \"لا حركة\"", + "DiscordErrorText": "تسبب الإرسال إلى Discord في خطأ", + "DiscordFailedText": "فشل إرسال إلى Discord", + "DiscordLoggedIn": "Discord Bot مصادقة", + "DiscordNotEnabledText": "لم يتم تمكين Bot Discord ، وتمكينه في إعدادات حسابك.", + "Documentation": "الوثائق", + "Does Not Contain": "لا يحتوي", + "Don't Show for 1 Week": "لا تظهر لمدة أسبوع واحد", + "Don't show this anymore": "لا تظهر هذا بعد الآن", + "DontAddToPreset": "لا تضيف إلى الإعداد المسبق", + "Double Quote Directory": "اقتباس مزدوجة الدليل بعض الدلائل التي تحتوي على مسافات. باستخدام هذا قد يتلف بعض الكاميرات.", + "Down": "أسفل عنوان URL", + "Down Stop": "أسفل وقف عنوان URL", + "Download": "تحميل", + "Download Bandwidth": "تنزيل النطاق الترددي", + "Downloading Videos": "تنزيل مقاطع الفيديو", + "Downloading...": "جارى التحميل...", + "Duplicate": "ينسخ", + "EU": "الاتحاد الأوروبي", + "Easy Remote Access (P2P)": "الوصول عن بُعد سهل (P2P)", + "Edit": "تحرير", + "Edit Configuration": "تحرير التكوين", + "Edit Selected": "تحرير محدد", + "Edited Schedule Configuration": "تحرير التكوين الجدول الزمني", + "Edited State Configuration": "تحرير تكوين الحالة", + "Email": "البريد الإلكتروني", + "Email Details": "البريد الإلكتروني التفاصيل", + "Email address is in use.": "عنوان البريد الإلكتروني قيد الاستخدام.", + "Email and Password fields cannot be empty": "لا يمكن أن تكون حقول البريد الإلكتروني وكلمة المرور فارغة", + "Email on No Motion": "البريد الإلكتروني على \"لا الحركة\"", + "Email on Trigger": "البريد الإلكتروني على الزناد رسائل البريد الإلكتروني الذهاب إلى الحساب الرئيسي حامل عنوان تسجيل الدخول.", + "Emotion": "المشاعر", + "Emotion Average": "متوسط العاطفة", + "Enable": "ممكن", + "Enable Night Vision": "تمكين الرؤية الليلية عنوان URL", + "Enable Nightvision": "تمكن رؤية ليلية", + "Enabled": "تمكين", + "Encoding": "التشفير", + "EncodingInterval": "I-Frame", + "End": "نهاية", + "End Time": "نهاية الوقت", + "Ended": "انتهت", + "Endpoint": "نقطة النهاية", + "Endpoint Address": "عنوان نقطة النهاية", + "Enlarge": "تكبير", + "Enter at least one IP": "أدخل IP واحد على الأقل", + "Enter this code to proceed": "أدخل هذا الرمز إلى المضي قدما", + "Equal to": "يساوي", + "Error Connecting": "حدث خطأ أثناء الاتصال", + "Error While Decoding": "خطأ أثناء فك التشفير", + "ErrorWhileDecodingText": "قد يكون لجهازك اتصال غير مستقر بالشبكة. تحقق من اتصالات الشبكة الخاصة بك.", + "ErrorWhileDecodingTextAudio": "تقدم الكاميرا الخاصة بك بيانات مكسورة. حاول تعطيل الصوت في الإعدادات الداخلية للكاميرا.", + "Event": "الحدث", + "Event Counts": "تعداد الأحداث", + "Event Filter Error": "خطأ مرشح الحدث", + "Event Filters": "مرشحات الأحداث", + "Event Limit": "الحدث الحد", + "Event Occurred": "حدث الحدث", + "Event Rules": "قواعد الحدث", + "Event Webhook Error": "حدث خطأ Webhook", + "EventText1": "أثار الحدث الحركة في", + "EventText2": "لا يمكن أن البريد الإلكتروني صورة الملف لا يمكن الوصول إليها", + "Events": "الأحداث", + "Events Found": "وجدت الأحداث", + "Example": "على سبيل المثال", + "Execute Command": "تنفيذ الأوامر", + "Executed": "أعدم", + "Export": "التصدير", + "Export Selected Videos": "تصدير مقاطع الفيديو المحددة", + "Export Video": "تصدير الفيديو", + "ExportSelectedVideosMsg": "هل تريد تصدير مقاطع الفيديو هذه؟ قد يستغرق الأمر بعض الوقت للضغط والتنزيل.", + "Exposure": "التعرض", + "FFmpegCantStart": "FFmpeg لم تبدأ", + "FFmpegCantStartText": "تسجيل محرك هذه الكاميرا لا يمكن أن تبدأ. قد يكون هناك شيء خاطئ مع الكاميرا التكوين. إذا كان هناك أي سجلات أخرى من هذا واحد يرجى نشرها في القضايا على جيثب.", + "FFmpegTip": "FFprobe بسيط تيارات متعددة الوسائط محلل. يمكنك استخدامه لانتاج جميع أنواع المعلومات حول المدخلات بما في ذلك مدة, معدل الإطار, حجم الإطار ، إلخ.", + "FFprobe": "التحقيق", + "FLV": "FLV", + "FLV Stream Type": "نوع دفق FLV", + "FactorAuthText1": "كود سوف يكون نشطا فقط لمدة 15 دقيقة. إذا كنت تسجيل الدخول مرة أخرى الموقت سيتم إعادة تعيين إلى 15 دقيقة مع نفس القانون.", + "Fatal": "قاتلة", + "Fatal Maximum Reached": "قاتلة الأقصى التوصل إلى وقف الكاميرا.", + "FatalMaximumReachedText": "JPEG كان خطأ فادح.", + "February": "شهر فبراير", + "Feed-in Image Height": "تغذية في ارتفاع الصورة", + "Feed-in Image Width": "تغذية في صورة العرض", + "Female": "أنثى", + "Field Missing Value": "الحقل القيمة المفقودة", + "Fields cannot be empty": "المجالات لا يمكن أن تكون فارغة", + "File Delete Error": "خطأ حذف خطأ", + "File Not Exist": "الملف غير موجود", + "File Not Found": "لم يتم العثور على الملف", + "File Not Found in Database": "الملف غير موجود في قاعدة البيانات", + "File Not Found in Filesystem": "الملف غير موجود في نظام الملفات", + "File Type": "نوع الملف", + "FileBin Share": "مشاركة FileBin", + "FileNotExistText": "لا يمكن حفظ غير متوفرة. شيء ذهب على نحو خاطئ.", + "Filename": "اسم الملف", + "Filesize": "حجم الملف", + "Filter ID": "تصفية معرف", + "Filter Matches": "تصفية المباريات", + "Filter Name": "اسم عامل التصفية", + "Filter for Objects only": "تصفية للكائنات فقط", + "FilterMatchesText1": "هذا الفلتر قد استوفت الشروط.", + "FilterMatchesText2": "أشرطة الفيديو وجدت.", + "Filters": "المرشحات", + "Filters Updated": "مرشحات تحديث", + "FiltersUpdatedText": "التغييرات التي تم حفظها وتطبيقها.", + "Find Where": "تجد فيها", + "First stream in feed": "الدفق الأول في التغذية", + "Fix": "يصلح", + "Fix Video": "إصلاح الفيديو", + "FixVideoMsg": "هل تريد إصلاح هذا الفيديو ؟ لا يمكن التراجع عن هذا الإجراء.", + "Flush PM2 Logs": "سجلات PM2 Flush", + "Font Path": "مسار الخط", + "Font Size": "حجم الخط", + "For Group": "للمجموعة", + "Force Monitors Per Row": "شاشات القوة لكل صف", + "Force Port": "القوة ميناء", + "Form Data Not Found": "لم يتم العثور على بيانات النموذج", + "Found Devices": "وجدت الأجهزة", + "Frame Rate": "معدل الإطار (FPS)", + "FrameRateLimit": "حد معدل الإطارات (FPS)", + "Frames": "إطارات", + "Friday": "جمعة", + "Frigate": "فرقاطة", + "Full Frame Detection": "الإطار الكامل الكشف", + "Full Stream URL": "URL دفق كامل", + "Full URL Path": "مسار URL الكامل", + "Fullscreen": "ملء الشاشة", + "Gateway": "بوابة", + "Gender": "جنس", + "Generate Subtitles": "توليد ترجمات", + "Get Code": "الحصول على رمز", + "Get Logs to Client": "احصل على سجلات إلى العميل", + "Global Detector Settings": "إعدادات الكاشف العالمية", + "Google Drive": "محرك Google", + "GovLength": "الحكومة", + "Greater Than": "أكبر من", + "Greater Than or Equal to": "أكبر من أو يساوي", + "Grid": "جريد", + "Group Key": "المجموعة الرئيسية", + "Group Key is in use.": "مفتاح المجموعة قيد الاستخدام.", + "Group Name": "اسم المجموعة", + "Grouping": "تجمع ", + "H.264 / H.265 / H.265+": "H. 264 / H. 265 / H. 265 ", + "H264Profile": "H264 profile", + "HEVC (H.265)": "HEVC (H.265)", + "HLS (.m3u8)": "HLS (.M3U8)", + "HLS (includes Audio)": "HLS (ويشمل الصوت)", + "HLS Audio Encoder": "HLS ترميز الصوت", + "HLS List Size": "HLS قائمة الحجم", + "HLS Live Start Index": "HLS Live Start Index", + "HLS Preset": "HLS مسبقا", + "HLS Segment Length": "HLS قطعة طول في ثواني", + "HLS Start Number": "HLS رقم بدء", + "HLS Video Encoder": "HLS ترميز الفيديو", + "HTTP": "http", + "HTTPS": "https", + "Hardware Accelerated": "تسارع الأجهزة", + "Height": "ارتفاع", + "Help": "مساعدة", + "Hide List": "إخفاء قائمة", + "Hide Notes": "إخفاء الملاحظات", + "Home": "مسكن", + "Host": "المضيف", + "Host Type": "نوع المضيف", + "Hostname": "اسم المضيف", + "Hotswap Modes (Watch-Only)": "Hotswap وسائط (مشاهدة فقط)", + "How to Record": "كيفية تسجيل", + "IP Address": "عنوان IP", + "Identity": "الهوية", + "IdentityText1": "هذا هو كيف نظام تحديد البيانات عن هذا التيار. لا يمكنك تغيير الشاشة معرف بمجرد الضغط على حفظ. إذا كنت تريد أن تجعل الشاشة معرف المزيد الإنسان للقراءة قبل المتابعة.", + "IdentityText2": "يمكنك تكرار رصد بتعديل رصد الهوية ثم الضغط على حفظ. هل يمكن استخدام معرف جهاز موجود بالفعل أو أنه سيوفر أكثر من أن الشاشة معلومات قاعدة البيانات.", + "Idle": "الخمول", + "Image Height": "ارتفاع الصورة", + "Image Location": "صورة موقع المسار المطلق أو ترك فارغا لاستخدام العالمية", + "Image Position": "صورة الموقف", + "Image Width": "صورة العرض", + "Imaging": "التصوير", + "Import": "استيراد", + "Import Monitor Configuration": "استيراد رصد التكوين", + "ImportMonitorConfigurationText": "عند القيام بذلك سوف overrwrite أي تغييرات في الوقت الراهن لم يتم حفظها. التغييرات المستوردة تطبق فقط عند الضغط على حفظ.", + "ImportMultiMonitorConfigurationText": "سيؤدي القيام بذلك إلى تجاوز أي شاشات باستخدام IDS الموجودة في ملف الاستيراد.", + "In": "في", + "Incorrect Settings Chosen": "إعدادات غير صحيحة المختار", + "Indifference": "اللامبالاة", + "Info": "معلومات", + "Information": "معلومة", + "Input": "المدخلات", + "Input Feed": "تغذية المدخلات", + "Input Feeds Selected": "تم اختيار تغذية الإدخال", + "Input Flags": "إدخال الأعلام", + "Input Map": "خريطة الإدخال", + "Input Selector": "محدد الإدخال", + "Input Settings": "إعدادات الإدخال", + "Input Type": "نوع الإدخال", + "InputText1": "هذا المقطع يحكي شينوبي كيفية تستهلك تيار. للحصول على الأداء الأمثل محاولة ضبط الكاميرا الإعدادات الداخلية. العثور على الخيارات التالية ومجموعة منهم كما هو مبين. العثور على الكاميرا الخاصة بك يمكنك استخدام بنيت في ONVIF الماسح الضوئي من شينوبي. بعض كاميرات ONVIF تتطلب استخدام أداة إدارة إلى تعديل الإعدادات الداخلية. إذا كنت لا يمكن العثور على الكاميرات الخاصة بك يمكنك محاولة ONVIF مدير جهاز ويندوز.", + "InputText2": "", + "InputText3": "إذا كنت بحاجة إلى مساعدة في معرفة ما هو إدخال نوع الكاميرا الخاصة بك يمكنك أن تأخذ نظرة في الكاميرا عناوين قائمة على شينوبي الموقع.", + "Inserted Schedule Configuration": "إدراج تكوين الجدول الزمني", + "Inserted State Configuration": "إدراج تكوين الحالة", + "Install": "ثَبَّتَ", + "Interface": "واجهه المستخدم", + "Invalid Data": "بيانات غير صالحة", + "Invalid JSON": "صالح سلمان", + "Invalid Settings": "إعدادات غير صالحة", + "InvalidJSONText": "يرجى التأكد من هذا هو صالح سلمان سلسلة شينوبي رصد التكوين.", + "Inverse Trigger": "الزناد العكسي", + "Invert Y-Axis": "عكس المحور الصادي", + "IrCutFilter": "رؤية ليلية", + "JPEG": "JPEG", + "JPEG (Auto Enables JPEG API)": "JPEG (السيارات تمكن JPEG API)", + "JPEG API": "JPEG API لقطة (cgi-bin)", + "JPEG Error": "JPEG خطأ", + "JPEG Mode": "وضع JPEG", + "JPEGErrorText": "كان هناك مشكلة في الحصول على البيانات من الكاميرا الخاصة بك.", + "January": "يناير", + "July": "يوليو", + "June": "يونيه", + "LDAP": "LDAP", + "LDAP Success": "نجاح LDAP", + "LDAP User Authenticated": "مستخدم LDAP مصادقة", + "LDAP User is New": "مستخدم LDAP جديد", + "Landing Page": "الصفحة المقصودة", + "Last": "آخر", + "Last Modified": "آخر تعديل", + "Launch in New Window": "إطلاق في نافذة جديدة", + "Leave blank for random.": "ترك فارغا عشوائية.", + "Leave blank for unlimited": "اترك فارغًا لغير محدود", + "Left": "ترك عنوان URL", + "Left Stop": "غادر وقف عنوان URL", + "Legacy Webhook": "Legacy Webhook", + "Less Than": "أقل من", + "Less Than or Equal to": "أقل من أو يساوي", + "License Activated": "ترخيص تنشيط", + "License Activation": "تنشيط الترخيص", + "License Activation Failed": "فشل تنشيط الترخيص", + "License Key": "مفتاح الترخيص", + "License Plate Detector": "كاشف لوحة الترخيص", + "Like": "مثل", + "Limited": "محدود", + "Link Google Account": "ربط حساب Google", + "Link LDAP Account": "رابط حساب LDAP", + "Link Shinobi": "رابط شينوبي", + "Lisence Plate Detector": "رخصة لوحة كاشف", + "List Toggle": "قائمة تبديل", + "List of Videos Delete Error": "قائمة مقاطع الفيديو حذف الخطأ", + "Live Grid": "شبكة حية", + "Live Stream Toggle": "بث مباشر تبديل", + "Live View": "عرض لايف", + "Local": "المحلية", + "Log Level": "سجل مستوى", + "Log Signal Event": "سجل إشارة الحدث من جانب العميل فقط", + "Log Stream": "دفق السجل", + "Logging": "تسجيل", + "Login": "تسجيل الدخول", + "Logout": "خروج", + "Logs": "سجلات", + "Loop Stream": "تدفق حلقة", + "MB": "ميغابايت", + "MJPEG": "MJPEG", + "MP4 (copy, libx264, libx265)": "MP4 (نسخ ، libx264, libx265)", + "MPEG-4 (.mp4 / .ts)": "MPEG-4 (.mp4 / .ts)", + "MPEG-DASH (includes Audio)": "mpeg-dash (يشمل الصوت)", + "MQTT Client": "عميل MQTT", + "MQTT Error": "خطأ MQTT", + "MQTT Inbound": "MQTT داخلي", + "MQTT Outbound": "MQTT خارج", + "MailError": "البريد خطأ : لا يمكن إرسال البريد الإلكتروني التحقق من conf.json. تخطي أي ملامح الاعتماد على البريدية.", + "Main": "الأساسية", + "Male": "ذكر", + "Manual": "كتيب", + "Map": "خريطة", + "March": "يمشي", + "Matches": "مباريات", + "Matrices": "المصفوفات", + "Max Indifference": "كحد أقصى اللامبالاة", + "Max Latency": "أقصى زمن انتقال", + "Max Number of Cameras": "الحد الأقصى لعدد الكاميرات", + "Max Storage Amount": "ماكس تخزين كمية في ميغا بايت", + "MaxExposureTime": "أقصى وقت التعرض", + "MaxGain": "كسب أقصى", + "Maximum Change": "الحد الأقصى للتغيير", + "Maximum dB": "أقصى ديسيبل", + "May": "مايو", + "Merge Selected Videos": "دمج مقاطع الفيديو المحددة", + "Merge Video": "دمج الفيديو", + "Merge and Download": "دمج وتنزيل", + "MergeSelectedVideosMsg": "هل تريد دمج مقاطع الفيديو هذه؟ قد يستغرق الأمر بعض الوقت لدمج وتنزيل. في اللحظة التي يتم فيها إغلاق الاتصال ، سيتم حذف الملف. تأكد من إبقاء المتصفح مفتوحًا حتى يكتمل.", + "Methods": "أساليب", + "Migrator": "المهاجر", + "MinExposureTime": "الحد الأدنى لوقت التعرض", + "MinGain": "كسب الحد الأدنى", + "Minimum Change": "الحد الأدنى من التغيير", + "Minimum dB": "الحد الأدنى ديسيبل", + "Minutes": "الدقائق", + "Mode": "وضع", + "Monday": "الاثنين", + "Monitor": "رصد", + "Monitor Added by user": "رصد المضافة من قبل المستخدم.", + "Monitor Capture Rate": "رصد معدل التقاط (FPS)", + "Monitor Died": "توفي الشاشة", + "Monitor Edit": "شاشة التحرير", + "Monitor Groups": "رصد مجموعات", + "Monitor ID": "رصد ID", + "Monitor Idling": "رصد تسكع", + "Monitor Name": "رصد اسم", + "Monitor Settings": "إعدادات الشاشة", + "Monitor Start": "شاشة البدء", + "Monitor States": "الحالات مراقبة", + "Monitor States and Schedules": "مراقبة الدول والجداول الزمنية", + "Monitor Stop": "توقف الرصد", + "Monitor Stopped": "رصد توقف", + "Monitor Updated by user": "مراقبة تحديث من قبل المستخدم.", + "Monitor is now Disabled": "تم تعطيل الشاشة الآن", + "Monitor is now Idle": "الشاشة الآن خام", + "Monitor is now Recording": "يتم تسجيل الشاشة الآن", + "Monitor is now Watching": "تشاهد الشاشة الآن", + "Monitor mode changed": "مراقبة الوضع تغير", + "Monitor mode is already": "رصد وضع بالفعل", + "Monitor or Key does not exist.": "مراقبة أو عدم وجود مفتاح.", + "MonitorIdlingText": "مراقبة الدورة وقد أمر إلى الخمول.", + "MonitorStatesText": "يمكنك معرفة كيفية استخدام هذا هنا على shinobihub .", + "MonitorStoppedText": "مراقبة الدورة وقد أمر التوقف.", + "Monitors": "شاشات", + "Monitors per row": "شاشات لكل صف على المونتاج", + "Monitors to Copy to": "يراقب النسخ إلى", + "Montage": "المونتاج", + "Motion": "اقتراح", + "Motion Detection": "اكتشاف الحركة", + "Motion GUI": "الحركة واجهة المستخدم الرسومية", + "Motion Meter": "الحركة متر", + "Motion Threshold": "عتبة الحركة", + "Mp4Frag": "mp4frag", + "Must be atleast one row": "يجب أن يكون على الأقل صف واحد", + "Mute Audio": "كتم الصوت", + "NTP": "NTP", + "NTP Servers": "خوادم NTP", + "NVIDIA": "نفيديا", + "Name": "اسم", + "Name cannot be empty.": "لا يمكن أن يكون الاسم فارغًا.", + "Nameservers": "خوادم الأسماء", + "Network": "شبكة", + "Network Manager": "مدير الشبكة", + "Never": "مطلقا", + "New Authentication Token": "رمز مصادقة جديد", + "New Monitor": "شاشة جديدة", + "Newest": "أحدث", + "Next Video": "الفيديو التالي", + "No": "لا", + "No API Key": "لا مفتاح API", + "No Audio": "لا الصوت", + "No Data": "لا توجد بيانات", + "No Events found for this video": "لا الأحداث وجدت هذا الفيديو", + "No Group with this key exists": "أي جماعة مع هذا المفتاح موجود", + "No Monitor Exists with this ID.": "لا يوجد شاشة مع هذا المعرف.", + "No Monitor Found, Ignoring Request": "لا مراقبة وجدت تجاهل الطلب", + "No Monitor ID Present in Form": "لا يوجد معرف شاشة موجود في الشكل", + "No Monitors Selected": "لا شاشات مختارة", + "No Region": "لا منطقة", + "No Rotation": "لا دوران", + "No Sound": "لا صوت", + "No Trigger": "لا الزناد", + "No Videos Found": "لا توجد فيديوهات", + "No such file": "لا يوجد مثل هذا الملف", + "NoLogsFoundForDateRange": "لا توجد سجلات موجودة في هذا النطاق التاريخ. حاول توسيع نطاق التاريخ.", + "NoMotionEmailText1": "لا الحركة", + "NoMotionEmailText2": "لم يكن هناك أي حركة الكشف على الكاميرا", + "NoVideosFoundForDateRange": "لا أشرطة الفيديو الموجودة في هذا النطاق الزمني. محاولة تحديد بداية التاريخ إلى الوراء.", + "Noise Filter": "فلتر الضوضاء", + "Noise Filter Range": "نطاق تصفية الضوضاء", + "Non-Standard ONVIF": "غير قياسي onvif", + "Not Activated": "لم يتم تنشيطه", + "Not Authorized": "لا يؤذن", + "Not Connected": "غير متصل", + "Not Equal to": "لا يساوي", + "Not Found": "لا يوجد لا تجد", + "Not In": "لا في", + "Not Matches": "لا يطابق", + "Not Permitted": "لا يسمح", + "Not Saved": "يتم حفظ", + "Not an Administrator Account": "لا حساب مسؤول", + "NotAuthorizedText1": "لا يحق تقديم init الأمر مع \"مصادقة\",\"كه\", \"uid\"", + "Notes": "ملاحظات", + "NotesPlacholder": "التعليقات كنت تريد أن تترك هذه الكاميرات الإعدادات.", + "Nothing exists": "لا يوجد شيء", + "Notice": "تنويه", + "Notification Sound": "صوت الإشعار", + "Notification Video Length": "طول الفيديو الإخطار", + "Notifications": "إشعارات", + "NotifyErrorText": "تسبب الإخطار في حدوث خطأ", + "November": "شهر نوفمبر", + "Number of Days to keep": "عدد أيام الاحتفاظ", + "Numeric criteria unsupported for Region tests, Ignoring Conditional": "المعايير الرقمية غير مدعومة لاختبارات المنطقة ، متجاهلة المشروط", + "OAuth Code": "رمز OAUTH", + "OAuth Credentials": "أوراق اعتماد OAUTH", + "ONVIF": "onvif", + "ONVIF Device Manager": "مدير الجهاز onvif", + "ONVIF Port": "منفذ ONVIF", + "ONVIF Scanner": "ONVIF الماسح الضوئي", + "ONVIFErr400": "وجدت منفذ ONVIF لكن التفويض فشل عند استرداد عنوان URL للتيار. تحقق من اسم المستخدم وكلمة المرور المستخدمة للمسح. تأكد من مزامنة وقت الكاميرا ووقت الخادم.", + "ONVIFErr404": "لا يوجد لا تجد. قد يكون هذا مجرد لوحة ويب لجهاز الشبكة.", + "ONVIFErr405": "الطريقة غير مسموحة. تحقق من اسم المستخدم وكلمة المرور المستخدمة للمسح.", + "ONVIFEventsNotAvailable": "أحداث onvif غير متوفرة", + "ONVIFEventsNotAvailableText1": "قد لا تكون هذه الخدمة متاحة لهذه الكاميرا أو لم يتم تهيئة ONVIF بعد.", + "ONVIFnotCompliantProfileT": "الكاميرا ليست ملف تعريف onvif", + "ONVIFnote": "اكتشاف ONVIF الأجهزة على الشبكات خارج الخاص بك أو اتركه فارغا لمسح شبكة الاتصال الحالية الخاصة بك.
اسم المستخدم وكلمة المرور يمكن أن تترك فارغة.", + "OR": "أو", + "Object": "هدف", + "Object Count": "عدد الكائنات", + "Object Detection": "الكشف عن الكائن", + "Object Detector Flags": "أعلام كاشف الكائن", + "Object Tag": "علامة الكائن", + "Objects to look for": "كائنات للبحث عنها", + "October": "اكتوبر", + "Off": "عن", + "Oldest": "أقدم", + "On": "تشغيل", + "On Unexpected Exit": "على مخرج غير متوقع", + "Open": "يفتح", + "Open All Monitors": "افتح جميع الشاشات", + "Open Remote Dashboard": "افتح لوحة القيادة عن بُعد", + "OpenCV Cascades": "بنسف شلالات", + "Operating Hours": "ساعات العمل", + "Optional": "اختياري", + "Options": "خيارات", + "Order Streams": "ترتيب الجداول", + "Original Choice": "الاختيار الأصلي", + "Other Devices": "أجهزة أخرى", + "Output": "انتاج |", + "Output Method": "أسلوب الإخراج", + "P2P API Key": "مفتاح API P2P", + "P2P Host": "مضيف P2P", + "P2P Server Not Selected": "لم يتم تحديد خادم P2P", + "P2P Settings Applied": "إعدادات P2P المطبقة", + "PTZ Tracking": "تتبع PTZ", + "PTZ Tracking Target": "هدف تتبع PTZ", + "Password": "كلمة المرور", + "Password Again": "كلمة المرور مرة أخرى", + "Passwords don't match": "كلمات السر لا تتطابق", + "Paste JSON here.": "لصق JSON هنا.", + "Path": "المسار", + "Pause": "يوقف", + "Per Monitor": "لكل شاشة", + "Performance Optimization Possible": "تحسين الأداء ممكن", + "Permissions": "أذونات", + "Ping Failed": "فشلت بينغ", + "Plain": "سهل", + "Play": "يلعب", + "Playback": "التشغيل", + "Please Check Your Settings": "يرجى التحقق من الإعدادات الخاصة بك", + "Please Wait for Completion": "يرجى انتظار الانتهاء ، اعتمادًا على عدد الملفات المحددة ، قد يستغرق الأمر بعض الوقت.", + "Please Wait or Click to Stop Checking": "يرجى الانتظار أو النقر للتوقف", + "Please Wait...": "ارجوك انتظر...", + "Plugin": "توصيل في", + "Plugin Manager": "مدير المساعد", + "Points": "نقاط عند إضافة نقاط انقر على حافة المضلع.", + "Pop": "البوب", + "Popout Monitor on Event": "شاشة البوبوت في الحدث", + "Port": "ميناء", + "Pose": "يشير إلى", + "Poseidon": "بوسيدون", + "Position X": "موقف X", + "Position Y": "موقف Y", + "Power Video Viewer": "فيديو المشاهد", + "Power Viewer": "السلطة المشاهد", + "Preferences": "تفضيلات", + "Prefix": "اختصار", + "Preset": "مسبقا", + "Preset Name": "الاسم المسبق", + "Presets": "الإعدادات المسبقة", + "Preview": "معاينة", + "Previous Video": "الفيديو السابق", + "Primary Engine": "المحرك الأساسي", + "Primary Input": "المدخلات الأولية", + "Privileges": "الامتيازات", + "Probe Size": "التحقيق الحجم", + "Process Already Running": "العملية تعمل بالفعل", + "Process Crashed for Monitor": "عملية تحطمت على رصد", + "Process Not Running": "العملية لا تعمل", + "Process Started": "بدأت العملية", + "Process Unexpected Exit": "عملية خروج غير متوقع", + "Processor": "المعالج", + "Profile": "الملف الشخصي", + "Protocol": "بروتوكول", + "Public on ShinobiHub": "الجمهور على شينوبيهوب", + "Quality": "جودة 1 عالية ، 23 منخفضة", + "Query": "الاستعلام", + "Quick Settings": "إعدادات سريعة", + "Quick Sync Video": "فيديو مزامنة سريعة", + "RAM": "ذاكرة الوصول العشوائي", + "RTMP": "RTMP", + "RTMP Stream": "تيار RTMP", + "RTMP Stream Flags": "RTMP دفق الأعلام", + "RTMPS": "RTMPs", + "RTSP": "RTSP", + "RTSP Transport": "RTSP النقل", + "Range or Single": "مجموعة أو واحد", + "Raspberry Pi": "فطيرة التوت", + "Rate": "معدل (FPS)", + "Raw": "خام", + "Raw H.264 Stream": "RAW H.264 تيار", + "Reason": "سبب", + "Reboot": "اعادة التشغيل", + "Reboot Camera": "إعادة تشغيل الكاميرا", + "Recent Events": "الأحداث الأخيرة", + "Recent Videos": "أحدث مقاطع الفيديو", + "Recipient ID": "معرف المتلقي", + "Recommended": "مُستَحسَن", + "Reconnect Stream": "إعادة الاتصال تيار", + "Record": "سجل", + "Record File Type": "تسجيل نوع الملف", + "Record Height": "سجل ارتفاع", + "Record Video Filter": "تسجيل الفيديو فلتر", + "Record Width": "سجل العرض", + "Recorded Buffer": "مسجل العازلة", + "Recording": "تسجيل", + "Recording FPS": "تسجيل FPS", + "Recording FPS Change on Start": "تسجيل FPS تغيير في البداية", + "Recording Flags": "تسجيل الأعلام", + "Recording Segment Interval": "تسجيل الجزء الفاصل الزمني في دقائق", + "Recording Timeout": "تسجيل مهلة في دقائق", + "Recording Timestamp": "تسجيل الزمني", + "Recording Watermark": "تسجيل العلامة المائية", + "RecordingText": "فمن المستحسن أن تقوم بتعيين تسجيل نوع الملف إلى WebMMP4 و ترميز الفيديو إلى libvpxنسخ أو libx264 لأن إدخال نوع هو .", + "Refresh List of Cascades": "تحديث قائمة شلالات", + "Region": "منطقة", + "Region Editor": "المنطقة محرر", + "Region Name": "اسم المنطقة", + "RegionNote": "النقاط يتم حفظها فقط عند الضغط حفظ على إعدادات الشاشة نافذة.", + "Regions": "المناطق", + "Registered": "مسجل", + "Registered Servers": "الخوادم المسجلة", + "Remember Me": "تذكر لي", + "Request": "طلب", + "Require Object to be in Region": "طلب أن يكون الكائن في المنطقة", + "Reset": "إعادة ضبط", + "Reset Form": "إعادة تعيين الشكل", + "Reset Timer": "إعادة تعيين جهاز ضبط الوقت", + "Resolution": "دقة", + "Restart": "اعادة البدء", + "Restart CRON": "أعد تشغيل كرون", + "Restart Core": "إعادة تشغيل النواة", + "Restarting": "إعادة التشغيل", + "Restarting Process": "إعادة تشغيل العملية", + "Retry Connection": "إعادة المحاولة اتصال عدد المرات التي يسمح تفشل", + "Retrying...": "إعادة المحاولة...", + "Right": "صحيح عنوان URL", + "Right Stop": "صحيح وقف عنوان URL", + "Rotate": "تدوير", + "Rule": "القاعدة", + "Run Installer": "تشغيل المثبت", + "S3-Based Network Storage": "تخزين الشبكة المستندة إلى S3", + "SFTP": "SFTP", + "SFTP (SSH File Transfer)": "SFTP (نقل ملف SSH)", + "SFTP Error": "خطأ SFTP", + "Saturday": "السبت", + "Save": "حفظ", + "Save Changes": "حفظ التغييرات", + "Save Directory": "حفظ الدليل", + "Save Events": "حفظ الأحداث", + "Save Events to SQL": "حفظ الأحداث إلى SQL", + "Save Frames to Events": "حفظ الإطارات إلى الأحداث", + "Save Links to Database": "حفظ الروابط إلى قاعدة البيانات", + "Save Log in SQL": "حفظ سجل في SQL وهذا يمكن أن تملأ بسرعة.", + "Save New": "حفظ جديد", + "Save as": "حفظ باسم", + "Saved": "أنقذ", + "Saved Filters": "حفظ الفلاتر", + "Saved Logs": "السجلات المحفوظة", + "Saved Presets": "الإعدادات المسبقة المحفوظة", + "Saved Schedules": "الجداول الزمنية المحفوظة", + "Scan Settings": "إعدادات المسح الضوئي", + "Schedule": "جدول", + "Schedule Configuration Not Found": "لم يتم العثور على تكوين الجدول الزمني", + "Schedules": "جداول", + "Search": "البحث", + "Search Base": "قاعدة البحث", + "Search Filter": "تصفية البحث", + "Search Images": "البحث عن الصور", + "Search Settings": "إعدادات البحث", + "Second stream in feed": "الدفق الثاني في التغذية", + "Secure": "يؤمن", + "Select a Monitor": "حدد شاشة", + "Select atleast one monitor to delete": "حدد شاشة واحدة على الأقل لحذفها.", + "Selected": "المحدد", + "Send Frames": "إرسال إطارات دفع الإطارات ليتم تحليلها", + "Send Notification": "إرسال إشعار", + "Send to": "ارسل إلى", + "Separate with commas, no spaces": "منفصلة بفواصل, بدون مسافات", + "September": "سبتمبر", + "Server URL": "عنوان URL الخادم", + "Session Key": "مفتاح جلسة", + "Set Home": "وضع المنزل", + "Set Home Position (ONVIF-only)": "وضع الوضع المنزلي (onvif فقط)", + "Set Mode": "ضبط الوضع", + "Set to Watch Only": "تعيين مشاهدة فقط", + "Settings": "الإعدادات", + "Settings Changed": "تغيير الإعدادات", + "SettingsChangedText": "إعدادات تم حفظها وتطبيقها.", + "Sharpness": "حدة", + "Shinobi": "شينوبي", + "Shinobi Ordered to Update": "تم الانتهاء من تحديث شينوبي", + "Shinobi Streamer": "شينوبي غاسل", + "ShinobiHub": "شينوبيهوب", + "Show Logs": "تظهر سجلات", + "Show Matrices": "عرض المصفوفات", + "Show Matrix": "عرض مصفوفة", + "Show Regions of Interest": "إظهار المناطق ذات الاهتمام", + "Show Stream HUD": "عرض تيار هود", + "Show Thumbnails in Video List": "عرض الصورة المصغرة في قائمة الفيديو", + "Silent": "الصمت", + "Simple": "بسيطة", + "Size (mb)": "الحجم (ميجا بايت)", + "Skip Ping": "تخطي بينغ", + "Snapshot": "لقطة", + "Snapshot Flags": "لقطة الأعلام", + "Snapshots": "لقطات", + "Sort By": "فرز حسب", + "Space Used": "المساحة المستخدمة", + "Start": "تبدأ", + "Start Recording": "بدء التسجيل", + "Start Time": "وقت البدء", + "Start Time cannot be empty.": "لا يمكن أن يكون وقت البدء فارغًا.", + "Started": "بدأت", + "Started Building": "بدأ البناء", + "Starting": "بدء", + "State Configuration Not Found": "لم يتم العثور على تكوين الحالة", + "State Configuration has no monitors associated": "تكوين الدولة لا يرتبط مراقبين", + "Status Changed": "تغيرت الحالة", + "Status Indicator": "مؤشر حالة", + "Stop": "قف", + "Stop Command": "توقف الأمر", + "Stop URL": "وقف URL", + "Stopped": "توقفت", + "Stopping": "وقف", + "Storage Location": "مكان التخزين", + "Storage Use": "استخدام التخزين", + "Stream": "تيار", + "Stream Channel": "قناة تيار", + "Stream Flags": "تيار الأعلام", + "Stream Key": "مفتاح التيار", + "Stream Timestamp": "تيار الزمني", + "Stream Type": "تيار من نوع", + "Stream Watermark": "تيار مائية", + "Stream in Background": "دفق في الخلفية", + "Stream to YouTube": "تيار يوتيوب", + "Stream to YouTube Flags": "تيار يوتيوب الأعلام", + "StreamText": "

هذا القسم سوف تعين الابتدائي تيار الأسلوب وإعدادات. هذا التيار سوف يتم عرضها في لوحة القيادة. إذا اخترت استخدام HLS, JPEG, أو MJPEG ثم يمكنك تستهلك تيار من خلال البرامج الأخرى.

استخدام JPEG تيار أساسا إيقاف البث الأساسية يستخدم اللقطة بن للحصول على إطارات.

", + "Streamed Logs": "سجلات البث", + "Streamer": "غاسل", + "Streams": "تيارات", + "Sub-Accounts": "الحسابات الفرعية", + "Subdivision": "التقسيم الفرعي", + "Substream": "تيار فرعي", + "Substream Process": "عملية سجن", + "SubstreamNotConfigured": "لم يتم تكوين السلع الفرعية. افتح إعدادات الشاشة الخاصة بك وتكوينها.", + "Subtitle": "الترجمة", + "Success": "نجاح", + "Sunday": "الأحد", + "Superuser": "الخارق", + "Superuser Logs": "سجلات Superuser", + "Switch on for Still Image": "التبديل على صورة ثابتة", + "System": "نظام", + "System Level": "مستوى النظام", + "TCP": "TCP", + "TV Channel": "قناة تلفزيونية", + "TV Channel Group": "مجموعة القناة التلفزيونية", + "TV Channel ID": "معرف القناة التلفزيونية", + "Telegram": "برقية", + "Text Box Color": "النص مربع اللون", + "Text Color": "لون النص", + "Text criteria unsupported for Object Count tests, Ignoring Conditional": "معايير النص غير مدعومة لاختبارات عدد الكائنات ، متجاهلة المشروط", + "Themes": "موضوعات", + "There are no monitors that you can view with this account.": "لا توجد شاشات يمكنك عرضها مع هذا الحساب.", + "Threads": "الخيوط", + "Thumbnail": "ظفري", + "Thursday": "يوم الخميس", + "Time": "وقت", + "Time Created": "الوقت الذي تم إنشاؤه", + "Time Left": "الوقت المتبقي", + "Time Occurred": "حدث الوقت", + "Time-lapse": "الوقت الفاصل بين", + "Time-lapse Tool": "الوقت الفاصل بين أداة", + "TimeZone": "وحدة زمنية", + "Timelapse": "timelapse", + "Timelapse Frames Share": "إطارات timelapse حصة", + "Timelapse Watermark": "Timelapse Watermark", + "Timeout": "مهلة", + "Timeout Reset on Next Event": "إعادة تعيين المهلة في الحدث التالي", + "Timeout Reset on Next Motion": "مهلة إعادة تعيين على الحركة القادمة", + "Timezone": "وحدة زمنية", + "Timezone Offset": "إزاحة المنطقة الزمنية", + "Title": "عنوان", + "Today": "اليوم", + "Toggle Sidebar": "تبديل الشريط الجانبي", + "Toggle Substream": "تبديل سجن", + "Token": "رمز", + "Top Left": "أعلى اليسار", + "Top Right": "أعلى اليمين", + "Traditional (Watch-Only, Includes Buffer)": "تقليدي (ساعة فقط ، تشمل المخزن المؤقت)", + "Traditional Recording": "التسجيل التقليدي", + "Traditional Recording Flags": "أعلام التسجيل التقليدية", + "Train": "قطار", + "TrainConfirm": "هل أنت متأكد أنك تريد أن تبدأ التدريب؟ هذا يمكن أن يستغرق أكثر من 12 ساعة مع أكثر من 500 صورة. هذا سوف يستهلك كمية كبيرة من الموارد ، مثل ذاكرة الوصول العشوائي و/أو وحدة المعالجة المركزية.", + "TrainConfirmStop": "هل أنت متأكد أنك تريد التوقف عن التدريب؟", + "Trainer Engine": "محرك المدرب", + "Trigger Blocked": "الزناد محظور", + "Trigger Camera Groups": "تشغيل مجموعات الكاميرا", + "Trigger Event": "حدث مسبب", + "Trigger Group to Record": "مجموعة الزناد لتسجيل", + "Trigger Record": "الزناد سجل", + "Trigger Successful": "الزناد ناجحة", + "Trigger Threshold": "عتبة الزناد", + "Tuesday": "يوم الثلاثاء", + "Turn Speed": "بدوره السرعة", + "Type": "يكتب", + "UDP": "UDP", + "URL": "عنوان URL", + "URL Stop Timeout": "URL وقف مهلة تشغيل إيقاف URL بعد X ثانية", + "US": "لنا", + "UTCDateTime": "تاريخ", + "Unable to Launch": "قادر على إطلاق", + "UnabletoLaunchText": "الرجاء حفظ رصد جديدة أولا. ثم محاولة إطلاق محرر المنطقة.", + "Uncommon Objects": "كائنات غير شائعة", + "Uniform": "زى موحد", + "Unlink": "غير محدود", + "Unlink Login": "تسجيل الدخول غير المرتبط؟", + "Unlinked": "غير مترابط", + "Up": "حتى عنوان URL", + "Up Stop": "حتى وقف عنوان URL", + "Update": "تحديث", + "Update to Development": "تحديث للتطوير", + "Update to Master": "تحديث إلى Master", + "Upload Bandwidth": "تحميل النطاق الترددي", + "Upload File": "رفع ملف", + "Uploaded Only": "تم الرفع فقط", + "Uploaders": "تحميلات", + "Use Built-In": "استخدام مدمج", + "Use Camera Timestamps": "استخدام الطابع الزمني للكاميرا", + "Use Global Amazon S3 Video Storage": "استخدم تخزين فيديو Amazon S3 العالمي", + "Use Global Backblaze B2 Video Storage": "استخدم تخزين الفيديو Global Backblaze B2", + "Use Global Wasabi Hot Cloud Storage Video Storage": "استخدم تخزين فيديو تخزين السحابة الساخن العالمي Wasabi", + "Use Global WebDAV Video Storage": "استخدم تخزين فيديو WebDav العالمي", + "Use HTML5 Play Method": "استخدم طريقة تشغيل HTML5", + "Use Max Storage Amount": "استخدم أقصى مبلغ التخزين", + "Use Raw Snapshot": "استخدام لقطة الخام", + "Use Substream": "استخدام Substream", + "Use coProcessor": "استخدام المعالج", + "UseCount": "usecount", + "User Log": "سجل المستخدم", + "User Not Found": "لم يتم العثور على المستخدم", + "Username": "اسم المستخدم", + "VA-API": "VA-api", + "Value": "القيمة", + "Video": "فيديو", + "Video Bit Rate": "معدل بت الفيديو", + "Video Codec": "ترميز الفيديو", + "Video Configuration": "تكوين الفيديو", + "Video Filter": "فيديو مرشح", + "Video Finished": "الفيديو النهائي", + "Video Length (minutes) and Motion Count per video": "طول الفيديو (دقيقة) و الحركة الاعتماد في الفيديو", + "Video Limit": "حد الفيديو", + "Video Record Rate": "الفيديو سجل معدل (FPS)", + "Video Set": "مجموعة الفيديو", + "Video Share": "مشاركة الفيديو", + "Video Status": "فيديو حالة", + "Video and Time Span (Minutes)": "فيديو من الزمن (دقيقة)", + "Video stream only from first feed": "دفق الفيديو فقط من الخلاصة الأولى", + "Video streams only": "تدفقات الفيديو فقط", + "Videos": "الفيديو", + "Videos List": "قائمة أشرطة الفيديو", + "Videos Merge": "دمج مقاطع الفيديو", + "Viewing Server Stats": "عرض إحصائيات الخادم", + "Warning": "تحذير", + "Wasabi Hot Cloud Storage": "تخزين السحابة الساخنة في Wasabi", + "Wasabi Hot Cloud Storage Upload Error": "خطأ في تحميل تخزين السحابة الساخنة", + "Watch": "مشاهدة", + "Watch Only": "مشاهدة فقط", + "Watch-Only": "الساعة فقط", + "Watching": "مشاهدة", + "Web Page": "صفحة ويب", + "WebDAV": "WebDav", + "WebM (libvpx)": "WebM (libvpx)", + "Webdav Error": "خطأ Webdav", + "WebdavErrorText": "لا يمكن حفظ. هل جعل الكاميرا المجلدات داخل اخترتها حفظ الدليل ؟ ", + "WebdavErrorTextCreatingDir": "لا يمكن إنشاء دليل.", + "WebdavErrorTextTryCreatingDir": "لا يمكن حفظ. محاولة إنشاء دليل.", + "Webhook": "webhook", + "Webhook URL": "Webhook URL", + "Websocket": "WebSocket", + "Websocket Connected": "WebSocket متصل", + "Websocket Disconnected": "WebSocket غير متصل", + "Wednesday": "الأربعاء", + "Welcome": "أهلا بك!", + "When Detector is Off": "عندما يكون الكاشف مطفئًا", + "When Detector is On": "عندما يكون الكاشف في", + "WhiteBalance": "توازن اللون الأبيض", + "WideDynamicRange": "مجال حركي واسع", + "Width": "العرض", + "X Point": "x نقطة", + "Y Point": "ص نقطة", + "Yes": "نعم", + "Zip and Download": "الرمز البريدي والتنزيل", + "Zipping Videos": "انقطاع مقاطع الفيديو", + "Zones": "المناطق", + "Zoom In": "التكبير في عنوان URL", + "Zoom In Stop": "التكبير في التوقف عن عنوان URL", + "Zoom Out": "تصغير عنوان URL", + "Zoom Out Stop": "تصغير وقف عنوان URL", + "a day": "اليوم", + "a few seconds": "بضع ثوان", + "a minute": "دقيقة", + "a month": "شهر", + "a year": "في السنة", + "aac": "الجميح للسيارات", + "aac (Default)": "الجميح للسيارات (افتراضي)", + "ac3": "AC3", + "accountActionFailed": "فشل إجراء الحساب", + "accountAdded": "تمت إضافة الحساب", + "accountAddedText": "تمت إضافة الحساب.", + "accountDeleted": "تم حذف الحساب", + "accountDeletedText": "تم حذف الحساب.", + "accountId": "معرف الحساب", + "accountSettingsDescription": "إدارة ملف التعريف الخاص بك وتعيين خيارات مثل Max Storage Come و Max عدد الأيام للحفاظ على مقاطع الفيديو.", + "accountSettingsError": "خطأ إعدادات الحساب", + "activatedText": "تم تنشيط التثبيت الخاص بك.", + "ago": "قبل", + "airplane": "مطار", + "alreadyLinked": "مرتبط بالفعل بحساب", + "an hour": "ساعة", + "apple": "تفاح", + "applicationKey": "مفتاح التطبيق", + "aws_accessKeyId": "الوصول إلى معرف المفتاح", + "aws_secretAccessKey": "مفتاح الوصول السري", + "backpack": "حقيبة ظهر", + "banana": "موز", + "baseball bat": "مضرب البيسبول", + "baseball glove": "قفاز البيسبول", + "bear": "دب", + "bed": "سرير", + "bench": "مقعد", + "bicycle": "دراجة", + "bindDN": "binddn", + "bird": "عصفور", + "blankPassword": "تترك فارغة إلى الحفاظ على نفس كلمة المرور", + "boat": "قارب", + "book": "الكتاب", + "bottle": "زجاجة", + "bowl": "صَحن", + "broccoli": "بروكلي", + "bus": "أوتوبيس", + "cake": "كيك", + "calendar": "التقويم", + "car": "السيارات", + "carrot": "جزرة", + "cat": "قطة", + "cell phone": "الهاتف الخلوي", + "chair": "كرسي", + "clientStreamFailedattemptingReconnect": "العميل ctream تحقق فشلت محاولة إعادة الاتصال.", + "clock": "ساعة حائط", + "coProcess Crashed for Monitor": "تحطمت المعالجة المشتركة للرصد", + "coProcess Unexpected Exit": "المعالجة المشتركة مخرج غير متوقع", + "coProcessor": "المعالج", + "coProcessor Started": "بدأ المعالج المشترك", + "coProcessor Stopped": "توقف المعالج", + "coProcessorTextStarted": "بدأ Coprocessor لمخرجات وحدة المعالجة المركزية فقط.", + "coProcessorTextStopped": "انتهى المعالج.", + "codecMismatchText1": "توفر الكاميرا بيانات دفق H.265 (HEVC) وتستخدم نسخًا كبرنامج ترميز الفيديو لقسم الدفق. قد لا يظهر دفقك من Shinobi على الأجهزة التي لا يمكنها استخدام برنامج الترميز هذا. يمكن لتطبيق Shinobi Mobile عرض هذه التدفقات.", + "codecMismatchText2": "برنامج ترميز الفيديو الذي اخترته غير قابل للتطبيق. توفر الكاميرا الخاصة بك بيانات دفق MJPEG وتستخدم نسخًا كبرنامج ترميز الفيديو لقسم الدفق. غيرت نوع الدفق إلى MJPEG.", + "codecMismatchText3": "برنامج ترميز الفيديو الذي اخترته غير قابل للتطبيق. توفر الكاميرا الخاصة بك بيانات دفق MJPEG وتستخدم نسخًا كبرنامج ترميز الفيديو لقسم التسجيل. غيرت برنامج ترميز الفيديو إلى libx264.", + "confirmDeleteFilter": "هل تريد حذف هذا الفلتر ؟ لا يمكنك استرداد.", + "contactAdmin": "اتصل بصيانة تثبيت Shinobi الخاص بك.", + "copy": "نسخ", + "couch": "أريكة", + "cow": "بقرة", + "cuda": "كودا (نفيديا نفينك)", + "cup": "فنجان", + "cuvid": "cuvid (nvidia nvenc)", + "days": "أيام", + "deleteApiKey": "حذف مفتاح API", + "deleteApiKeyText": "هل تريد حذف مفتاح API هذا؟ لا يمكنك استعادته.", + "deleteMonitorStateText1": "هل تريد حذف هذه الشاشة المسبقة؟ لا يمكن استرداد تكوينات الشاشة المرتبطة.", + "deleteMonitorStateText2": "هل تريد حذف مسبق هذا الشاشة؟", + "deleteScheduleText": "هل تريد حذف هذا الجدول؟ لن يتم تعديل مراقبة الإعدادات المسبقة المرتبطة ..", + "deleteSubAccount": "حذف الحساب الفرعي", + "deleteSubAccountText": "هل تريد حذف هذا الحساب الفرعي؟ لا يمكنك استعادته.", + "dining table": "طاولة الطعام", + "dog": "كلب", + "donut": "كعكة محلاة", + "drm": "مشاركة كائن DRM", + "dropBoxSuccess": "النجاح! الملفات المحفوظة إلى دروببوإكس الخاص بك.", + "dxva2": "DXVA2 (Video DirectX ، Windows)", + "elephant": "الفيل", + "eventFilterActionText": "هذه هي الإجراءات التي تحدث من ظروف المرشح التي نجحت. يشير \"الاختيار الأصلي\" إلى الخيار الذي اخترته في إعدادات شاشتك.", + "eventFilterErrorBrackets": "لديك عدد من الأقواس غير المقدمة. يتم تجاهلهم.", + "eventFiltersDescription": "مرشحات الإعداد عندما تحدث الأحداث.", + "failedLoginText1": "لقد فشلت في تسجيل الدخول عدة مرات. يجب أن تنتظر 15 دقيقة قبل المحاولة مرة أخرى.", + "failedLoginText2": "يرجى التحقق من بيانات اعتماد تسجيل الدخول الخاصة بك.", + "fieldMissingValueText1": "توفر الكاميرا بيانات دفق MJPEG. تحتاج إلى ضبط معدل التقاط الشاشة. سيحاول Shinobi اكتشافها وملءها تلقائيًا.", + "fieldTextAccelerator": "تسريع الأجهزة (Hwaccel) لفك تشفير التدفقات.", + "fieldTextAcodec": "برنامج ترميز الصوت للتسجيل.", + "fieldTextActionsCommand": "يمكنك استخدام هذا لإطلاق برنامج نصي على الأمر.", + "fieldTextActionsHalt": "اجعل الحدث لا يفعل شيئًا ، كما لو لم يحدث أبدًا.", + "fieldTextActionsIndifference": "تعديل الحد الأدنى من اللامبالاة المطلوبة للحدث.", + "fieldTextActionsRecord": "استخدم التسجيل التقليدي أو hotswap أو حذف بلا حراك مع خياراتهم المحددة حاليًا في قسم إعدادات الكشف العالمي.", + "fieldTextAduration": "حدد عدد microseconds التي يتم تحليلها للتحقيق في المدخلات. قم بتعيين 100000 إذا كنت تستخدم RTSP وتواجد مشكلات الدفق.", + "fieldTextAudioAlert": "الصوت عندما يحدث الحدث.", + "fieldTextAudioDelay": "تأخير حتى في المرة القادمة يمكن أن يبدأ الحدث في تنبيه. تقاس في ثوان.", + "fieldTextAudioNote": "الصوت عندما تظهر فقاعة المعلومات.", + "fieldTextAutoHost": "عنوان URL للتيار الكامل.", + "fieldTextAutoHostEnable": "قم بتغذية القطع الفردية المطلوبة لإنشاء عنوان URL للتيار أو توفير عنوان URL الكامل والسماح لشينوبي بتحليله لك.", + "fieldTextChannelHlsListSize": "عدد الأجزاء الحد الأقصى قبل حذف الأجزاء القديمة تلقائيًا.", + "fieldTextChannelHlsTime": "كم من الوقت يجب أن يكون كل قطعة فيديو ، في دقائق. سيتم رسم كل قطعة بواسطة العميل من خلال ملف M3U8. الأجزاء الأقصر تأخذ مساحة أقل.", + "fieldTextChannelPresetStream": "العلم المسبق لبعض الترميزات الفيديو. إذا وجدت أن الكاميرا تتعطل كل بضع ثوانٍ: حاول تركها فارغة.", + "fieldTextChannelStreamAcodec": "برامج الترميز الصوتي للبث.", + "fieldTextChannelStreamAcodecAac": "تستخدم لفيديو mp4.", + "fieldTextChannelStreamAcodecAc3": "تستخدم لفيديو mp4.", + "fieldTextChannelStreamAcodecAuto": "دع FFMPEG يختار.", + "fieldTextChannelStreamAcodecCopy": "تستخدم لفيديو mp4. يحتوي على استخدام منخفض للغاية لوحدة المعالجة المركزية ولكن بعض برامج ترميز الصوت تحتاج إلى أعلام مخصصة مثل -strict 2 لـ AAC.", + "fieldTextChannelStreamAcodecLibmp3lame": "تستخدم لفيديو mp4.", + "fieldTextChannelStreamAcodecLibopus": "تستخدم للفيديو webm.", + "fieldTextChannelStreamAcodecLibvorbis": "تستخدم للفيديو webm.", + "fieldTextChannelStreamAcodecNoAudio": "لا يوجد صوت ، هذا خيار يجب تعيينه في بعض أجزاء العالم لأسباب قانونية.", + "fieldTextChannelStreamFps": "السرعة التي يتم بها عرض الإطارات للعملاء ، في إطارات في الثانية. كن على دراية لا يوجد افتراضي. هذا يمكن أن يؤدي إلى استخدام النطاق الترددي العالي.", + "fieldTextChannelStreamQuality": "عدد منخفض يعني جودة أعلى. العدد الأعلى يعني أقل جودة.", + "fieldTextChannelStreamRotate": "قم بتغيير زاوية عرض دفق الفيديو.", + "fieldTextChannelStreamScaleX": "عرض صورة الدفق التي يتم إخراجها بعد المعالجة.", + "fieldTextChannelStreamScaleY": "ارتفاع صورة الدفق التي يتم إخراجها بعد المعالجة.", + "fieldTextChannelStreamType": "الطريقة التي سيتم استخدامها لاستهلاك دفق الفيديو.", + "fieldTextChannelStreamTypeFLV": "إرسال إطارات مشفرة FLV عبر WebSocket.", + "fieldTextChannelStreamTypeHLS(includesAudio)": "طريقة مماثلة لتدفقات Facebook Live. يتضمن الصوت إذا كان الإدخال يوفره. هناك تأخير من حوالي 4-6 ثوان لأن هذه الطريقة تسجل شرائح ثم يدفعها إلى العميل بدلاً من الدفع كما تنشئها.", + "fieldTextChannelStreamTypeMJPEG": "صورة قياسية JPEG صورة. لا صوت.", + "fieldTextChannelStreamTypePoseidon": "تم بناء Poseidon على رمز معالجة MP4 من Kevin Godell. يحاكي ملف MP4 المتداول ولكن باستخدام بيانات الدفق المباشر. يشمل الصوت. يمكن لبعض المتصفحات تشغيله مثل ملف MP4 عادي. تدفقات فوق HTTP أو WebSocket.", + "fieldTextChannelStreamVcodec": "برنامج ترميز الفيديو للدفق.", + "fieldTextChannelStreamVcodecAuto": "دع FFMPEG يختار.", + "fieldTextChannelStreamVcodecCopy": "تستخدم لفيديو mp4. يحتوي على استخدام منخفض للغاية لوحدة المعالجة المركزية ولكن لا يمكن استخدام مرشحات الفيديو والملفات قد يكون عملاقًا. من الأفضل إعداد إعدادات إعدادات MP4 الخاصة بك عند استخدام هذا الخيار.", + "fieldTextChannelStreamVcodecLibx264": "تستخدم لفيديو mp4.", + "fieldTextChannelStreamVcodecLibx265": "تستخدم لفيديو mp4.", + "fieldTextChannelSvf": "ضع مرشحات الفيديو FFMPEG في هذا المربع للتأثير على جزء البث. بدون مسافات.", + "fieldTextControlInvertY": "لأنه عندما يتم تثبيت الكاميرا رأسًا على عقب أو تستخدم عناصر تحكم رأسية مقلوبة.", + "fieldTextCrf": "عدد منخفض يعني جودة أعلى. العدد الأعلى يعني أقل جودة.", + "fieldTextCustDetect": "أعلام مخصصة ترتبط بكاشف الدفق للتحليل.", + "fieldTextCustDetectObject": "أعلام مخصصة ترتبط بكاشف الدفق للتحليل.", + "fieldTextCustInput": "أعلام مخصصة ترتبط بإدخال عملية FFMPEG.", + "fieldTextCustRecord": "أعلام مخصصة ترتبط بتسجيل عملية FFMPEG.", + "fieldTextCustSipRecord": "أعلام مخصصة ترتبط بالإخراج الذي تسجله الحدث.", + "fieldTextCustSnap": "الأعلام المخصصة التي ترتبط اللقطات.", + "fieldTextCustStream": "أعلام مخصصة ترتبط بالدفق (العرض جانب العميل) لعملية FFMPEG.", + "fieldTextCustomOutput": "إضافة إخراج مخصص مثل إطارات JPEG أو إرسال البيانات مباشرة إلى خادم آخر.", + "fieldTextCutoff": "في دقائق. متى تنطلق وبدء ملف فيديو جديد.", + "fieldTextDays": "عدد الأيام للحفاظ على مقاطع الفيديو قبل التطهير.", + "fieldTextDetailSubstreamInputRtspTransportAuto": "دع FFMPEG تقرر. عادة سيحاول UDP أولاً.", + "fieldTextDetailSubstreamInputRtspTransportTCP": "اضبطه على هذا إذا بدأ UDP في إعطاء نتائج غير مرغوب فيها.", + "fieldTextDetailSubstreamInputRtspTransportUDP": "FFMPEG يحاول هذا أولا.", + "fieldTextDetailSubstreamOutputHlsListSize": "عدد الأجزاء الحد الأقصى قبل حذف الأجزاء القديمة تلقائيًا.", + "fieldTextDetailSubstreamOutputHlsTime": "كم من الوقت يجب أن يكون كل قطعة فيديو ، في دقائق. سيتم رسم كل قطعة بواسطة العميل من خلال ملف M3U8. الأجزاء الأقصر تأخذ مساحة أقل.", + "fieldTextDetailSubstreamOutputPresetStream": "العلم المسبق لبعض الترميزات الفيديو. إذا وجدت أن الكاميرا تتعطل كل بضع ثوانٍ: حاول تركها فارغة.", + "fieldTextDetailSubstreamOutputStreamAcodec": "برامج الترميز الصوتي للبث.", + "fieldTextDetailSubstreamOutputStreamAcodecAac": "تستخدم لفيديو mp4.", + "fieldTextDetailSubstreamOutputStreamAcodecAc3": "تستخدم لفيديو mp4.", + "fieldTextDetailSubstreamOutputStreamAcodecAuto": "دع FFMPEG يختار.", + "fieldTextDetailSubstreamOutputStreamAcodecCopy": "تستخدم لفيديو mp4. يحتوي على استخدام منخفض للغاية لوحدة المعالجة المركزية ولكن بعض برامج ترميز الصوت تحتاج إلى أعلام مخصصة مثل -strict 2 لـ AAC.", + "fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame": "تستخدم لفيديو mp4.", + "fieldTextDetailSubstreamOutputStreamAcodecLibopus": "تستخدم للفيديو webm.", + "fieldTextDetailSubstreamOutputStreamAcodecLibvorbis": "تستخدم للفيديو webm.", + "fieldTextDetailSubstreamOutputStreamAcodecNoAudio": "لا يوجد صوت ، هذا خيار يجب تعيينه في بعض أجزاء العالم لأسباب قانونية.", + "fieldTextDetailSubstreamOutputStreamFps": "السرعة التي يتم بها عرض الإطارات للعملاء ، في إطارات في الثانية. كن على دراية لا يوجد افتراضي. هذا يمكن أن يؤدي إلى استخدام النطاق الترددي العالي.", + "fieldTextDetailSubstreamOutputStreamQuality": "عدد منخفض يعني جودة أعلى. العدد الأعلى يعني أقل جودة.", + "fieldTextDetailSubstreamOutputStreamRotate": "قم بتغيير زاوية عرض دفق الفيديو.", + "fieldTextDetailSubstreamOutputStreamScaleX": "عرض صورة الدفق التي يتم إخراجها بعد المعالجة.", + "fieldTextDetailSubstreamOutputStreamScaleY": "ارتفاع صورة الدفق التي يتم إخراجها بعد المعالجة.", + "fieldTextDetailSubstreamOutputStreamType": "الطريقة التي سيتم استخدامها لاستهلاك دفق الفيديو.", + "fieldTextDetailSubstreamOutputStreamVcodec": "برنامج ترميز الفيديو للدفق.", + "fieldTextDetailSubstreamOutputStreamVcodecAuto": "دع FFMPEG يختار.", + "fieldTextDetailSubstreamOutputStreamVcodecCopy": "تستخدم لفيديو mp4. يحتوي على استخدام منخفض للغاية لوحدة المعالجة المركزية ولكن لا يمكن استخدام مرشحات الفيديو والملفات قد يكون عملاقًا. من الأفضل إعداد إعدادات إعدادات MP4 الخاصة بك عند استخدام هذا الخيار.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx264": "تستخدم لفيديو mp4.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx265": "تستخدم لفيديو mp4.", + "fieldTextDetailSubstreamOutputSvf": "ضع مرشحات الفيديو FFMPEG في هذا المربع للتأثير على جزء البث. بدون مسافات.", + "fieldTextDetector": "سيؤدي ذلك إلى إضافة إخراج آخر في أمر FFMPEG للكشف عن الحركة.", + "fieldTextDetectorAudio": "تحقق مما إذا كان الصوت قد حدث على شهادة Certiain. قد لا تكون القراءة المألوفة دقيقة لقياس العالم الحقيقي.", + "fieldTextDetectorBufferHlsListSize": "عدد الأجزاء الحد الأقصى قبل حذف الأجزاء القديمة تلقائيًا.", + "fieldTextDetectorBufferHlsTime": "كم من الوقت يجب أن يكون كل قطعة فيديو ، في ثوان. سيتم رسم كل قطعة بواسطة العميل من خلال ملف M3U8. الأجزاء الأقصر تأخذ مساحة أقل.", + "fieldTextDetectorColorThreshold": "مقدار الاختلاف المسموح به في بكسل قبل اعتباره الحركة.", + "fieldTextDetectorCommand": "الأمر الذي سيتم تشغيله. هذا هو ما يعادل تشغيل أمر شل من المحطة.", + "fieldTextDetectorCommandTimeout": "هذه القيمة هي مؤقت للسماح بالتشغيل التالي للنص الخاص بك. هذه القيمة في دقائق.", + "fieldTextDetectorFps": "كم عدد الإطارات الثانية لإرسالها إلى كاشف الحركة ؛ 2 هو الافتراضي.", + "fieldTextDetectorFrame": "هذا سوف يقرأ الإطار بأكمله للاختلافات بكسل. هذا هو نفسه إنشاء منطقة تغطي الشاشة بأكملها.", + "fieldTextDetectorHttpApi": "هل تريد السماح بمحفزات HTTP لهذه الكاميرا؟", + "fieldTextDetectorLisencePlate": "تمكين التعرف على لوحة الترخيص. قام OpenAlpr Plugin بتمكين هذا دائمًا.", + "fieldTextDetectorLisencePlateCountry": "اختر نوع اللوحات للتعرف عليها. فقط نحن والاتحاد الأوروبي مدعومون في هذا الوقت.", + "fieldTextDetectorLockTimeout": "قفل عندما يُسمح للمشغل التالي ، لتجنب التحميل الزائد لقاعدة البيانات واستقبال العملاء. تقاس بالميلي ثانية.", + "fieldTextDetectorMaxSensitivity": "يجب أن يكون تصنيف ثقة الحركة أقل من هذه القيمة ليتم اعتباره مشغلًا. اترك فارغًا بدون الحد الأقصى. تم تسمية هذا الخيار مسبقًا \"Max Lateperference\".", + "fieldTextDetectorNoiseFilter": "محاولة تصفية الحبوب أو الحركة المتكررة في لامبالاة معينة.", + "fieldTextDetectorNoiseFilterRange": "مقدار الاختلاف المسموح به في بكسل قبل اعتباره الحركة.", + "fieldTextDetectorNotrigger": "تحقق مما إذا كانت الحركة قد حدثت على فاصل. إذا حدث الحركة ، فسيتم إعادة تعيين الشيك.", + "fieldTextDetectorNotriggerCommand": "الأمر الذي سيتم تشغيله. هذا هو ما يعادل تشغيل أمر شل من المحطة.", + "fieldTextDetectorNotriggerCommandTimeout": "هذه القيمة هي مؤقت للسماح بالتشغيل التالي للنص الخاص بك. هذه القيمة في دقائق.", + "fieldTextDetectorNotriggerDiscord": "إذا لم يتم اكتشاف الحركة بعد فترة المهلة ، فسوف تتلقى إخطارًا بالخلاف.", + "fieldTextDetectorNotriggerTimeout": "يتم حساب المهلة في دقائق.", + "fieldTextDetectorNotriggerWebhook": "إرسال طلب الحصول على عنوان URL مع بعض القيم من الحدث.", + "fieldTextDetectorObjCount": "العد الكشف عن الكائنات.", + "fieldTextDetectorObjCountInRegion": "عدد الكائنات فقط داخل المناطق.", + "fieldTextDetectorPam": "استخدم كاشف حركة كيفن جوديل. هذا مدمج في شينوبي ولا يتطلب أي تكوين آخر لتفعيله.", + "fieldTextDetectorPtzFollow": "اتبع أكبر كائن تم اكتشافه مع PTZ؟ يتطلب تشغيل كاشف الكائن أو المصفوفات المتوفرة بالأحداث.", + "fieldTextDetectorRecordMethod": "هناك طرق متعددة للبدء في التسجيل عند حدوث حدث ، مثل الحركة. التسجيل التقليدي هو الأكثر سهولة في الاستخدام.", + "fieldTextDetectorSave": "حفظ أحداث الحركة في SQL. سيسمح ذلك بعرض الحركة عبر الفيديو خلال الوقت الذي حدثت فيه أحداث الحركة في عارض الطاقة.", + "fieldTextDetectorScaleX": "عرض الصورة التي يتم اكتشافها. أحجام أصغر تأخذ أقل وحدة المعالجة المركزية.", + "fieldTextDetectorScaleY": "ارتفاع الصورة التي يتم اكتشافها. أحجام أصغر تأخذ أقل وحدة المعالجة المركزية.", + "fieldTextDetectorSendFrames": "ادفع إطارات إلى المكون الإضافي المتصل ليتم تحليله.", + "fieldTextDetectorSendFramesObject": "ادفع إطارات إلى المكون الإضافي المتصل ليتم تحليله.", + "fieldTextDetectorSendVideoLength": "في ثوان. طول الفيديو الذي يتم إرساله إلى خدمة الإخطار الخاصة بك ، مثل البريد الإلكتروني أو الخلاف.", + "fieldTextDetectorSensitivity": "يجب أن يتجاوز تصنيف ثقة الحركة هذه القيمة المراد اعتبارها مشغلًا. يرتبط هذا الرقم مباشرة بتصنيف الثقة الذي تم إرجاعه بواسطة كاشف الحركة. تم تسمية هذا الخيار مسبقًا \"اللامبالاة\".", + "fieldTextDetectorThreshold": "الحد الأدنى لعدد الاكتشافات لإطلاق حدث حركة. يجب أن تكون الاكتشافات داخل الكاشف ، العتبة مقسومًا على ثواني الكاشف. على سبيل المثال ، إذا كان الكاشف FPS هو 2 وعتبة الزناد هي 3 ، فيجب أن تحدث ثلاثة اكتشافات في غضون 1.5 ثانية لتشغيل حدث حركة. هذه العتبة هي منطقة الكشف.", + "fieldTextDetectorTimeout": "سيتم تشغيل طول الوقت \"سجل الزناد\". هذا يقرأ في دقائق.", + "fieldTextDetectorTrigger": "سيطلب هذا الكاميرا لتسجيل ما إذا كانت تم ضبطها على \"الساعة فقط\" عند اكتشاف حدث ما.", + "fieldTextDetectorUseDetectObject": "قم بإنشاء إطارات للإرسال إلى أي مكون إضافي متصل.", + "fieldTextDetectorWebhook": "إرسال طلب الحصول على عنوان URL مع بعض القيم من الحدث.", + "fieldTextDetectorWebhookTimeout": "هذه القيمة عبارة عن مؤقت للسماح بالتشغيل التالي لـ WebHook. هذه القيمة في دقائق.", + "fieldTextDir": "موقع المكان الذي سيتم حفظ فيه الملفات المسجلة. يمكنك تكوين المزيد من المواقع باستخدام متغير AddStorage .", + "fieldTextEventDays": "عدد الأيام للحفاظ على الأحداث قبل التطهير.", + "fieldTextEventMonPop": "عندما يحدث حدث popout دفق الشاشة.", + "fieldTextEventRecordScaleX": "عرض صورة التسجيل المستندة إلى الحدث التي يتم إخراجها بعد المعالجة.", + "fieldTextEventRecordScaleY": "ارتفاع صورة التسجيل المستندة إلى الأحداث التي يتم إخراجها بعد المعالجة.", + "fieldTextExt": "نوع الملف لملف الفيديو المسجل.", + "fieldTextExtMP4": "نوع الملف قابل للتشغيل هو جميع متصفحات الويب الحديثة تقريبًا ، والتي تشمل الهاتف المحمول. تميل الملفات فقط إلى أن تكون أكبر إلا إذا قمت بتخفيض الجودة.", + "fieldTextExtWebM": "ملفات صغيرة ، انخفاض توافق العميل. جيد للتحميل إلى مواقع مثل YouTube.", + "fieldTextFactorAuth": "تمكين متطلبات ثانوية لتسجيل الدخول من خلال إحدى الطرق الممكّنة.", + "fieldTextFatalMax": "عدد المرات التي يجب إعادة محاولتها لاتصال الشبكة بين الخادم والكاميرا قبل إعداد الشاشة على تعطيلها. لا عشرية. ضبط على 0 لإعادة المحاولة إلى الأبد.", + "fieldTextFps": "السرعة التي يتم فيها تسجيل الإطارات على الملفات ، إطارات في الثانية. كن على دراية لا يوجد افتراضي. هذا يمكن أن يؤدي إلى ملفات كبيرة. من الأفضل ضبط هذا الكاميرا.", + "fieldTextHeight": "ارتفاع صورة الدفق.", + "fieldTextHlsListSize": "عدد الأجزاء الحد الأقصى قبل حذف الأجزاء القديمة تلقائيًا.", + "fieldTextHlsTime": "كم من الوقت يجب أن يكون كل قطعة فيديو ، في دقائق. سيتم رسم كل قطعة بواسطة العميل من خلال ملف M3U8. الأجزاء الأقصر تأخذ مساحة أقل.", + "fieldTextHost": "عنوان الاتصال", + "fieldTextHwaccel": "فك تشفير المحرك", + "fieldTextHwaccelVcodec": "فك تشفير المحرك", + "fieldTextInverseTrigger": "لتشغيل خارج المناطق المحددة. لن يؤدي إلى تمكين الكشف عن الإطار الكامل.", + "fieldTextIp": "المدى أو واحد", + "fieldTextIrCutFilterAuto": "يتم تنشيط مرشح CUT IR تلقائيًا بواسطة الجهاز.", + "fieldTextIrCutFilterOff": "تعطيل IR Cut Fiter. وضع الليل عادة.", + "fieldTextIrCutFilterOn": "تمكين الأشعة تحت الحمراء. وضع اليوم عادة.", + "fieldTextIsOnvif": "هل هذه كاميرا متوافقة مع onvif؟", + "fieldTextLang": "اللغة الأساسية لعناصر النص. للحصول على ترجمة كاملة ، أضف لغتك في conf.json على سبيل المثال: \"لغة\": \"EN_CA\" ، ", + "fieldTextLogDays": "عدد الأيام للحفاظ على السجلات قبل التطهير.", + "fieldTextLoglevel": "كمية البيانات التي يجب توفيرها أثناء القيام بالمهمة.", + "fieldTextLoglevelAllWarnings": "عرض جميع التحذيرات. استخدم هذا إذا لم تتمكن من معرفة الخطأ في الكاميرا.", + "fieldTextLoglevelFatal": "عرض أخطاء مميتة فقط.", + "fieldTextLoglevelOnError": "عرض جميع الأخطاء المهمة. ملاحظة: هذا لا يعرض دائمًا معلومات مهمة.", + "fieldTextLoglevelSilent": "لا أحد. هذا سوف يسيطر على كل قطع الأشجار.", + "fieldTextMail": "تسجيل الدخول للحسابات. سيحصل عنوان البريد الإلكتروني لحامل الحساب الرئيسي على إشعارات.", + "fieldTextMapRtspTransportAuto": "دع FFMPEG تقرر. عادة سيحاول UDP أولاً.", + "fieldTextMapRtspTransportTCP": "اضبطه على هذا إذا بدأ UDP في إعطاء نتائج غير مرغوب فيها.", + "fieldTextMapRtspTransportUDP": "FFMPEG يحاول هذا أولا.", + "fieldTextMaxKeepDays": "عدد الأيام التي يجب الاحتفاظ بها في مقاطع الفيديو قبل التطهير لهذا الشاشة على وجه التحديد.", + "fieldTextMid": "هذا هو معرف غير قابل للتغيير للشاشة. يمكنك تكرار الشاشة بالنقر المزدوج على معرف الشاشة وتغييره.", + "fieldTextMode": "هذه هي المهمة الأساسية للشاشة.", + "fieldTextModeDisabled": "شاشة غير نشطة ، لن يتم إنشاء أي عملية في هذا الوضع.", + "fieldTextModeRecord": "تسجيل مستمر. يتم تصنيع الأجزاء كل 15 دقيقة بشكل افتراضي.", + "fieldTextModeWatchOnly": "سيتم بث الشاشة فقط ، ولن يحدث أي تسجيل ما لم يتم طلب API أو الكاشف خلاف ذلك.", + "fieldTextMpass": "كلمة المرور للكاميرا الخاصة بك", + "fieldTextMuser": "تسجيل دخول المستخدم للكاميرا", + "fieldTextName": "هذا هو اسم العرض القابل للقراءة البشرية للشاشة.", + "fieldTextNotes": "التعليقات التي تريد مغادرتها لهذه الكاميرا.", + "fieldTextOnvifNonStandard": "هل هذه كاميرا غير قياسية؟", + "fieldTextOnvifPort": "عادة ما يتم تشغيل OnVIF على المنفذ 8000 . يمكن أن يكون هذا 80 وكذلك اعتمادًا على طراز الكاميرا.", + "fieldTextPass": "اترك فارغًا للحفاظ على نفس كلمة المرور أثناء تعديل الإعدادات.", + "fieldTextPasswordAgain": "يجب مطابقة حقل كلمة المرور إذا كنت ترغب في تغييره.", + "fieldTextPath": "الطريق إلى الكاميرا", + "fieldTextPort": "منفصل عن طريق الفواصل أو النطاق", + "fieldTextPortForce": "يمكن أن يسمح استخدام منفذ الويب الافتراضي بالتبديل التلقائي إلى منافذ أخرى لدارات مثل RTSP.", + "fieldTextPresetRecord": "العلم المسبق لبعض الترميزات الفيديو. إذا وجدت أن الكاميرا تتعطل كل بضع ثوانٍ: حاول تركها فارغة.", + "fieldTextPresetStream": "العلم المسبق لبعض الترميزات الفيديو. إذا وجدت أن الكاميرا تتعطل كل بضع ثوانٍ: حاول تركها فارغة.", + "fieldTextProbesize": "حدد مدى حجم مسبار التحليل للمدخلات. قم بتعيين 100000 إذا كنت تستخدم RTSP وتواجد مشكلات الدفق.", + "fieldTextProtocol": "البروتوكول الذي سيستخدم لاستهلاك دفق الفيديو.", + "fieldTextRecordScaleX": "عرض صورة الدفق.", + "fieldTextRecordScaleY": "ارتفاع صورة الدفق.", + "fieldTextRecordTimelapse": "إنشاء timelapse يعتمد على JPEG.", + "fieldTextRecordTimelapseMp4": "قم بإنشاء ملف MP4 في نهاية كل يوم ل timelapse.", + "fieldTextRecordTimelapseWatermark": "صورة يتم حرقها على إطارات الفيديو المسجل.", + "fieldTextRecordTimelapseWatermarkLocation": "موقع الصورة الذي سيتم استخدامه كعلامة مائية.", + "fieldTextRecordTimelapseWatermarkPosition": "صورة يتم حرقها على إطارات الفيديو المسجل.", + "fieldTextRotate": "قم بتغيير زاوية تسجيل دفق الفيديو.", + "fieldTextRtmpKey": "دفق مفتاح التدفقات الواردة على منفذ RTMP.", + "fieldTextRtspTransport": "بروتوكول النقل سوف تستخدم الكاميرا. TCP هو عادة الخيار الأفضل.", + "fieldTextRtspTransportAuto": "دع FFMPEG تقرر. عادة سيحاول UDP أولاً.", + "fieldTextRtspTransportHTTP": "طريقة الاتصال القياسية.", + "fieldTextRtspTransportTCP": "اضبطه على هذا إذا بدأ UDP في إعطاء نتائج غير مرغوب فيها.", + "fieldTextRtspTransportUDP": "FFMPEG يحاول هذا أولا.", + "fieldTextSfps": "حدد معدل الإطار (FPS) الذي توفر فيه الكاميرا دفقها.", + "fieldTextSignalCheck": "عدد المرات التي سيتحقق فيها عميلك من الدفق لمعرفة ما إذا كان على قيد الحياة. يتم حساب هذا في دقائق.", + "fieldTextSignalCheckLog": "هذا لجانب العميل فقط. سيتم عرضه في مؤشر ترابط السجل عند حدوث فحص إشارة جانب العميل.", + "fieldTextSize": "سوف يسمح كمية مساحة القرص Shinobi بالاستهلاك قبل التطهير. تتم قراءة هذه القيمة في Megabytes.", + "fieldTextSizeFilebinPercent": "في المئة من أقصى مبلغ تخزين يمكن أن يستخدمه أرشيف FileBin.", + "fieldTextSizeTimelapsePercent": "في المئة من الحد الأقصى للتخزين مبلغ إطارات timelapse يمكن أن تسجل.", + "fieldTextSizeVideoPercent": "في المئة من الحد الأقصى للتخزين مبلغ يمكن أن تسجل مقاطع الفيديو.", + "fieldTextSkipPing": "اختر ما إذا كانت هناك حاجة إلى نقطة انتشار ناجحة قبل بدء عملية الشاشة.", + "fieldTextSnap": "احصل على أحدث إطار في JPEG.", + "fieldTextSnapSecondsInward": "في ثوان", + "fieldTextSqllog": "استخدم هذا بحذر حيث يحب FFMPEG إلقاء بيانات غير ضرورية في بعض الأحيان والتي يمكن أن تؤدي إلى الكثير من صفوف قاعدة البيانات.", + "fieldTextSqllogNo": "لا هو الافتراضي.", + "fieldTextSqllogYes": "افعل هذا إذا كنت تواجه مشكلات متكررة فقط.", + "fieldTextStreamAcodec": "برامج الترميز الصوتي للبث.", + "fieldTextStreamAcodecAac": "تستخدم لفيديو mp4.", + "fieldTextStreamAcodecAc3": "تستخدم لفيديو mp4.", + "fieldTextStreamAcodecAuto": "دع FFMPEG يختار.", + "fieldTextStreamAcodecCopy": "تستخدم لفيديو mp4. يحتوي على استخدام منخفض للغاية لوحدة المعالجة المركزية ولكن بعض برامج ترميز الصوت تحتاج إلى أعلام مخصصة مثل -strict 2 لـ AAC.", + "fieldTextStreamAcodecLibmp3lame": "تستخدم لفيديو mp4.", + "fieldTextStreamAcodecLibopus": "تستخدم للفيديو webm.", + "fieldTextStreamAcodecLibvorbis": "تستخدم للفيديو webm.", + "fieldTextStreamAcodecNoAudio": "لا يوجد صوت ، هذا خيار يجب تعيينه في بعض أجزاء العالم لأسباب قانونية.", + "fieldTextStreamFlvType": "هذا هو لوحة القيادة Shinobi فقط. لا تزال كلتا طريقتان الدفق نشطة وجاهزة للاستخدام.", + "fieldTextStreamFps": "السرعة التي يتم بها عرض الإطارات للعملاء ، في إطارات في الثانية. كن على دراية لا يوجد افتراضي. هذا يمكن أن يؤدي إلى استخدام النطاق الترددي العالي.", + "fieldTextStreamLoop": "حلقة ملف ثابت بحيث يتصرف دفق الملف مثل دفق مباشر.", + "fieldTextStreamQuality": "عدد منخفض يعني جودة أعلى. العدد الأعلى يعني أقل جودة.", + "fieldTextStreamRotate": "قم بتغيير زاوية عرض دفق الفيديو.", + "fieldTextStreamScaleX": "عرض صورة الدفق التي يتم إخراجها بعد المعالجة.", + "fieldTextStreamScaleY": "ارتفاع صورة الدفق التي يتم إخراجها بعد المعالجة.", + "fieldTextStreamTimestamp": "ساعة يتم حرقها على إطارات دفق الفيديو.", + "fieldTextStreamTimestampBoxColor": "Timstamp Backdrop Color.", + "fieldTextStreamTimestampColor": "لون النص timstamp.", + "fieldTextStreamTimestampFont": "ملف الخط لأسلوب الطابع الزمني الخاص بك.", + "fieldTextStreamTimestampFontSize": "حجم الخط في حزب العمال.", + "fieldTextStreamTimestampX": "الموقف الأفقي من الطابع الزمني", + "fieldTextStreamTimestampY": "الموقف الرأسي من الطابع الزمني", + "fieldTextStreamType": "الطريقة التي سيتم استخدامها لاستهلاك دفق الفيديو.", + "fieldTextStreamTypeBase64OverWebsocket": "إرسال إطارات مشفرة BASE64 عبر WebSocket. هذا يتجنب التخزين المؤقت ولكن لا يوجد صوت.", + "fieldTextStreamTypeFLV": "إرسال إطارات مشفرة FLV عبر WebSocket.", + "fieldTextStreamTypeHLS(includesAudio)": "طريقة مماثلة لتدفقات Facebook Live. يتضمن الصوت إذا كان الإدخال يوفره. هناك تأخير من حوالي 4-6 ثوان لأن هذه الطريقة تسجل شرائح ثم يدفعها إلى العميل بدلاً من الدفع كما تنشئها.", + "fieldTextStreamTypeMJPEG": "صورة قياسية JPEG صورة. لا صوت.", + "fieldTextStreamTypePoseidon": "تم بناء Poseidon على رمز معالجة MP4 من Kevin Godell. يحاكي ملف MP4 المتداول ولكن باستخدام بيانات الدفق المباشر. يشمل الصوت. يمكن لبعض المتصفحات تشغيله مثل ملف MP4 عادي. تدفقات فوق HTTP أو WebSocket.", + "fieldTextStreamVcodec": "برنامج ترميز الفيديو للدفق.", + "fieldTextStreamVcodecAuto": "دع FFMPEG يختار.", + "fieldTextStreamVcodecCopy": "تستخدم لفيديو mp4. يحتوي على استخدام منخفض للغاية لوحدة المعالجة المركزية ولكن لا يمكن استخدام مرشحات الفيديو والملفات قد يكون عملاقًا. من الأفضل إعداد إعدادات إعدادات MP4 الخاصة بك عند استخدام هذا الخيار.", + "fieldTextStreamVcodecLibx264": "تستخدم لفيديو mp4.", + "fieldTextStreamVcodecLibx265": "تستخدم لفيديو mp4.", + "fieldTextStreamVf": "ضع مرشحات الفيديو FFMPEG في هذا المربع للتأثير على جزء البث. بدون مسافات.", + "fieldTextStreamWatermark": "صورة يتم حرقها على إطارات دفق الفيديو.", + "fieldTextStreamWatermarkLocation": "موقع الصورة الذي سيتم استخدامه كعلامة مائية.", + "fieldTextStreamWatermarkPosition": "صورة يتم حرقها على إطارات دفق الفيديو.", + "fieldTextTimestamp": "ساعة يتم حرقها على إطارات الفيديو المسجل.", + "fieldTextTimestampBoxColor": "Timstamp Backdrop Color.", + "fieldTextTimestampColor": "لون النص timstamp.", + "fieldTextTimestampFont": "ملف الخط لأسلوب الطابع الزمني الخاص بك.", + "fieldTextTimestampFontSize": "حجم الخط في حزب العمال.", + "fieldTextTimestampX": "الموقف الأفقي من الطابع الزمني", + "fieldTextTimestampY": "الموقف الرأسي من الطابع الزمني", + "fieldTextTvChannel": "سيكون لهذا الشاشة ميزات قناة تلفزيونية ممكّنة. ستتمكن من مشاهدته في قائمة قناتك التلفزيونية.", + "fieldTextTvChannelGroupTitle": "مجموعة مخصصة للقناة.", + "fieldTextTvChannelId": "معرف مخصص للقناة.", + "fieldTextType": "الطريقة التي سيتم استخدامها لاستهلاك دفق الفيديو.", + "fieldTextTypeDashcam(StreamerV2)": "دفق P2P المستند إلى WebSocket.", + "fieldTextTypeH.264/H.265/H.265+": "قراءة مقطع فيديو عالي الجودة تتضمن في بعض الأحيان الصوت.", + "fieldTextTypeHLS(.m3u8)": "قراءة مقطع فيديو عالي الجودة تتضمن في بعض الأحيان الصوت.", + "fieldTextTypeJPEG": "قراءة لقطات من عنوان URL وصنع دفق و/أو فيديو منها.", + "fieldTextTypeLocal": "قراءة بطاقات الالتقاط أو كاميرات الويب أو الكاميرات المتكاملة.", + "fieldTextTypeMJPEG": "على غرار JPEG باستثناء معالجة الإطار يتم بواسطة FFMPEG ، وليس شينوبي.", + "fieldTextTypeMPEG4(.mp4/.ts)": "ملف ثابت. اقرأ بمعدل أقل ويجب عدم استخدامه في البث المباشر الفعلي.", + "fieldTextTypeMxPEG": "Mobotix MJPEG تيار", + "fieldTextTypeRTMP": "تعلم الاتصال هنا: المقالة: كيفية دفع التدفقات عبر RTMP إلى Shinobi ", + "fieldTextTypeShinobiStreamer": "WebSocket B2P Stream المستند إلى JPEG.", + "fieldTextVcodec": "برنامج ترميز الفيديو للتسجيل.", + "fieldTextVf": "ضع مرشحات الفيديو FFMPEG في هذا المربع للتأثير على جزء التسجيل. بدون مسافات.", + "fieldTextWallClockTimestampIgnore": "بناء جميع بيانات الكاميرا الواردة في وقت الكاميرا بدلاً من وقت الخادم.", + "fieldTextWatchdogReset": "إذا كان هناك تداخل في سجل الزناد في حالة إعادة تعيينه.", + "fieldTextWatermark": "صورة يتم حرقها على إطارات الفيديو المسجل.", + "fieldTextWatermarkLocation": "موقع الصورة الذي سيتم استخدامه كعلامة مائية.", + "fieldTextWatermarkPosition": "صورة يتم حرقها على إطارات الفيديو المسجل.", + "fieldTextWidth": "عرض صورة الدفق.", + "fire hydrant": "صنبور الاطفاء", + "flv": "FLV", + "for Global Access": "من أجل الوصول العالمي", + "fork": "شوكة", + "frisbee": "الطبق الطائر", + "getAMonitor": "احصل على شاشة", + "getATvChannel": "احصل على قنوات تلفزيونية للشاشة", + "getATvChannelText": "احصل على تدفقات H.264 المتاحة لشاشة واحدة في قائمة تشغيل .m3u8.", + "getAllMonitors": "احصل على جميع الشاشات", + "getAllTvChannels": "احصل على جميع القنوات التلفزيونية", + "getAllTvChannelsText": "الحصول على جميع تدفقات H.264 في قائمة تشغيل .m3u8. قم بتمكين خيار قناة التلفزيون في إعدادات الشاشة لمشاهدة تدفقاتها في هذه القائمة.", + "getUserInfo": "الحصول على معلومات المستخدم", + "getVideos": "احصل على مقاطع فيديو", + "getVideosForMonitor": "احصل على مقاطع فيديو للشاشة", + "giraffe": "زرافة", + "h264_cuvid": "H.264 Cuvid", + "h264_mmal": "H.264 (Raspberry Pi)", + "h264_nvenc": "H.264 Nvenc (Nvidia HW Accel)", + "h264_omx": "H.264 OpenMax (Raspberry PI)", + "h264_qsv": "H.264 (فيديو مزامنة سريع)", + "h264_vaapi": "H.264 VA-API (Intel HW Accel)", + "h265BrowserText1": "إذا كنت تحاول تشغيل ملف H.265 ، فقد تحتاج إلى تنزيله وفتحه في تطبيق آخر مثل VLC.", + "hair drier": "مجفف الشعر", + "handbag": "حقيبة يد", + "hevc_cuvid": "H.265 Cuvid", + "hevc_nvenc": "H.265 Nvenc (Nvidia HW Accel)", + "hevc_qsv": "H.265 (فيديو مزامنة سريع)", + "hevc_vaapi": "H.265 VA-API (Intel HW Accel)", + "hlsOptions": "خيارات HLS", + "hlsOptionsInvalid": "خيارات HLS غير صالحة", + "horse": "حصان", + "hot dog": "نقانق", + "hour": "ساعة", + "hours": "ساعات", + "hwaccel": "محرك التسارع", + "hwaccel_device": "جهاز Hwaccel", + "hwaccel_vcodec": "فك تشفير الفيديو", + "in": "في", + "in Days": "في الايام", + "in seconds": "في ثوان", + "keyId": "المعرف الرئيسي", + "keyboard": "لوحة المفاتيح", + "kite": "طائرة ورقية", + "knife": "سكين", + "laptop": "حاسوب محمول", + "lastLogin": "آخر تسجيل دخول", + "libmp3lame": "libmp3lame", + "libopus": "libopus", + "libvorbis (Default)": "libvorbis (الافتراضي)", + "libvpx (Default)": "libvpx (الافتراضي)", + "libvpx-vp9": "libvpx-vp9", + "libx264": "libx264", + "libx264 (Default)": "libx264 (الافتراضي)", + "libx265": "libx265", + "liveGridDescription": "Live Grid هي شاشة الدفق المتعددة لشينوبي. تم تصميم طريقة المشاهدة هذه بشكل أساسي لسطح المكتب.", + "loginHandleUnbound": "لقد تم إلغاء ارتباط تسجيل الدخول من هذا الحساب.", + "microwave": "الميكروويف", + "migrateText1": " نوع الإدخال لا يمكن تحليله. يرجى تعيينه يدويًا.", + "minute": "اللحظة", + "minutes": "دقائق", + "mjpeg_cuvid": "MJPEG CUVID", + "modifyVideoText1": "طريقة لا وجود لها. تحقق للتأكد من أن آخر قيمة URL ليست فارغة.", + "monSavedButNotCopied": "تم حفظ شاشتك ولكن لم يتم نسخها إلى أي شاشة أخرى.", + "monitorConfigFinderDescription": "ستساعدك هذه الأداة في البحث عن تكوينات للكاميرات التي نشرها المجتمع. جميع المستضافة على shinobihub . يمكنك نشرك أيضًا ، فسيساعد المجتمع حقًا :)", + "monitorEditFailedMaxReached": "حسابك قد بلغ أقصى عدد من الكاميرات التي يمكن إنشاؤها. التحدث إلى مسؤول إذا كنت ترغب في تغيير هذا.", + "monitorEditText1": "بيانات غير صالحة ، تحقق لمعرفة هذا هو صالح استيراد السلسلة.", + "monitorEditText2": "غير صالحة تفاصيل السلسلة. تحقق لمعرفة ما هو سلسلة JSON و غير منتظم وجوه يتم تمريرها.", + "monitorGetText1": "الطلب غير المكتمل ، وإزالة الماضي مائل في URL أو وضع مقبول القيمة.", + "monitorStateNotEnoughChanges": "تحتاج إلى إجراء تغيير في تكوين الشاشة الخاص بك قبل محاولة إضافته إلى إعداد مسبق.", + "monitorStatesError": "مراقبة خطأ الإعدادات المسبقة", + "months": "أشهر", + "motorcycle": "دراجة نارية", + "mouse": "الفأر", + "mpeg2_mmal": "MPEG-2 (Raspberry PI)", + "mpeg2_qsv": "MPEG2 (فيديو مزامنة سريع)", + "mpeg4_cuvid": "MPEG4 Cuvid", + "mpeg4_mmal": "MPEG-4 (Raspberry PI)", + "noLoginTokensAdded": "لا توجد تسجيلات دخول بديلة مرتبطة بهذا الحساب.", + "noSpecialCharacters": "بدون مسافات أو أحرف خاصة.", + "noTriggerText": "إذا لم يتم اكتشاف الحركة بعد فترة المهلة ، فسوف تتلقى إخطارًا بالخلاف.", + "noUndoForAction": "لا يمكنك التراجع عن هذا الإجراء.", + "notActivatedText": "لقد فشل التثبيت في التنشيط.", + "notEnoughFramesText1": "لا توجد إطارات كافية للتجميع.", + "notPermitted1": "هذا الإجراء لا يسمح به مسؤول حسابك.", + "on": "على", + "on Error": "على خطأ", + "on Event": "على الحدث", + "onvifdeviceManagerGlobalTip": "يسمح OnVIF بتعديل الإعدادات الداخلية للكاميرا. Onvif هو إلى حد ما من مصطلح مظلة ، يمكن أن يعني الكثير من الأشياء للأسف. مع هذا هو الحال ، قد ترى خيارًا في هذه الأداة ولكن قد لا يكون قابلاً للتحرير. هذا عادةً لأن بائع الكاميرا لم يضف هذه الطريقة أو انحرفت عن استخدامه المقصود. في هذه الحالات ، ستحتاج إلى إدخال تكوين الكاميرا من خلال الطريقة المحددة لبائع الكاميرا ، وهذا يفتح عمومًا عنوان IP للكاميرا في متصفح الويب الخاص بك.", + "onvifdeviceSavedFoundErrorText": "قد تكون بعض الإعدادات قد عادت إلى قيمة سابقة. من الممكن أن يكون الخيار المعدل غير متوفر مع هذه الكاميرا من خلال ONVIF.", + "onvifdeviceSavedText": "تم حفظ الإعدادات الداخلية للكاميرا. قد تحتاج إلى إعادة تشغيل الكاميرا لتصبح هذه التغييرات سارية المفعول.", + "openImagesDownloadConfirm": "هل أنت متأكد من أنك ترغب في بدء تنزيل الصور والمربعات المحيطة (المصفوفات المسبقة) من OpenImages؟", + "openImagesDownloadConfirmStop": "هل أنت متأكد أنك تريد التوقف عن التدريب؟", + "opencl": "opencl", + "opencvCascadesText": "إذا لم ترى شيئًا هنا ، فما عليك سوى تنزيل هذه الحزمة من cascades . قم بإسقاطها إلى Plugins/OpenCV/Cascades ثم اضغط على Refresh .", + "orange": "البرتقالي", + "oven": "فرن", + "p2pServerNotSelectedText": "حدد خادمًا من القائمة واضغط على حفظ. انتظر 10 ثوان ثم حاول فتح لوحة القيادة عن بعد.", + "p2pSettingsText1": "ستحتاج إلى تحديث هذه الصفحة لتطبيق التغييرات.", + "parking meter": "عداد موقف السيارات", + "performanceOptimizeText1": "توفر الكاميرا الخاصة بك بيانات دفق H.264. يمكنك تعيين نوع الدفق على HLS و Poseidon وبرنامج ترميز الفيديو لنسخه.", + "person": "شخص", + "pizza": "بيتزا", + "possibleInternalError": "خطأ داخلي ممكن", + "postDataBroken": "تحقق من تنسيق JSON. تأكد من أنه تم تشييده وتحديده تحت \"البيانات\"", + "potted plant": "النبات المحفوظ بوعاء", + "powerVideoEventLimit": "لقد حددت حد الحدث العالي. هل أنت متأكد أنك تريد تقديم هذا الطلب؟", + "privateKey": "مفتاح سري", + "qsv": "QSV", + "rebootingCamera": "إعادة تشغيل الكاميرا", + "refrigerator": "ثلاجة", + "remote": "التحكم عن بعد", + "restartRequired": "إعادة تشغيل Core Shinobi مطلوب للتغييرات لتسريع.", + "sandwich": "ساندويتش", + "scissors": "مقص", + "separateByCommasOrRange": "منفصل عن طريق الفواصل أو النطاق", + "setMaxStorageAmountText": "يجب عليك تعيين مبلغ تخزين Max في إعدادات حسابك الموجودة على اليسار. ابحث عن الخيار ضمن قسم الملف الشخصي. الافتراضي هو 10 جيجابايت.", + "sheep": "خروف", + "sink": "حوض", + "sizePurgeLockedText": "يبدو أن قفل تطهير حجم (DeleteOverMax) فشل في فتحه. فتح الآن ...", + "skateboard": "لوح تزلج", + "skipPingText1": "حاول تعيين \"تخطي ping\" إلى نعم.", + "skis": "الزلاجات", + "snowboard": "لوح التزلج", + "sorryNo": "اسف لا", + "sorryNothingWasFound": "آسف ، لم يتم العثور على شيء.", + "spoon": "ملعقة", + "sports ball": "كرة رياضية", + "startUpText0": "حجم التحقق من أشرطة الفيديو", + "startUpText1": "الغاية من حجم التحقق من أشرطة الفيديو", + "startUpText2": "جميع المستخدمين فحص الانتظار لإغلاق الملفات المفتوحة وإزالة الملفات على حد المستخدم", + "startUpText3": "في انتظار أن تعطي لم تنته الفيديو تحقق بعض الوقت. 3 ثوان.", + "startUpText4": "بدأت كل مجموعة شاشات لمشاهدة وتسجيل", + "startUpText5": "شينوبي جاهز.", + "startUpText6": "تم العثور على مقاطع الفيديو اليتيمة وإدراجها", + "stop sign": "لافتة توقف", + "subAccountManager": "مدير الحساب الفرعي", + "substreamConnectionText": "يمكنك ترك تفاصيل الاتصال كما هو الحال إذا كنت تريد استخدام معلومات الاتصال الرئيسية المذكورة أعلاه.", + "substreamOutputText": "هنا يمكنك تعيين تكوين الدفق عند الطلب. تعرف على الكمون من أنواع الدفق هنا. ", + "substreamText": "هذه طريقة عند الطلب لعرض البث المباشر. يمكنك القيام بذلك حتى تتوفر عملية المشاهدة فقط عندما يراقب شخص ما أو لاستخدامه للتبديل بين الدقة المنخفضة والعالية.", + "suitcase": "حقيبة سفر", + "superAdminText": "\"السوبر.سلمان\" لا وجود لها. الرجاء إعادة تسمية \"سوبر.العينة.سلمان\" إلى \"السوبر.سلمان\".", + "superAdminTitle": "شينوبي : المشرف المميز", + "surfboard": "لوح ركوب الأمواج", + "teddy bear": "دمية دب", + "tennis racket": "مضرب التنس", + "tie": "رَابِطَة", + "toaster": "محمصة", + "toilet": "الحمام", + "tokenNotUserBound": "لا يرتبط مقبض تسجيل الدخول هذا بمستخدم على هذا الخادم!", + "tokenNotUserBoundPt2": "اكتب بيانات الاعتماد الخاصة بك ثم استخدم زر تسجيل الدخول إلى Google للربط بسرعة.", + "toothbrush": "فرشاة الأسنان", + "total": "مجموع", + "traffic light": "إشارة المرور", + "train": "قطار", + "truck": "شاحنة", + "tv": "تلفزيون", + "umbrella": "مظلة", + "undefined": "غير معرف", + "undoAllUnsaveChanges": "هل انت متأكد من أنك تريد أن تفعل هذا؟ هذا سوف التراجع عن جميع التغييرات غير المحفوظة.", + "unexpectedExitText": "سيتم العثور على معلومات حول هذا المخرج قبل هذا السجل. بالإضافة إلى ذلك ، هنا أمر FFMPEG الذي تم استخدامه عندما تحطمت العملية.", + "updateCamerasInternalSettings": "تحديث الإعدادات الداخلية للكاميرا؟", + "updateKeyText1": "\"updateKey\" مفقود من \"conf.json\", لا يمكن أن تفعل التحديثات هذه الطريقة حتى يمكنك إضافته.", + "updateKeyText2": "\"updateKey\" غير صحيحة.", + "updateNotice1": "تحديث Shinobi يعني ملفات الكتابة فوق. إذا قمت بتعديل أي ملفات بنفسك ، فيجب عليك تحديث Shinobi يدويًا. لن يتم تعديل التكوينات وملفات الفيديو الخاصة بك.", + "useSubStreamOnlyWhenWatching": "فقط عند المشاهدة ، استخدم Substream", + "vaapi": "VAAPI (VA-API)", + "vase": "مزهرية", + "vda": "VDA (تسريع أجهزة Apple VDA)", + "vdpau": "VDPAU", + "videoBuildingText1": "الفيديو بناء حاليا. تحقق مرة أخرى لاحقًا.", + "videotoolbox": "Videotoolbox", + "vp8_cuvid": "VP8 NVenc (Nvidia HW Accel)", + "vp8_qsv": "VP8 (فيديو مزامنة سريع)", + "vp9_cuvid": "VP9 Nvenc (Nvidia HW Accel)", + "wannaReset": "هل تريد إعادة التعيين؟", + "willTriggerAnEvent": "سوف يؤدي إلى حدث", + "wine glass": "كأس نبيذ", + "years": "سنوات", + "zebra": "الحمار الوحشي" +} \ No newline at end of file diff --git a/languages/cs.json b/languages/cs.json new file mode 100644 index 00000000..0a035267 --- /dev/null +++ b/languages/cs.json @@ -0,0 +1,1692 @@ +{ + "\"No Motion\" Detector": "Detektor „žádný pohyb“", + "# of Allow MJPEG Clients": "# Povolit MJPEG Klienti 0 pro nekonečné ", + "'Already Installing...'": "\"Již nainstaluji ... '", + "180 Degrees": "180 stupňů", + "2-Factor Authentication": "Ověřování 2-faktorů", + "90 Clockwise": "90 ve směru hodinových ručiček", + "90 Clockwise and Vertical Flip": "90 ve směru hodinových ručiček a vertikální převrácení", + "90 Counter Clockwise and Vertical Flip (default)": "90 proti směru hodinových ručiček a vertikální flip (výchozí)", + "AND": "A", + "API": "API", + "API Key": "Klíč API", + "API Key Action Failed": "Akce klíče API selhala", + "API Key Added": "Přidán klíč API", + "API Key Deleted": "Smazáno klíč API", + "API Keys": "Klávesy API", + "APIKeyAddedText": "Tento klíč můžete použít nyní.", + "APIKeyDeletedText": "Klíč byl odstraněn. Už to nebude fungovat.", + "ASC": "ASC", + "Accelerator": "Plynový pedál", + "Account Edited": "Účet upraven", + "Account Info": "Informace o účtu", + "Account Information": "Informace o účtu", + "Account Privileges": "Oprávnění účtu", + "Account Save": "Uložení účtu", + "Account Settings": "Nastavení účtu", + "AccountEditText1": "Nemohl upravit. Obnovit stránka, pokud problém pokračuje.", + "Accounts": "Účty", + "Accuracy Mode": "Režim přesnosti", + "Action for Selected": "Akce pro vybrané", + "Activated": "Aktivováno", + "Active Monitors": "Aktivní monitory", + "Add": "Přidat", + "Add All": "Přidat vše", + "Add Camera": "Přidejte kameru", + "Add Cameras": "Přidejte kamery", + "Add Channel": "Přidat kanál", + "Add Input Feed": "Přidejte vstupní zdroj", + "Add Map": "Přidejte mapu", + "Add Monitor": "Přidat monitor", + "Add New": "Přidat nový", + "AddToPreset": "Přidat k předvolbě", + "Additional Inputs": "Další vstupy", + "Admin": "Admin", + "Advanced": "Pokročilý", + "After": "Po", + "Again": "Znovu", + "Age": "Stáří", + "Alert Sound": "Varovný zvuk", + "Alert Sound Delay": "Upozornění zpoždění zvuku", + "All Logs": "Všechny protokoly", + "All Monitors": "Všechny monitory", + "All Monitors and Privileges": "Všechny monitory a privilegia", + "All Privileges": "Všechna privilegia", + "All Warnings": "Všechna varování", + "All streams in first feed": "Všechny potoky v prvním krmivu", + "Allow API Trigger": "Povolte spoušť API", + "Allow Next Alert": "Povolit další upozornění", + "Allow Next Command": "Povolit další příkaz", + "Allow Next Discord Alert": "Povolte další výstrahu na Discord v minutách ", + "Allow Next Email": "Povolte další e -mail v minutách ", + "Allow Next Trigger": "Povolte další spoušť", + "Allow Next Webhook": "Povolte další webhook", + "Allowed IPs": "Povoleno IPS", + "Already Processing": "Již zpracování", + "Already exists": "Již existuje", + "Alternate Logins": "Alternativní přihlášení", + "Always": "Vždy", + "Amazon S3": "Amazon S3", + "Amazon S3 Upload Error": "Chyba nahrávání Amazon S3", + "Analyzation Duration": "Analyzační doba trvání", + "AppNotEnabledText": "Aplikace není povolena, povolte ji v nastavení vašeho účtu.", + "April": "duben", + "Archive": "Archiv", + "Are you sure?": "Jsi si jistá?", + "Attach Snapshot": "Připojte snímek", + "Attach Video Clip": "Připojte videoklip", + "Audio": "Zvuk", + "Audio Bit Rate": "Zvuková bitová rychlost", + "Audio Codec": "Zvukový kodek", + "Audio Detection": "Detekce zvuku", + "Audio Detector": "Audio detektor", + "Audio stream only from first feed": "Zvukový proud pouze z prvního krmiva", + "Audio streams only": "Pouze zvukové proudy", + "August": "srpen", + "Authenticate": "Ověřit", + "Authenticated": "Ověřeno", + "Authentication Failed": "Ověření se nezdařilo", + "Auto": "Auto", + "Automatic": "Automatický", + "Automatic Checking Cancelled": "Automatická kontrola zrušena", + "Automatic Codec Repair": "Automatická oprava kodeku", + "Automatic Field Fill": "Automatická výplň pole", + "Autosave": "Autosave", + "Back": "Zadní", + "Backblaze B2": "Backblaze B2", + "Backblaze Error": "Chyba zpětného blokování", + "BacklightCompensation": "Kompenzace podsvícení", + "Backup": "Záloha", + "Bandwidth": "Šířka pásma", + "Base64 over Websocket": "Base64 nad WebSocket", + "Basic Authentication": "Základní ověřování", + "Batch": "Dávka", + "Batch Download": "Stahování dávek", + "Before": "Před", + "Bind Credentials": "Vázat přihlašovací údaje (heslo)", + "BitrateLimit": "Limit bitrate", + "Blank for No Change": "Prázdné bez změny", + "Bottom Left": "Vlevo dole", + "Bottom Right": "Vpravo dole", + "Brightness": "Jas", + "Browser Console Log": "Protokol konzoly prohlížeče", + "Bucket": "Kbelík", + "Buffer Preview": "Náhled vyrovnávací paměti", + "Buffer Time from Event": "Čas vyrovnávací paměti z události", + "Build": "Stavět", + "Build Video": "Sestavit video", + "Building": "Budova", + "CPU": "procesor", + "CPU indicator will not work. Continuing...": "Indikátor CPU nebude fungovat. Pokračování ...", + "CPU used by this stream": "CPU používaný tímto proudem", + "CSS": "CSS Style váš řídicí panel. ", + "Calendar": "Kalendář", + "Call Method": "Metoda volání", + "Camera Password": "Heslo kamery", + "Camera Username": "Uživatelské jméno kamery", + "Camera is not recording": "Fotoaparát nezaznamenává", + "Camera is not running": "Kamera neběží", + "Camera is not streaming": "Kamera ne streamuje", + "CameraNotRecordingText": "Nastavení může být nekompatibilní. Zkontrolujte kodéry. Restartování ...", + "Can Authenticate Websocket": "Může ověřit WebSocket", + "Can Change User Settings": "Může změnit nastavení uživatele", + "Can Control Monitors": "Mohou ovládat monitory", + "Can Create and Delete Monitors": "Mohou vytvářet a smazat monitory", + "Can Delete Videos": "Může smazat videa", + "Can Delete Videos and Events": "Může smazat videa a události", + "Can Edit Monitor": "Může upravit monitor", + "Can Get Logs": "Může získat protokoly", + "Can Get Monitors": "Může získat monitory", + "Can View Logs": "Můžete zobrazit protokoly", + "Can View Monitor": "Může zobrazit monitor", + "Can View Snapshots": "Může zobrazit snímky", + "Can View Streams": "Může zobrazit proudy", + "Can View Videos": "Může zobrazit videa", + "Can View Videos and Events": "Může zobrazit videa a události", + "Can edit Max Days": "Může upravit maximální dny", + "Can edit Max Storage": "Může upravit maximální úložiště", + "Can edit how long to keep Events": "Může upravit, jak dlouho udržovat události", + "Can edit how long to keep Logs": "Může upravit, jak dlouho udržovat protokoly", + "Can use Admin Panel": "Může použít administrativní panel", + "Can use Amazon S3": "Může použít Amazon S3", + "Can use Discord Bot": "Může použít Bot Discord", + "Can use LDAP": "Může použít LDAP", + "Can use SFTP": "Může použít SFTP", + "Can use Wasabi Hot Cloud Storage": "Může použít úložiště horkého cloudu Wasabi", + "Can use WebDAV": "Může použít WebDav", + "Can't Connect": "Nelze se připojit", + "Cannot watch a monitor that isn't running.": "Nelze sledovat monitor, který není spuštěn.", + "Cards": "Karty", + "Carousel in Background": "Kolotoč v pozadí", + "Center": "Centrum adresa URL ", + "Channel": "Kanál", + "Channel ID": "ID kanálu", + "Chat on Discord": "Chat on Discord", + "Check": "Šek", + "Check Signal Interval": "Zkontrolujte interval signálu v minutách ", + "Check for Motion First": "Nejprve zkontrolujte pohyb", + "Check the Channel ID": "Zkontrolujte ID kanálu", + "Check the Recipient ID": "Zkontrolujte ID příjemce", + "Clear": "Průhledná", + "Clear Recorder Process": "Proces jasného zapisovače", + "Clock Format": "Formát hodin", + "Close": "Zavřít", + "Close All Monitors": "Zavřete všechny monitory", + "Closed": "ZAVŘENO", + "Cloud": "Mrak", + "Codec Mismatch": "Neshoda kodeku", + "Color Threshold": "Práh barev", + "ColorSaturation": "Nasycení barev", + "Command": "Příkaz", + "Command on Trigger": "Příkaz na spouště", + "Common Objects": "Běžné objekty", + "Complete Stream URL": "Kompletní streamovací adresa URL", + "Conditions": "Podmínky", + "Confidence": "Důvěra", + "Confidence of Detection": "Důvěra detekce", + "Configuration": "Konfigurace", + "Confirm": "Potvrdit", + "Connected": "Připojeno", + "Connected Users": "Připojené uživatelé", + "Connection": "Spojení", + "Connection Type": "Typ připojení", + "Connection timed out": "Připojení vypršelo", + "Contains": "Obsahuje", + "Contrast": "Kontrast", + "Control": "Řízení", + "Control Error": "Chyba kontroly", + "Control Trigger Ended": "Kontrolní spoušť skončil", + "Control Trigger Started": "Spouštěč ovládání začal", + "Control Triggered": "Kontrola spuštěna", + "ControlErrorText1": "Ovládání není povoleno", + "ControlErrorText2": "Zkontrolujte údaje o připojení. Možná budete muset nasměrovat základní adresu URL na portu 8000 nebo 80. Zkontrolujte informace o ověření.", + "Controllable": "Kontrolovatelné", + "Controls and Logs": "Ovládací prvky a protokoly", + "Copied": "Zkopírováno", + "Copied to Clipboard": "Zkopírováno do schránky", + "Copy": "kopírovat", + "Copy Connection Settings": "Kopírovat nastavení připojení", + "Copy Custom Settings": "Zkopírujte vlastní nastavení", + "Copy Detector Settings": "Nastavení detektoru kopírování", + "Copy Group Settings": "Nastavení skupiny kopírování", + "Copy Input Settings": "Zkopírujte nastavení vstupu", + "Copy JPEG API Settings": "Zkopírujte nastavení API JPEG", + "Copy Logging Settings": "Zkopírujte nastavení protokolování", + "Copy Mode": "Kopírovat režim", + "Copy Recording Settings": "Kopírovat Nastavení záznamu", + "Copy Remote Link": "Zkopírujte vzdálený odkaz", + "Copy Settings": "Nastavení kopírování", + "Copy Stream Channel Settings": "Kopírovat nastavení kanálu proudu", + "Copy Stream Channels": "Kopírovat kanály proudu", + "Copy Stream Settings": "Kopírovat nastavení proudu", + "Copy Stream URL": "URL stream kopírovat", + "Copy Timelapse Settings": "Zkopírujte nastavení timelapse", + "Copy to Selected Monitor(s)": "Zkopírujte vybraný monitor (monitor)", + "Copy to Settings": "Zkopírovat do nastavení", + "Cores": "Jádra", + "Could not create Bucket.": "Nemohl vytvořit kbelík.", + "Count Objects": "Počet objektů", + "Count Objects only inside Regions": "Počítejte objekty pouze uvnitř regionů", + "Country of Plates": "Země desek", + "Counts of Motion": "Počty pohybu", + "Create Sub-Accounts at /admin": "Vytvořte dílčí účty na /admin", + "Created": "Vytvořeno", + "Creating New Account": "Vytváření nového účtu", + "Creation Interval": "Interval stvoření", + "Current": "Proud", + "Currently Active": "Aktuálně aktivní", + "Currently viewing": "V současné době sleduje", + "Custom": "Zvyk", + "Custom Auto Load": "Vlastní automatické zatížení", + "Custom Base URL": "Zakázková základna URL Ponechte prázdné pro použití URL HOST ", + "Custom Endpoint": "Vlastní koncový bod", + "DB Lost.. Retrying..": "Ztracená databáze .. Opakování ..", + "DESC": "Desc", + "DHCP": "DHCP", + "DNS": "DNS", + "Daily Events": "Denní události", + "Dashboard": "Přístrojová deska", + "Dashboard Language": "Jazyk řídicího panelu", + "Dashcam": "Dashcam", + "Dashcam (Streamer v2)": "Dashcam (Streamer V2)", + "Database": "Databáze", + "Database Not Found": "Databáze nebyla nalezena", + "Database row does not exist": "Řádek databáze neexistuje", + "Date": "datum", + "Date Added": "datum přidáno", + "Date Range": "Časové období", + "Date Updated": "Aktualizováno datum", + "Date and Time": "Datum a čas", + "DateTimeType": "Správa data", + "DaylightSavings": "Denní osvětlení", + "Days": "Dny", + "Debug": "Ladit", + "December": "prosinec", + "Default": "Výchozí", + "Delay for Snapshot": "Zpoždění pro snímky", + "Delete": "Vymazat", + "Delete Camera": "Odstranit kameru", + "Delete Filter": "Odstranit filtr", + "Delete Logs": "Odstranit protokoly", + "Delete Matches": "Odstranit zápasy", + "Delete Monitor": "Odstranit monitor", + "Delete Monitor State?": "Smazat stav monitoru", + "Delete Monitor States Preset": "Předvolba stavů smazat monitor", + "Delete Monitors and Files": "Odstranit monitory a soubory", + "Delete Motionless Video": "Odstraňte nehybné video", + "Delete Motionless Videos (For Record Mode)": "Odstraňte nehybné videa (pro režim záznamu)", + "Delete Region": "Odstranit oblast", + "Delete Schedule": "Odstranit plán", + "Delete Selected Videos": "Odstranit vybraná videa", + "Delete Timelapse Frame": "Smazat rámeček Timelapse", + "Delete Video": "Odstranit video", + "Delete Videos": "Odstranit videa", + "Delete selected": "Smazat vybrané", + "DeleteMonitorText": "Chcete tento monitor smazat? Nemůžete to obnovit. Můžete si vybrat, zda soubory zůstanou v souborovém systému. Pokud se rozhodnete znovu vytvořit monitor se stejným ID, videa a události budou na palubní desce viditelné.", + "DeleteMonitorsText": "Chcete tyto monitory odstranit? Nemůžete je obnovit. Můžete se rozhodnout ponechat soubory pro tyto IDS v souborovém systému. Pokud se rozhodnete znovu vytvořit monitor s jedním z IDS, videa a události budou na palubní desce viditelné.", + "DeleteSelectedVideosMsg": "Chcete tato videa odstranit? Nemůžete je obnovit.", + "DeleteTheseMsg": "Chcete je odstranit? Nemůžete je obnovit.", + "DeleteThisMsg": "Chcete to smazat? Nemůžete to obnovit.", + "DeleteVideoMsg": "Chcete toto video smazat? Nemůžete to obnovit.", + "Deleted": "Smazáno", + "Deleted Schedule Configuration": "Smazaná konfigurace plánu", + "Deleted State Configuration": "Smazaná konfigurace stavu", + "Detect Objects": "Detekovat objekty viz níže ", + "Detection": "Detekce", + "Detection Engine": "Detekční motor", + "Detection Event": "Detekční událost", + "Detector": "Detektor", + "Detector Buffer": "Pufr detektoru", + "Detector Filters": "Filtry detektoru", + "Detector Flags": "Vlajky detektoru", + "Detector Grouping": "Seskupení detektoru Přidat skupiny do nastavení ", + "Detector Rate": "Rychlost detektoru (fps) ", + "Detector Recording Complete": "Kompletní záznam detektoru", + "Detector Recording Process Exited Prematurely. Restarting.": "Proces záznamu detektoru předčasně vystupoval. Restartování.", + "Detector Settings": "Nastavení detektoru", + "DetectorText": "

Když jsou zobrazeny krabice šířky a výšky, měli byste je nastavit na 640x480 nebo níže. Tím se optimalizují rychlost čtení rámů.

", + "Died": "Zemřel", + "Digest Authentication": "Autentizace digest", + "Disable": "Zakázat", + "Disable Night Vision": "Zakázat noční vidění adresa URL ", + "Disable Nightvision": "Zakázat noční vizi", + "Disabled": "Deaktivované", + "Discord": "Svár", + "Discord Alert on Trigger": "Upozornění na Discord na Trigger", + "Discord Bot": "Discord Bot", + "Discord on No Motion": "Discord na „Žádný pohyb“", + "DiscordErrorText": "Odeslání k Discordu způsobilo chybu", + "DiscordFailedText": "Odeslání k Discordu selhalo", + "DiscordLoggedIn": "Discord Bot ověřeno", + "DiscordNotEnabledText": "Bot Discord není povolen, povolte jej v nastavení vašeho účtu.", + "Documentation": "Dokumentace", + "Does Not Contain": "Neobsahuje", + "Don't Show for 1 Week": "Nezobrazujte se po dobu 1 týdne", + "Don't Stretch Monitors": "Netáhněte monitory", + "Don't show this anymore": "Už to neukazujte", + "Done": "Hotovo", + "Done!": "Hotovo!", + "DontAddToPreset": "Nepřidávejte do předvolby", + "Double Quote Directory": "Directory Double Quote Directory Některé adresáře mají mezery. Použití tohoto může zhroutit některé kamery. ", + "Down": "Down adresa URL ", + "Down Stop": "Down Stop adresa URL ", + "Download": "Stažení", + "Download Bandwidth": "Stáhněte si šířku pásma", + "Downloaded!": "Stahováno!", + "Downloading Videos": "Stahování videí", + "Downloading...": "Stahování ...", + "Duplicate": "Duplikát", + "EU": "EU", + "Easy Remote Access (P2P)": "Snadný vzdálený přístup (P2P)", + "Edit": "Upravit", + "Edit Configuration": "Upravit konfiguraci", + "Edit Selected": "Upravit vybrané", + "Edited Schedule Configuration": "Upravená konfigurace plánu", + "Edited State Configuration": "Upravená konfigurace stavu", + "Email": "E-mailem", + "Email Details": "E -mailové údaje", + "Email address is in use.": "Používá se e -mailová adresa.", + "Email and Password fields cannot be empty": "Pole e -mailu a hesla nemohou být prázdná", + "Email on No Motion": "E -mail na „Žádný pohyb“", + "Email on Trigger": "E -mail na spouště", + "Emotion": "Emoce", + "Emotion Average": "Průměr emocí", + "Enable": "Umožnit", + "Enable Night Vision": "Povolit noční vidění adresa URL ", + "Enable Nightvision": "Povolit noční vizi", + "Enabled": "Povoleno", + "Encoding": "Kódování", + "EncodingInterval": "I-rámec", + "End": "Konec", + "End Time": "Ukončený čas", + "Ended": "Skončil", + "Endpoint": "Koncový bod", + "Endpoint Address": "Adresa koncového bodu", + "Enlarge": "Zvětšit", + "Enter at least one IP": "Zadejte alespoň jednu IP", + "Enter this code to proceed": "Zadejte tento kód a pokračujte", + "Equal to": "Rovná", + "Error Connecting": "Připojení chyby", + "Error While Decoding": "Chyba při dekódování", + "ErrorWhileDecodingText": "Váš hardware může mít nestabilní připojení k síti. Zkontrolujte své síťové připojení.", + "ErrorWhileDecodingTextAudio": "Váš fotoaparát poskytuje rozbitá data. Zkuste deaktivovat zvuk v interním nastavení kamery.", + "Event": "událost", + "Event Counts": "Počty událostí", + "Event Filter Error": "Chyba filtru událostí", + "Event Filters": "Filtry událostí", + "Event Limit": "Limit události", + "Event Occurred": "Došlo k události", + "Event Rules": "Pravidla události", + "Event Webhook Error": "Chyba webhook události", + "Event-Based Recording": "Nahrávání založené na událostech", + "Event-Based Recording (For Watch-Only Mode)": "Nahrávání založené na událostech (pro režim pouze pro hodinky)", + "Event-Based Recording Flags": "Záznamové příznaky založené na událostech", + "EventText1": "Spustil událost na", + "EventText2": "Nemohl e -mailem obrázek, soubor nebyl přístupný", + "Events": "Události", + "Events Found": "Nalezené události", + "Example": "Příklad", + "Execute Command": "Provést příkaz", + "Executed": "Popraven", + "Export": "Vývozní", + "Export Selected Videos": "Export vybraných videí", + "Export Video": "Exportní video", + "ExportSelectedVideosMsg": "Chcete tato videa exportovat? Zip a stažení může nějakou dobu trvat.", + "Exposure": "Vystavení", + "FFmpegCantStart": "FFMPEG nemohl začít", + "FFmpegCantStartText": "Záznamový motor pro tento fotoaparát nemohl spustit. S konfigurací fotoaparátu může být něco špatného. Pokud existují jiné protokoly, než je tento, prosím, pošlete je do problémů na GitHubu.", + "FFmpegTip": "FFProbe je jednoduchý multimediální analyzátor Streams. Můžete jej použít k výkonu všech druhů informací o vstupu, včetně trvání, snímkové frekvence, velikosti rámce atd.", + "FFprobe": "Sonda", + "FLV": "Flv", + "FLV Stream Type": "Typ proudu FLV", + "FactorAuthText1": "Kód bude aktivní pouze po dobu 15 minut. Pokud se znovu přihlásíte, časovač bude resetován na 15 minut se stejným kódem.", + "Failed to Edit Account": "Nepodařilo se upravit účet", + "Fatal": "Fatální", + "Fatal Maximum Reached": "Fatální maximálně dosaženo, zastavení kamery.", + "FatalMaximumReachedText": "Chyba JPEG byla fatální.", + "February": "Únor", + "Feed-in Image Height": "Výška obrázku", + "Feed-in Image Width": "Šířka obrázku", + "Female": "ženský", + "Field Missing Value": "Chybějící hodnota pole", + "Fields cannot be empty": "Pole nemohou být prázdná", + "File Delete Error": "Chyba smazat souboru", + "File Download Ready": "Stahování souboru připraveno", + "File Not Exist": "Soubor neexistuje", + "File Not Found": "Soubor nenalezen", + "File Not Found in Database": "Soubor není nalezen v databázi", + "File Not Found in Filesystem": "Soubor není nalezen v souborovém systému", + "File Type": "Typ souboru", + "FileBin": "Filebin", + "FileBin Share": "Sdílet filebin", + "FileNotExistText": "Nelze uložit existující soubor. Něco se pokazilo.", + "Filename": "Název souboru", + "Filesize": "Velikost souboru", + "Filter ID": "ID filtru", + "Filter Matches": "Filtr shody", + "Filter Name": "Název filtru", + "Filter for Objects only": "Filtrujte pouze pro objekty", + "FilterMatchesText1": "Tento filtr splnil podmínky.", + "FilterMatchesText2": "Nalezená videa.", + "Filters": "Filtry", + "Filters Updated": "Aktualizované filtry", + "FiltersUpdatedText": "Vaše změny byly uloženy a aplikovány.", + "Find Where": "Najděte, kde", + "First stream in feed": "První proud v krmivu", + "Fix": "Opravit", + "Fix Video": "Opravte video", + "FixVideoMsg": "Chcete toto video opravit? Tím se vytvoří nový soubor a přepsá starý. Tuto akci nemůžete zrušit.", + "Flush PM2 Logs": "Flush PM2 protokoly", + "Font Path": "Cesta písma", + "Font Size": "Velikost písma", + "For Group": "Pro skupinu", + "Force Monitors Per Row": "Monitory síly na řadu", + "Force Port": "Force Port", + "Form Data Not Found": "Formová data nebyla nalezena", + "Found Devices": "Nalezená zařízení", + "Frame Rate": "Snímková frekvence", + "FrameRateLimit": "Limit snímků (FPS)", + "Frames": "Rámečky", + "Friday": "pátek", + "Frigate": "Fregata", + "Full Frame Detection": "Detekce celého snímku", + "Full Stream URL": "URL Full Stream", + "Full URL Path": "Plná cesta adresy URL", + "Fullscreen": "Celá obrazovka", + "Gateway": "Brána", + "Gender": "Rod", + "Generate Subtitles": "Generovat titulky", + "Get Code": "Získat kód", + "Get Logs to Client": "Získejte protokoly na klienta", + "Global Detector Settings": "Nastavení globálního detektoru", + "Google Drive": "Disk Google", + "GovLength": "Gov", + "Greater Than": "Větší než", + "Greater Than or Equal to": "Větší nebo rovnající se", + "Grid": "Mřížka", + "Group Key": "Skupinový klíč", + "Group Key is in use.": "Skupinový klíč se používá.", + "Group Name": "Skupinové jméno", + "Grouping": "Seskupení", + "H.264 / H.265 / H.265+": "H.264 / H.265 / H.265+", + "H264Profile": "Profil H264", + "HEVC (H.265)": "HEVC (H.265)", + "HLS (.m3u8)": "HLS (.M3U8)", + "HLS (includes Audio)": "HLS (zahrnuje zvuk)", + "HLS Audio Encoder": "Zvukový kodér", + "HLS List Size": "Velikost seznamu", + "HLS Live Start Index": "HLS Live Start Index", + "HLS Preset": "Přednastavená šablona", + "HLS Segment Length": "Délka segmentu v sekundách ", + "HLS Start Number": "Počáteční číslo HLS", + "HLS Video Encoder": "Video kodér", + "HTTP": "Http", + "HTTPS": "Https", + "Hardware Accelerated": "Hardware zrychlil", + "Height": "Výška", + "Help": "Pomoc", + "Hide Detection on Stream": "Skrýt detekci na proudu", + "Hide List": "Skrýt seznam", + "Hide Notes": "Skrýt poznámky", + "Home": "Domov", + "Host": "Hostitel", + "Host Type": "Typ hostitele", + "Hostname": "Název hostitele", + "How to Connect": "Jak se připojit", + "How to Record": "Jak nahrávat", + "IP Address": "IP adresa", + "Identity": "Identita", + "IdentityText1": "Takto systém identifikuje data pro tento proud. Jakmile stisknete, nemůžete změnit monitor ID . Pokud chcete, můžete před pokračováním učinit monitor ID více člověka.", + "IdentityText2": "Monitor můžete duplikovat úpravou monitor ID a poté stisknutím uložení. Nelze použít ID monitoru, který již existuje, nebo to uloží přes informace o databázi tohoto monitoru.", + "Idle": "Líný", + "Image Height": "Výška obrázku", + "Image Location": "Umístění obrázku", + "Image Position": "Pozice obrázku", + "Image Width": "Šířka obrázku", + "Imaging": "Zobrazování", + "Import": "Import", + "Import Monitor Configuration": "Konfigurace monitoru importu", + "ImportMonitorConfigurationText": "Tímto způsobem přepsáte všechny změny, které nejsou v současné době uloženy. Importované změny budou použity pouze tehdy, když stisknete uložte .", + "ImportMultiMonitorConfigurationText": "Tímto způsobem přepsáte jakékoli monitory s ID existujícími v importním souboru.", + "In": "v", + "Incorrect Settings Chosen": "Nesprávná nastavení", + "Indifference": "Lhostejnost", + "Info": "Informace", + "Information": "Informace", + "Input": "Vstup", + "Input Feed": "Vstupní krmivo", + "Input Feeds Selected": "Vstupní zdroj vybraný", + "Input Flags": "Vstupní příznaky", + "Input Map": "Vstupní mapa", + "Input Selector": "Vstupní volič", + "Input Settings": "Nastavení vstupu", + "Input Type": "Typ vstupu", + "InputText1": "Tato část říká Shinobimu, jak konzumovat proud. Pro optimální výkon zkuste vyladit interní nastavení fotoaparátu. Chcete -li najít fotoaparát, můžete použít vestavěný ve skeneru OnVif Shinobi. Chcete -li otevřít skener ONVIF, klikněte na své uživatelské jméno vlevo nahoře a poté ONVIF.", + "InputText2": "Zjistěte o konfiguraci a vyladění svých fotoaparátů zde .", + "InputText3": "Pokud potřebujete pomoci zjistit, jaký vstup je váš fotoaparát, můžete se podívat do seznamu Urls URLS adresa URL ", + "Left Stop": "Levá zastávka adresa URL ", + "Legacy Webhook": "Legacy Webhook", + "Less Than": "Méně než", + "Less Than or Equal to": "Menší nebo rovné", + "License Activated": "Aktivována licence", + "License Activation": "Aktivace licence", + "License Activation Failed": "Aktivace licence selhala", + "License Key": "Licenční klíč", + "License Plate Detector": "Detektor poznávacích značek", + "Like": "Jako", + "Limited": "Omezený", + "Link Google Account": "Odkaz na účet Google", + "Link LDAP Account": "Odkaz LDAP účet", + "Link Shinobi": "Odkaz Shinobi", + "List Toggle": "Seznam přepínání", + "List of Videos Delete Error": "Seznam videí Odstranit chybu", + "Live Grid": "Živá mřížka", + "Live Stream Toggle": "Přepínač živého proudu", + "Live View": "Živý pohled", + "Loading...": "Načítání...", + "Local": "Místní", + "Log Level": "Úroveň protokolu", + "Log Signal Event": "Event signálu protokolu Klientská strana pouze ", + "Log Stream": "Stream protokolu", + "Logging": "Protokolování", + "Login": "Přihlásit se", + "Logout": "Odhlásit se", + "Logs": "Protokoly", + "Loop Stream": "Stream smyčky", + "MB": "MB", + "MJPEG": "MJPEG", + "MP4 (copy, libx264, libx265)": "MP4 (kopie, libx264, libx265)", + "MPEG-4 (.mp4 / .ts)": "MPEG-4 (.mp4 / .ts)", + "MPEG-DASH (includes Audio)": "MPEG-DASH (zahrnuje zvuk)", + "MQTT Client": "Klient MQTT", + "MQTT Error": "Chyba MQTT", + "MQTT Inbound": "MQTT příchozí", + "MQTT Outbound": "MQTT odchozí", + "MailError": "Chyba pošty: Nelze odeslat e -mail, zkontrolovat Conf.Json. Přeskočení funkcí spoléhajících se na poštu.", + "Main": "Hlavní", + "Male": "mužský", + "Manual": "Manuál", + "Map": "Mapa", + "March": "březen", + "Matches": "Zápasy", + "Matrices": "Matice", + "Max Indifference": "Maximální lhostejnost", + "Max Latency": "Maximální latence", + "Max Number of Cameras": "Maximální počet kamer", + "Max Storage Amount": "Maximální částka úložiště", + "MaxExposureTime": "Maximální doba expozice", + "MaxGain": "Maximální zisk", + "Maximum Change": "Maximální změna", + "Maximum dB": "Maximální db", + "May": "Smět", + "Merge Selected Videos": "Sloučit vybraná videa", + "Merge Video": "Sloučit video", + "Merge and Download": "Sloučit a stahovat", + "MergeSelectedVideosMsg": "Chcete sloučit tato videa? Sloučení a stažení může nějakou dobu trvat. V okamžiku, kdy je připojení uzavřeno, bude soubor smazán. Ujistěte se, že prohlížeč budete mít otevřený, dokud nebude dokončen.", + "Methods": "Metody", + "Migrator": "Migrator", + "MinExposureTime": "Minimální doba expozice", + "MinGain": "Minimální zisk", + "Minimum Change": "Minimální změna", + "Minimum dB": "Minimální db", + "Minutes": "Minut", + "Mode": "Režim", + "Monday": "pondělí", + "Monitor": "Monitor", + "Monitor Added by user": "Monitor přidán uživatelem.", + "Monitor Capture Rate": "Rychlost zachycení monitoru (fps) ", + "Monitor Died": "Monitor zemřel", + "Monitor Edit": "Monitor Upravit", + "Monitor Groups": "Skupiny monitoru", + "Monitor ID": "ID monitoru", + "Monitor Idling": "Monitorovat volnoběh", + "Monitor Name": "Název monitoru", + "Monitor Settings": "Nastavení monitoru", + "Monitor Start": "START Monitor", + "Monitor States": "Monitorovat stavy", + "Monitor States and Schedules": "Monitorujte stavy a plány", + "Monitor Stop": "Stop monitor", + "Monitor Stopped": "Monitor se zastavil", + "Monitor Updated by user": "Monitor aktualizován uživatelem.", + "Monitor is now Disabled": "Monitor je nyní deaktivován", + "Monitor is now Idle": "Monitor je nyní nečinný", + "Monitor is now Recording": "Monitor nyní nahrává", + "Monitor is now Watching": "Monitor nyní sleduje", + "Monitor mode changed": "Režim monitoru se změnil", + "Monitor mode is already": "Režim monitoru je již", + "Monitor or Key does not exist.": "Monitor nebo klíč neexistuje.", + "MonitorIdlingText": "Relace monitoru byla objednána na nečinnost.", + "MonitorStatesText": "Můžete se dozvědět o tom, jak používat tento zde na ShinobiHub .", + "MonitorStoppedText": "Monitorová relace byla nařízena zastavit.", + "Monitors": "Monitory", + "Monitors per row": "Monitory na řadu pro Montage ", + "Monitors to Copy to": "Monitoruje kopírování do", + "Montage": "Montáž", + "Motion": "Pohyb", + "Motion Detection": "Detekce pohybu", + "Motion GUI": "Motion GUI", + "Motion Meter": "Měřič pohybu", + "Motion Threshold": "Hraba pohybu", + "Moving to Home Preset": "Přechod na předvolbu domů", + "Mp4Frag": "MP4FRAG", + "Must be atleast one row": "Musí to být alespoň jeden řádek", + "Mute Audio": "Mute zvuk", + "NTP": "NTP", + "NTP Servers": "Servery NTP", + "NVIDIA": "Nvidia", + "Name": "název", + "Name cannot be empty.": "Jméno nemůže být prázdné.", + "Nameservers": "Jmennérvers", + "Network": "Síť", + "Network Manager": "Správce sítě", + "Never": "Nikdy", + "New Authentication Token": "Nový ověřovací token", + "New Monitor": "Nový monitor", + "Newest": "Nejnovější", + "Next Video": "Další video", + "No": "Ne", + "No API Key": "Žádný klíč API", + "No Audio": "Žádný zvuk", + "No Data": "Žádná data", + "No Events found for this video": "Pro toto video nebyly nalezeny žádné události", + "No Group with this key exists": "Žádná skupina s tímto klíčem neexistuje", + "No Monitor Exists with this ID.": "S tímto ID neexistuje žádný monitor.", + "No Monitor Found, Ignoring Request": "Žádný monitor nenalezl, ignoroval požadavek", + "No Monitor ID Present in Form": "Ve formě není přítomno žádné ID monitoru", + "No Monitors Selected": "Žádné monitory byly vybrány", + "No Region": "Žádný region", + "No Rotation": "Žádná rotace", + "No Sound": "Žádný zvuk", + "No Trigger": "Žádný spoušť", + "No Videos Found": "Nalezena žádná videa", + "No such file": "Žádný takový soubor", + "NoLogsFoundForDateRange": "V tomto datovém rozsahu nebyly nalezeny žádné protokoly. Zkuste rozšířit rozsah data.", + "NoMotionEmailText1": "Žádný pohyb pro", + "NoMotionEmailText2": "Na kameře nebyl zjištěn žádný pohyb", + "NoVideosFoundForDateRange": "V tomto rozsahu data nenalezena žádná videa. Zkuste nastavit datum zahájení dále zpět.", + "Noise Filter": "Hlukový filtr", + "Noise Filter Range": "Rozsah filtru šumu", + "Non-Standard ONVIF": "Nestandardní onvif", + "Not Activated": "Neaktivováno", + "Not Authorized": "Neautorizovaný", + "Not Connected": "Nepřipojený", + "Not Equal to": "Nerovná se", + "Not Found": "Nenalezeno", + "Not In": "Ne v", + "Not Matches": "Ne shody", + "Not Permitted": "Nepovoleno", + "Not Saved": "Není zachráněno", + "Not an Administrator Account": "Není účet správce", + "NotAuthorizedText1": "Není autorizován, odeslat příkaz init s \"Auth\", \"KE\" a \"UID\"", + "Notes": "Poznámky", + "NotesPlacholder": "Komentáře, které chcete opustit pro toto nastavení kamer.", + "Nothing exists": "Nic neexistuje", + "Notice": "Oznámení", + "Notification Sound": "Zvuk oznámení", + "Notification Video Length": "Délka videa", + "Notifications": "Oznámení", + "NotifyErrorText": "Oznámení o odeslání způsobilo chybu", + "November": "listopad", + "Number of Days to keep": "Počet dní", + "Numeric criteria unsupported for Region tests, Ignoring Conditional": "Numerická kritéria nepodporovaná pro testy regionu, ignorování podmíněného", + "OAuth Code": "OAuth Code", + "OAuth Credentials": "OAuth pověření", + "ONVIF": "Onvif", + "ONVIF Device Manager": "OnVif Správce zařízení", + "ONVIF Events": "Onvif události", + "ONVIF Port": "Port OnVif", + "ONVIF Scanner": "OnVif skener", + "ONVIFErr400": "Nalezen port onvif, ale autorizace selhala při získávání URL proudu. Zkontrolujte uživatelské jméno a heslo použité pro skenování. Ujistěte se, že čas fotoaparátu a čas serveru jsou synchronizovány.", + "ONVIFErr404": "Nenalezeno. Může to být pouze webový panel pro síťové zařízení.", + "ONVIFErr405": "Metoda není povolena. Zkontrolujte uživatelské jméno a heslo použité pro skenování.", + "ONVIFEventsNotAvailable": "Události onvif nejsou k dispozici", + "ONVIFEventsNotAvailableText1": "Tato služba nemusí být k dispozici pro tento fotoaparát nebo OnVif dosud nebyl inicializován.", + "ONVIFnotCompliantProfileT": "Kamera není v souladu s profilem OnVif", + "ONVIFnote": "Objevte zařízení ONVIF v sítích mimo své vlastní nebo nechte je prázdné prohledání vaší aktuální sítě.
Uživatelské jméno a heslo lze ponechat prázdné.", + "OR": "NEBO", + "Object": "Objekt", + "Object Count": "Počet objektů", + "Object Detection": "Detekce objektu", + "Object Detector Flags": "Příznaky detektoru objektů", + "Object Tag": "Značka objektu", + "Objects Found": "Nalezené objekty", + "Objects to look for": "Objekty hledat", + "October": "říjen", + "Off": "Vypnuto", + "Oldest": "Nejstarší", + "On": "Na", + "On Event": "Na akci", + "On Release": "Při vydání", + "On Unexpected Exit": "Na neočekávaný odchod", + "Open": "OTEVŘENO", + "Open All Monitors": "Otevřete všechny monitory", + "Open Remote Dashboard": "Otevřete vzdálený palubní deska", + "OpenCV Cascades": "OpenCV kaskády", + "Operating Hours": "Provozní hodiny", + "Optional": "Volitelný", + "Options": "Možnosti", + "Order Streams": "Toky objednávky", + "Original Aspect Ratio": "Původní poměr stran", + "Original Choice": "Originální volba", + "Other Devices": "Další zařízení", + "Output": "Výstup", + "Output Method": "Metoda výstupu", + "P2P API Key": "Klíč API P2P", + "P2P Host": "P2P hostitel", + "P2P Server Not Selected": "Server P2P není vybrán", + "P2P Settings Applied": "Nastavení nastavení P2P", + "PTZ Tracking": "Sledování PTZ", + "PTZ Tracking Target": "Cíl sledování PTZ", + "Password": "Heslo", + "Password Again": "Heslo znovu", + "Passwords don't match": "Hesla se neshodují", + "Paste JSON here.": "Vložte json zde nebo nahrajte soubor.", + "Path": "Cesta", + "Pause": "Pauza", + "Per Monitor": "Na monitor", + "Performance Optimization Possible": "Je možné optimalizace výkonu", + "Permissions": "Oprávnění", + "Ping Failed": "Ping selhal", + "Plain": "Prostý", + "Play": "Hrát si", + "Playback": "Přehrávání", + "Please Check Your Settings": "Zkontrolujte prosím své nastavení", + "Please Wait for Completion": "Počkejte prosím na dokončení, v závislosti na počtu vybraných souborů může nějakou dobu trvat.", + "Please Wait or Click to Stop Checking": "Počkejte nebo klikněte a přestaňte kontrolovat", + "Please Wait...": "Prosím, čekejte...", + "Plugin": "Zapojit", + "Plugin Manager": "Správce pluginů", + "Points": "Body", + "Pop": "Pop", + "Popout Monitor on Event": "Popout Monitor na události", + "Port": "Přístav", + "Pose": "Póza", + "Poseidon": "Poseidon", + "Position X": "Pozice x", + "Position Y": "Pozice y", + "Power Video Viewer": "Prohlížeč videa Power Video", + "Power Viewer": "Prohlížeč energie", + "Preferences": "Preference", + "Prefix": "Předpona", + "Preset": "Přednastavení", + "Preset Name": "Přednastavený název", + "Presets": "Předvolby", + "Preview": "Náhled", + "Previous Video": "Předchozí video", + "Primary Engine": "Primární motor", + "Primary Input": "Primární vstup", + "Privileges": "Privilegia", + "Probe Size": "Velikost sondy", + "Process Already Running": "Proces již běží", + "Process Crashed for Monitor": "Proces havaroval pro monitor", + "Process Not Running": "Proces nespustí", + "Process Started": "Proces začal", + "Process Unexpected Exit": "Proces neočekávaný odchod", + "Processor": "Procesor", + "Profile": "Profil", + "Protocol": "Protokol", + "Public on ShinobiHub": "Veřejnost na Shinobihubu", + "Quality": "Kvalitní", + "Query": "Dotaz", + "Quick Settings": "Rychlé nastavení", + "Quick Sync Video": "Rychlé synchronizace videa", + "RAM": "RAM", + "RTMP": "RTMP", + "RTMP Stream": "Stream RTMP", + "RTMP Stream Flags": "Příznaky proudu RTMP", + "RTMPS": "RTMP", + "RTSP": "RTSP", + "RTSP Transport": "RTSP Transport", + "Range or Single": "Rozsah nebo svobodný", + "Raspberry Pi": "Raspberry Pi", + "Rate": "RADE (fps) ", + "Raw": "Drsný", + "Raw H.264 Stream": "Surový proud H.264", + "Reason": "Důvod", + "Reboot": "Restartovat", + "Reboot Camera": "Restartovat kameru", + "Recent Events": "Nedávné události", + "Recent Videos": "Nedávná videa", + "Recipient ID": "ID příjemce", + "Recommended": "Doporučeno", + "Reconnect Stream": "Znovu připojit proud", + "Record": "Záznam", + "Record File Type": "Typ souboru záznamu", + "Record Height": "Výška záznamu", + "Record Video Filter": "Záznam filtru videa", + "Record Width": "Šířka záznamu", + "Recorded Buffer": "Zaznamenaná vyrovnávací paměť", + "Recording": "Záznam", + "Recording FPS": "Nahrávání FPS", + "Recording FPS Change on Start": "Zaznamenávání změny FPS na startu", + "Recording Flags": "Záznam příznaků", + "Recording Segment Interval": "Interval nahrávání segmentu v minutách ", + "Recording Timeout": "Časový limit nahrávání v minutách ", + "Recording Timestamp": "Nahrávání časového razítka", + "Recording Watermark": "Nahrávání vodoznaku", + "RecordingText": "Doporučuje se nastavit záznamový typ souboru na webm mp4 a video Codec do libvpx COPIT H_T_H264 H_T_HLS H_T_MP4 H_T_LOCAL \"> libx264 Protože váš vstupní typ je nastaven na .", + "Refresh List of Cascades": "Obnovit seznam kaskád", + "Region": "Kraj", + "Region Editor": "Editor regionu", + "Region Name": "Název regionu", + "RegionNote": "Při přidávání bodů klikněte na okraj polygonu. Kliknutím pravým tlačítkem klikněte na bod a odstraňte.", + "Regions": "Regiony", + "Registered": "Registrovaný", + "Registered Servers": "Registrované servery", + "Remember Me": "Zapamatuj si mě", + "Remember Positions": "Pamatujte na pozice", + "Request": "Žádost", + "Require Object to be in Region": "Vyžadujte, aby objekt byl v regionu", + "Reset": "Resetovat", + "Reset Form": "Resetovací formulář", + "Reset Timer": "Resetovat časovač", + "Resolution": "Rozlišení", + "Restart": "Restart", + "Restart CRON": "Restartujte Cron", + "Restart Core": "Restartujte jádro", + "Restarting": "Restartování", + "Restarting Process": "Proces restartování", + "Retry Connection": "Retry připojení Početkrát dovoleno selhat ", + "Retrying...": "Opakování ...", + "Right": "Pravá adresa URL ", + "Right Stop": "Pravá zastávka adresa URL ", + "Rotate": "Točit se", + "Rule": "Pravidlo", + "Run Installer": "Spusťte instalační program", + "S3-Based Network Storage": "Síťové úložiště založené na S3", + "SFTP": "SFTP", + "SFTP (SSH File Transfer)": "SFTP (přenos souborů SSH)", + "SFTP Error": "Chyba SFTP", + "Saturday": "sobota", + "Save": "Uložit", + "Save Built Video on Completion": "Uložit postavené video na dokončení", + "Save Changes": "Uložit změny", + "Save Directory": "Uložit adresář", + "Save Events": "Uložit události", + "Save Events to SQL": "Uložit události na SQL", + "Save Frames to Events": "Uložit rámce na události", + "Save Links to Database": "Uložit odkazy na databázi", + "Save Log in SQL": "Uložit přihlášení v SQL to se může rychle vyplnit. ", + "Save New": "Uložit nové", + "Save as": "Uložit jako", + "Saved": "Uložené", + "Saved Filters": "Uložené filtry", + "Saved Logs": "Uložené protokoly", + "Saved Presets": "Uložené předvolby", + "Saved Schedules": "Uložené plány", + "Scan Settings": "Nastavení skenování", + "Schedule": "Plán", + "Schedule Configuration Not Found": "Konfigurace plánu nebyla nalezena", + "Schedules": "Plány", + "Search": "Vyhledávání", + "Search Base": "Hledat základna", + "Search Filter": "Vyhledávací filtr", + "Search Images": "Hledat obrázky", + "Search Object Tags": "Vyhledávací značky objektů", + "Search Settings": "Nastavení vyhledávání", + "Second stream in feed": "Druhý proud v krmivu", + "Secure": "Zajistit", + "Select a Monitor": "Vyberte monitor", + "Select atleast one monitor to delete": "Vyberte alespoň jeden monitor pro odstranění.", + "Selected": "Vybraný", + "Send Frames": "Odeslat rámečky Push Frames, které mají být analyzovány ", + "Send Notification": "Odeslat oznámení", + "Send to": "Poslat komu", + "Separate with commas, no spaces": "Oddělené s čárkami, žádné prostory", + "September": "září", + "Server URL": "URL serveru", + "Session Key": "Klíč relace", + "Set Home": "Nastavit domů", + "Set Home Position (ONVIF-only)": "Nastavit domácí pozici (pouze OnVif)", + "Set Mode": "Nastavit režim", + "Set to Watch Only": "Nastavte pouze sledovat", + "Settings": "Nastavení", + "Settings Changed": "Nastavení se změnilo", + "SettingsChangedText": "Vaše nastavení byla uložena a použita. Některá nastavení může vyžadovat obnovení této stránky.", + "Sharpness": "Ostrost", + "Shinobi": "Shinobi", + "Shinobi Ordered to Update": "Aktualizace Shinobi dokončena", + "Shinobi Streamer": "Shinobi Streamer", + "ShinobiHub": "Shinobihub", + "Show Logs": "Zobrazit protokoly", + "Show Matrices": "Zobrazit matice", + "Show Matrix": "Zobrazit matici", + "Show Regions of Interest": "Zobrazit regiony zájmu", + "Show Stream HUD": "Zobrazit stream HUD", + "Show Thumbnails in Video List": "Zobrazit miniatury v seznamu videa", + "Silent": "Tichý", + "Simple": "Jednoduchý", + "Size": "Velikost", + "Size (mb)": "Velikost (MB)", + "Skip Ping": "Přeskočit ping", + "Snapshot": "Momentka", + "Snapshot Flags": "Snapshot Flags", + "Snapshots": "Snímky", + "Sort By": "Seřazeno podle", + "Space Used": "Použitý prostor", + "Start": "Start", + "Start Recording": "Začít nahrávat", + "Start Time": "Doba spuštění", + "Start Time cannot be empty.": "Čas zahájení nemůže být prázdný.", + "Started": "Začalo", + "Started Building": "Začal budovat", + "Starting": "Začíná", + "State Configuration Not Found": "Konfigurace stavu nebyla nalezena", + "State Configuration has no monitors associated": "Konfigurace státu nemá přidružené monitory", + "Status Changed": "Stav se změnil", + "Status Indicator": "Indikátor stavu", + "Stop": "Stop", + "Stop Command": "Příkaz zastavit", + "Stop URL": "Zastavte adresu URL", + "Stopped": "Zastavil", + "Stopping": "Zastavení", + "Storage Location": "Umístění skladu", + "Storage Use": "Použití úložiště", + "Stream": "Proud", + "Stream Channel": "Stream Channel", + "Stream Channels": "Streamovací kanály", + "Stream Flags": "Příznaky streamu", + "Stream Key": "Klíč streamu", + "Stream Timestamp": "Streamovací časová razítko", + "Stream Type": "Typ proudu", + "Stream Watermark": "Streamování vodoznaku", + "Stream in Background": "Streamovat na pozadí", + "Stream to YouTube": "Streamujte na YouTube", + "Stream to YouTube Flags": "Streamujte příznaky YouTube", + "StreamText": "

Tato část označí primární metodu streamování a jeho nastavení. Tento proud se zobrazí na palubní desce. Pokud se rozhodnete použít HLS, JPEG nebo MJPEG, pak můžete konzumovat proud prostřednictvím jiných programů. rámečky.

", + "Streamed Logs": "Streamované protokoly", + "Streamer": "Stuha", + "Streams": "Streamy", + "Sub-Accounts": "Dílčí účty", + "Subdivision": "Pododdělení", + "Substream": "Substream", + "Substream Process": "Proces substream", + "SubstreamNotConfigured": "Substream není nakonfigurován. Otevřete nastavení monitoru a nakonfigurujte jej.", + "Subtitle": "Podtitul", + "Success": "Úspěch", + "Sunday": "Neděle", + "Superuser": "Superužitel", + "Superuser Logs": "Protokoly superužitelů", + "Switch on for Still Image": "Zapněte se pro statický obrázek", + "System": "Systém", + "System Level": "Úroveň systému", + "TCP": "TCP", + "TV Channel": "Televizní kanál", + "TV Channel Group": "Skupina televizních kanálů", + "TV Channel ID": "ID televizního kanálu", + "Telegram": "Telegram", + "Text Box Color": "Barva textového pole", + "Text Color": "Barva textu", + "Text criteria unsupported for Object Count tests, Ignoring Conditional": "Textová kritéria nepodporovaná pro testy počtu objektů, ignorování podmíněného", + "Themes": "Témata", + "There are no monitors that you can view with this account.": "Na tomto účtu si můžete prohlédnout žádné monitory.", + "Threads": "Vlákna", + "Thumbnail": "Miniatura", + "Thursday": "Čtvrtek", + "Tile Size": "Velikost dlaždic", + "Time": "Čas", + "Time Created": "Vytvořený čas", + "Time Left": "Zbývající čas", + "Time Occurred": "Došlo k času", + "Time-lapse": "Časová prodleva", + "Time-lapse Tool": "Časosběrný nástroj", + "TimeZone": "Časové pásmo", + "Timed": "Načasované", + "Timelapse": "Časová prodleva", + "Timelapse Frames Share": "Sdílet snímky časové doby", + "Timelapse Frames Video": "VIDEO TIMELAPSE STRAMES", + "Timelapse Video Build Complete": "Vytvoření videa Timelapse Complete", + "Timelapse Watermark": "Vodoznak timelapse", + "Timeout": "Časový limit", + "Timeout Reset on Next Event": "Obnovení časového limitu na další události", + "Timeout Reset on Next Motion": "Resetování časového limitu na další pohyb", + "Timezone": "Časové pásmo", + "Timezone Offset": "Offset TimeZone", + "Title": "Titul", + "Today": "Dnes", + "Toggle Sidebar": "Přepínač postranního panelu", + "Toggle Substream": "Přepínač substream", + "Token": "Žeton", + "Top Left": "Vlevo nahoře", + "Top Right": "Vpravo nahoře", + "Train": "Vlak", + "TrainConfirm": "Jste si jisti, že chcete začít trénovat? To může trvat déle než 12 hodin s více než 500 obrázky. Tím bude spotřebovat velké množství zdrojů, jako je RAM a/nebo CPU.", + "TrainConfirmStop": "Jste si jisti, že chcete přestat trénovat?", + "Trainer Engine": "Trainer Engine", + "Trigger Blocked": "Spouštěč blokován", + "Trigger Camera Groups": "Spouštěcí skupiny fotoaparátů", + "Trigger Event": "Spouštěcí událost", + "Trigger Group to Record": "Spouštěcí skupina pro nahrávání", + "Trigger Record": "Spouštěcí záznam", + "Trigger Successful": "Spouštět úspěšný", + "Trigger Threshold": "Spouštěcí prahová hodnota", + "Tuesday": "úterý", + "Turn Speed": "Otočit rychlost", + "Type": "Typ", + "UDP": "UDP", + "URL": "URL", + "URL Stop Timeout": "URL STOP TIMEOUT Run Stop URL po x milisekundách ", + "US": "NÁS", + "UTCDateTime": "datum", + "Unable to Launch": "Nelze spustit", + "UnabletoLaunchText": "Nejprve uložte nový monitor. Poté se pokuste spustit editor regionu.", + "Uncommon Objects": "Neobvyklé objekty", + "Uniform": "Jednotný", + "Unlink": "Unlink", + "Unlink Login": "Nelink přihlášení?", + "Unlinked": "Nepřipojeno", + "Up": "Up adresa URL ", + "Up Stop": "Up Stop adresa URL ", + "Update": "Aktualizace", + "Update to Development": "Aktualizace vývoje", + "Update to Master": "Aktualizace Master", + "Upload Bandwidth": "Nahrajte šířku pásma", + "Upload File": "Nahrát soubor", + "Uploaded Only": "Pouze nahráno", + "Uploaders": "Nahrávače", + "Use Built-In": "Použijte vestavěný", + "Use Camera Timestamps": "Používejte časová razítka fotoaparátu", + "Use Global Amazon S3 Video Storage": "Použijte Global Amazon S3 Video Storage", + "Use Global Backblaze B2 Video Storage": "Použijte Global BackBlaze B2 Video Storage", + "Use Global Wasabi Hot Cloud Storage Video Storage": "Používejte globální úložiště s úložištěm cloudu Wasabi", + "Use Global WebDAV Video Storage": "Použijte globální úložiště videa WebDAV", + "Use HTML5 Play Method": "Použijte metodu přehrávání HTML5", + "Use Max Storage Amount": "Použijte maximální částku úložiště", + "Use Raw Snapshot": "Použijte surový snímek", + "Use Substream": "Použijte substream", + "Use coProcessor": "Použijte koprocesor", + "UseCount": "Usecunt", + "User Log": "Protokol uživatele", + "User Not Found": "Uživatel nenalezen", + "Username": "Uživatelské jméno", + "VA-API": "Va-api", + "Value": "Hodnota", + "Video": "Video", + "Video Bit Rate": "Bitová rychlost videa", + "Video Codec": "Video Codec", + "Video Configuration": "Konfigurace videa", + "Video Filter": "Video filtr", + "Video Finished": "Video dokončeno", + "Video Length (minutes) and Motion Count per video": "Délka videa (minuty) a počet pohybů na video", + "Video Limit": "Video limit", + "Video Record Rate": "Sazba záznamu videa", + "Video Set": "Video sada", + "Video Share": "Sdílet videa", + "Video Status": "Stav videa", + "Video and Time Span (Minutes)": "Video a časové rozpětí (minuty)", + "Video stream only from first feed": "Video stream pouze z prvního krmiva", + "Video streams only": "Pouze video streamy", + "Videos": "Videa", + "Videos List": "Seznam videí", + "Videos Merge": "Videa se sloučí", + "Viewing Server Stats": "Prohlížení statistik serveru", + "Warning": "Varování", + "Wasabi Hot Cloud Storage": "Wasabi Hot Cloud Storage", + "Wasabi Hot Cloud Storage Upload Error": "Chyba uložení úložiště Wasabi Hot Cloud Storage", + "Watch": "Hodinky", + "Watch Only": "Sledujte pouze", + "Watch-Only": "Pouze pro sledování", + "Watching": "Sledování", + "Web Page": "Webová stránka", + "WebDAV": "WebDav", + "WebM (libvpx)": "WebM (libvpx)", + "Webdav Error": "Chyba webdav", + "WebdavErrorTextCreatingDir": "Nelze vytvořit adresář.", + "WebdavErrorTextTryCreatingDir": "Nelze ušetřit. Pokus o vytvoření adresáře.", + "Webhook": "Webhook", + "Webhook URL": "Webhook URL", + "Websocket": "WebSocket", + "Websocket Connected": "WebSocket připojeno", + "Websocket Disconnected": "WebSocket odpojeno", + "Wednesday": "středa", + "Welcome": "Vítejte!", + "When Detector is Off": "Když je detektor vypnutý", + "When Detector is On": "Když je detektor zapnutý", + "WhiteBalance": "Bilance bílé", + "WideDynamicRange": "Široký dynamický rozsah", + "Width": "Šířka", + "X Point": "X bod", + "Y Point": "Y bod", + "Yes": "Ano", + "Z-Wave": "Z-wave", + "Z-Wave Manager": "Z-Wave Manager", + "Zip and Download": "Zip a stahování", + "Zipping Videos": "Ziphing videa", + "Zones": "Zóny", + "Zoom In": "Přiblížit", + "Zoom In Stop": "Zoom in Stop adresa URL ", + "Zoom Out": "Oddálit", + "Zoom Out Stop": "Oddálit zastávku adresa URL ", + "a day": "den", + "a few seconds": "pár sekund", + "a minute": "minutu", + "a month": "měsíc", + "a year": "rok", + "aac": "AAC", + "aac (Default)": "AAC (výchozí)", + "ac3": "AC3", + "accountActionFailed": "Akce účtu selhala", + "accountAdded": "Přidán účet", + "accountAddedText": "Byl přidán účet.", + "accountDeleted": "Smazán účet", + "accountDeletedText": "Účet byl odstraněn.", + "accountId": "Číslo účtu", + "accountSettingsDescription": "Spravujte svůj profil a nastavte možnosti, jako je maximální částka úložiště a maximální počet dní, abyste si uchovávali videa.", + "accountSettingsError": "Chyba nastavení účtu", + "activatedText": "Vaše instalace byla aktivována.", + "ago": "před", + "airplane": "letoun", + "alreadyLinked": "Již propojeno s účtem", + "an hour": "hodina", + "apple": "jablko", + "applicationKey": "Klíč aplikace", + "aws_accessKeyId": "ID přístupu k klíči", + "aws_secretAccessKey": "Tajný přístupový klíč", + "backpack": "batoh", + "banana": "banán", + "baseball bat": "baseballovou pálkou", + "baseball glove": "baseballová rukavice", + "batchDownloadText": "Chcete si stáhnout vybrané soubory?", + "bear": "medvěd", + "bed": "postel", + "bench": "lavice", + "bicycle": "jízdní kolo", + "bindDN": "Binddn", + "bird": "pták", + "blankPassword": "Nechte prázdné, abyste si udrželi stejné heslo", + "boat": "loď", + "book": "rezervovat", + "bottle": "láhev", + "bowl": "miska", + "broccoli": "brokolice", + "bus": "autobus", + "cake": "dort", + "car": "auto", + "carrot": "mrkev", + "cat": "kočka", + "cell phone": "mobilní telefon", + "chair": "židle", + "clientStreamFailedattemptingReconnect": "Kontrola bočního proudu klienta selhala a pokus o opětovné připojení.", + "clock": "hodiny", + "coProcess Crashed for Monitor": "Coprocess havaroval pro monitor", + "coProcess Unexpected Exit": "coprocess neočekávaný východ", + "coProcessor": "coprocessor", + "coProcessor Started": "Coprocessor začal", + "coProcessor Stopped": "Coprocessor se zastavil", + "coProcessorTextStarted": "Coprocessor zahájil pouze výstupy CPU.", + "coProcessorTextStopped": "Coprocessor skončil.", + "codecMismatchText1": "Váš fotoaparát poskytuje data streamu H.265 (HEVC) a jako video kodek používáte kopii pro sekci Stream. Váš proud ze Shinobi se nemusí objevit na zařízeních, která nemohou tento kodek používat. Mobilní aplikace Shinobi může tyto streamy zobrazit.", + "codecMismatchText2": "Váš vybraný video kodek není použitelný. Váš fotoaparát poskytuje data streamu MJPEG a jako video kodek používáte kopii pro sekci Stream. Změnil typ proudu na mjpeg.", + "codecMismatchText3": "Váš vybraný video kodek není použitelný. Váš fotoaparát poskytuje data streamu MJPEG a jako video kodek používáte kopii pro sekci nahrávání. Změnil video kodek na libx264.", + "confirmDeleteFilter": "Chcete tento filtr smazat? Nemůžete to obnovit.", + "contactAdmin": "Kontaktujte správce instalace Shinobi.", + "copy": "kopírovat", + "couch": "gauč", + "cow": "kráva", + "cuda": "CUDA (NVIDIA NVENC)", + "cup": "pohár", + "cuvid": "CUVID (NVIDIA NVENC)", + "days": "dny", + "deleteApiKey": "Odstranit klíč API", + "deleteApiKeyText": "Chcete odstranit tento klíč API? Nemůžete to obnovit.", + "deleteMonitorStateText1": "Chcete odstranit předvolbu tohoto monitoru? Přidružené konfigurace monitoru nelze obnovit.", + "deleteMonitorStateText2": "Chcete odstranit předvolbu tohoto monitoru?", + "deleteScheduleText": "Chcete tento plán odstranit? Předvolba monitoru nebude upravena ..", + "deleteSubAccount": "Odstranit sub-účty", + "deleteSubAccountText": "Chcete tento sub-účty smazat? Nemůžete to obnovit.", + "dining table": "jídelní stůl", + "dog": "Pes", + "donut": "Kobliha", + "drm": "Sdílení objektů DRM", + "dropBoxSuccess": "Úspěch! Soubory uložené do vašeho Dropboxu.", + "dxva2": "DXVA2 (DirectX Video, Windows)", + "elephant": "slon", + "eventFilterActionText": "Toto jsou akce, které se vyskytují z podmínek filtru, které uspěly. „Původní volba“ odkazuje na možnost, kterou jste si vybrali v nastavení monitoru.", + "eventFilterErrorBrackets": "Máte nerovnoměrné množství závorek. Jsou ignorovány.", + "eventFiltersDescription": "Nastavení filtrů, kdy se objeví události.", + "failedLoginText1": "Nepodařilo se vám přihlásit příliš mnohokrát. Než to zkusíte znovu, musíte počkat 15 minut.", + "failedLoginText2": "Zkontrolujte prosím své přihlašovací údaje.", + "fieldMissingValueText1": "Váš fotoaparát poskytuje data streamu MJPEG. Musíte nastavit rychlost zachycení monitoru. Shinobi se ji pokusí detekovat a vyplnit jej automaticky.", + "fieldTextAccelerator": "Hardwarové zrychlení (HwaCcel) pro dekódování toků.", + "fieldTextAcodec": "Zvukový kodek pro nahrávání.", + "fieldTextActionsCommand": "Můžete to použít ke spuštění skriptu na příkazu.", + "fieldTextActionsHalt": "Aby se událost nestala, jako by se to nikdy nestalo.", + "fieldTextActionsIndifference": "Upravit minimální lhostejnost potřebná pro událost.", + "fieldTextActionsRecord": "Použijte nahrávání založenou na událostech, Hotswap nebo smažte nehybnou s jejich aktuálně nastavenými možnostmi v části Nastavení globální detekce.", + "fieldTextAduration": "Určete, kolik mikrosekund je analyzováno pro sondu vstup. Nastavte na 100 000, pokud používáte RTSP a máte problémy s proudem.", + "fieldTextAudioAlert": "Zvuk, když dojde k události.", + "fieldTextAudioDelay": "Zpoždění až příště může událost spustit upozornění. Měřeno během několika sekund.", + "fieldTextAudioNote": "Zvuk, když se objeví informační bublina.", + "fieldTextAutoHost": "URL úplného proudu.", + "fieldTextAutoHostEnable": "Nakrmte jednotlivé kusy potřebné k vytvoření URL proudu nebo poskytnutí celé adresy URL a umožnění Shinobi, aby ji analyzoval za vás.", + "fieldTextBufferTimeFromEvent": "Množství sekund zaznamenat předtím, než došlo k spuštění. Pokud je to důsledně nepřesné, budete se muset podívat na Optimalizační průvodce nebo silou kódování na server.", + "fieldTextChannelHlsListSize": "Počet segmentů maximální před automaticky odstraněním starých segmentů.", + "fieldTextChannelHlsTime": "Jak dlouho by měl být každý segment videa za pár minut. Každý segment bude nakreslen klientem prostřednictvím souboru M3U8. Kratší segmenty zabírají méně místa.", + "fieldTextChannelPresetStream": "Předvolba příznaku pro určité videokodéry videa. Pokud zjistíte, že váš fotoaparát zhroutí každých několik sekund: Zkuste jej nechat prázdné.", + "fieldTextChannelStreamAcodec": "Zvukový kodek pro streamování.", + "fieldTextChannelStreamAcodecAac": "Používá se pro video MP4.", + "fieldTextChannelStreamAcodecAc3": "Používá se pro video MP4.", + "fieldTextChannelStreamAcodecAuto": "Nechte FFMPEG zvolit.", + "fieldTextChannelStreamAcodecCopy": "Používá se pro video MP4. Má velmi nízké využití CPU, ale některé zvukové kodeky potřebují vlastní příznaky jako -strict 2 pro AAC.", + "fieldTextChannelStreamAcodecLibmp3lame": "Používá se pro video MP4.", + "fieldTextChannelStreamAcodecLibopus": "Používá se pro webm video.", + "fieldTextChannelStreamAcodecLibvorbis": "Používá se pro webm video.", + "fieldTextChannelStreamAcodecNoAudio": "Žádný zvuk, jedná se o možnost, která musí být stanovena v některých částech světa z právních důvodů.", + "fieldTextChannelStreamFps": "Rychlost, ve které jsou snímky zobrazovány klientům, v rámcích za sekundu. Uvědomte si, že není výchozí. To může vést k využití vysoké šířky pásma.", + "fieldTextChannelStreamQuality": "Nízký počet znamená vyšší kvalitu. Vyšší počet znamená méně kvality.", + "fieldTextChannelStreamRotate": "Změňte úhel pohledu proudu videa.", + "fieldTextChannelStreamScaleX": "Šířka obrazu proudu, který je výstup po zpracování.", + "fieldTextChannelStreamScaleY": "Výška obrazu proudu, který je výstup po zpracování.", + "fieldTextChannelStreamType": "Metoda, která se použije ke spotřebě proudu videa.", + "fieldTextChannelStreamTypeFLV": "Odesílání FLV kódovaných rámců přes WebSocket.", + "fieldTextChannelStreamTypeHLS(includesAudio)": "Podobná metoda jako živé streamy na Facebooku. Zahrnuje zvuk , pokud jej vstup poskytne. Existuje zpoždění asi 4-6 sekund, protože tato metoda zaznamenává segmenty a poté je tlačí k klientovi, spíše než tlačí, protože je vytváří.", + "fieldTextChannelStreamTypeMJPEG": "Standardní obrázek JPEG. Žádný zvuk.", + "fieldTextChannelStreamTypePoseidon": "Poseidon je postaven na zpracovatelském kódu MP4 Kevina Godella. Simuluje soubor streamování MP4, ale pomocí dat živého proudu. Zahrnuje zvuk. Některé prohlížeče to mohou hrát jako běžný soubor MP4. Streamy přes HTTP nebo WebSocket.", + "fieldTextChannelStreamVcodec": "Video kodek pro streamování.", + "fieldTextChannelStreamVcodecAuto": "Nechte FFMPEG zvolit.", + "fieldTextChannelStreamVcodecCopy": "Používá se pro video MP4. Má velmi nízké využití CPU, ale nemůže používat filtry a soubory pro soubory mohou být gigantické. Nejlepší je nastavit nastavení MP4 na straně fotoaparátu při použití této možnosti.", + "fieldTextChannelStreamVcodecLibx264": "Používá se pro video MP4.", + "fieldTextChannelStreamVcodecLibx265": "Používá se pro video MP4.", + "fieldTextChannelSvf": "Umístěte filtry videa FFMPEG do tohoto pole a ovlivňují to streamovací část. Žádné prostory.", + "fieldTextControlInvertY": "Když je fotoaparát namontován vzhůru nohama nebo používá obrácené vertikální ovládací prvky.", + "fieldTextCrf": "Nízký počet znamená vyšší kvalitu. Vyšší počet znamená méně kvality.", + "fieldTextCustDetect": "Vlastní příznaky, které se vážou na detektor proudu, používá pro analýzu.", + "fieldTextCustDetectObject": "Vlastní příznaky, které se vážou na detektor proudu, používá pro analýzu.", + "fieldTextCustInput": "Vlastní příznaky, které se vážou na vstup procesu FFMPEG.", + "fieldTextCustRecord": "Vlastní příznaky, které se vážou na záznam procesu FFMPEG.", + "fieldTextCustSipRecord": "Vlastní příznaky, které se vážou na výstup, od kterého nahrávky založené na událostech sifonují.", + "fieldTextCustSnap": "Vlastní příznaky, které se vážou na snímky.", + "fieldTextCustStream": "Vlastní příznaky, které se vážou na proud (pohled na straně klienta) procesu FFMPEG.", + "fieldTextCustomOutput": "Přidejte vlastní výstup, jako jsou rámečky JPEG nebo odesílejte data přímo na jiný server.", + "fieldTextCutoff": "Za pár minut. Kdy se odřízne a spustí nový video soubor.", + "fieldTextDays": "Počet dní na ponechání videí před očištěním.", + "fieldTextDetailSubstreamInputRtspTransportAuto": "Nechte FFMPEG rozhodnout. Normálně to zkusí nejprve UDP.", + "fieldTextDetailSubstreamInputRtspTransportTCP": "Nastavte to, pokud UDP začne poskytovat nežádoucí výsledky.", + "fieldTextDetailSubstreamInputRtspTransportUDP": "FFMPEG to zkusí první.", + "fieldTextDetailSubstreamOutputHlsListSize": "Počet segmentů maximální před automaticky odstraněním starých segmentů.", + "fieldTextDetailSubstreamOutputHlsTime": "Jak dlouho by měl být každý segment videa za pár minut. Každý segment bude nakreslen klientem prostřednictvím souboru M3U8. Kratší segmenty zabírají méně místa.", + "fieldTextDetailSubstreamOutputPresetStream": "Předvolba příznaku pro určité videokodéry videa. Pokud zjistíte, že váš fotoaparát zhroutí každých několik sekund: Zkuste jej nechat prázdné.", + "fieldTextDetailSubstreamOutputStreamAcodec": "Zvukový kodek pro streamování.", + "fieldTextDetailSubstreamOutputStreamAcodecAac": "Používá se pro video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecAc3": "Používá se pro video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecAuto": "Nechte FFMPEG zvolit.", + "fieldTextDetailSubstreamOutputStreamAcodecCopy": "Používá se pro video MP4. Má velmi nízké využití CPU, ale některé zvukové kodeky potřebují vlastní příznaky jako -strict 2 pro AAC.", + "fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame": "Používá se pro video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecLibopus": "Používá se pro webm video.", + "fieldTextDetailSubstreamOutputStreamAcodecLibvorbis": "Používá se pro webm video.", + "fieldTextDetailSubstreamOutputStreamAcodecNoAudio": "Žádný zvuk, jedná se o možnost, která musí být stanovena v některých částech světa z právních důvodů.", + "fieldTextDetailSubstreamOutputStreamFps": "Rychlost, ve které jsou snímky zobrazovány klientům, v rámcích za sekundu. Uvědomte si, že není výchozí. To může vést k využití vysoké šířky pásma.", + "fieldTextDetailSubstreamOutputStreamQuality": "Nízký počet znamená vyšší kvalitu. Vyšší počet znamená méně kvality.", + "fieldTextDetailSubstreamOutputStreamRotate": "Změňte úhel pohledu proudu videa.", + "fieldTextDetailSubstreamOutputStreamScaleX": "Šířka obrazu proudu, který je výstup po zpracování.", + "fieldTextDetailSubstreamOutputStreamScaleY": "Výška obrazu proudu, který je výstup po zpracování.", + "fieldTextDetailSubstreamOutputStreamType": "Metoda, která se použije ke spotřebě proudu videa.", + "fieldTextDetailSubstreamOutputStreamVcodec": "Video kodek pro streamování.", + "fieldTextDetailSubstreamOutputStreamVcodecAuto": "Nechte FFMPEG zvolit.", + "fieldTextDetailSubstreamOutputStreamVcodecCopy": "Používá se pro video MP4. Má velmi nízké využití CPU, ale nemůže používat filtry a soubory pro soubory mohou být gigantické. Nejlepší je nastavit nastavení MP4 na straně fotoaparátu při použití této možnosti.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx264": "Používá se pro video MP4.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx265": "Používá se pro video MP4.", + "fieldTextDetailSubstreamOutputSvf": "Umístěte filtry videa FFMPEG do tohoto pole a ovlivňují to streamovací část. Žádné prostory.", + "fieldTextDetector": "Tím se přidá další výstup do příkazu FFMPEG pro detektor pohybu.", + "fieldTextDetectorAudio": "Zkontrolujte, zda došlo k zvuku na Certiain Decible. Decibilní čtení nemusí být přesné pro měření v reálném světě.", + "fieldTextDetectorBufferHlsListSize": "Počet segmentů maximální před automaticky odstraněním starých segmentů.", + "fieldTextDetectorBufferHlsTime": "Jak dlouho by měl být každý segment videa, během několika sekund. Každý segment bude nakreslen klientem prostřednictvím souboru M3U8. Kratší segmenty zabírají méně místa.", + "fieldTextDetectorColorThreshold": "Množství rozdílu povoleného v pixelu před jeho pohybem.", + "fieldTextDetectorCommand": "Příkaz, který bude spuštěn. Toto je ekvivalent spuštění příkazu Shell z terminálu.", + "fieldTextDetectorCommandTimeout": "Tato hodnota je časovač, který umožňuje další spuštění skriptu. Tato hodnota je za pár minut.", + "fieldTextDetectorFps": "Kolik rámců na sekundu odesláním do detektoru pohybu; 2 je výchozí.", + "fieldTextDetectorFrame": "Tím se přečte celý snímky pro rozdíly v pixelech. To je stejné jako vytvoření oblasti, která pokrývá celou obrazovku. Pokud do tohoto monitoru není přidána žádná oblast, bude tato možnost výchozí pro ano.", + "fieldTextDetectorHttpApi": "Chcete k této kameře povolit spouštěče HTTP?", + "fieldTextDetectorLisencePlate": "Povolit rozpoznávání poznávací značky. Plugin OpenALPR to má vždy povoleno.", + "fieldTextDetectorLisencePlateCountry": "Vyberte typ desek, které chcete rozpoznat. V tuto chvíli jsme podporováni pouze my a EU.", + "fieldTextDetectorLockTimeout": "Uzavření, když je povolen další spoušť, zabránit přetížení databáze a přijímání klientů. Měřeno v milisekundách.", + "fieldTextDetectorMaxSensitivity": "Hodnocení spolehlivosti pohybu musí být nižší než tato hodnota, kterou je třeba považovat za spouštěč. Nechte prázdné bez maxima. Tato možnost byla dříve pojmenována „Max Insiferenference“.", + "fieldTextDetectorNoiseFilter": "Pokuste se filtrovat zrno nebo opakovaný pohyb při zvláštní lhostejnosti.", + "fieldTextDetectorNoiseFilterRange": "Množství rozdílu povoleného v pixelu před jeho pohybem.", + "fieldTextDetectorNotrigger": "Zkontrolujte, zda k pohybu došlo v intervalu. Pokud došlo k pohybu, bude kontrola resetována.", + "fieldTextDetectorNotriggerCommand": "Příkaz, který bude spuštěn. Toto je ekvivalent spuštění příkazu Shell z terminálu.", + "fieldTextDetectorNotriggerCommandTimeout": "Tato hodnota je časovač, který umožňuje další spuštění skriptu. Tato hodnota je za pár minut.", + "fieldTextDetectorNotriggerDiscord": "Pokud nebyl po časovém období detekován pohyb, obdržíte oznámení o neshodě.", + "fieldTextDetectorNotriggerTimeout": "Časový limit se počítá v minutách.", + "fieldTextDetectorNotriggerWebhook": "Pošlete žádost GET na adresu URL s některými hodnotami z události.", + "fieldTextDetectorObjCount": "Počet zjištěných objektů.", + "fieldTextDetectorObjCountInRegion": "Počítejte objekty pouze uvnitř regionů.", + "fieldTextDetectorPam": "Použijte detektor pohybu Kevina Godella. To je zabudováno do Shinobi a nevyžaduje žádnou jinou konfiguraci k aktivaci.", + "fieldTextDetectorPtzFollow": "Sledujte největší detekovaný objekt s PTZ? Vyžaduje běh detektoru objektů nebo matice poskytované událostmi.", + "fieldTextDetectorRecordMethod": "Když dojde k události, jako je pohyb, existuje několik způsobů, jak začít nahrávat. Nahrávání založené na událostech je nejúžasnější.", + "fieldTextDetectorSave": "Uložit události v databázi. To umožní zobrazení událostí přes video během přehrávání.", + "fieldTextDetectorScaleX": "Šířka detekovaného obrazu. Menší velikosti mají méně CPU.", + "fieldTextDetectorScaleY": "Výška detekovaného obrazu. Menší velikosti mají méně CPU.", + "fieldTextDetectorSendFrames": "Stiskněte rámečky k připojenému pluginu, které mají být analyzovány.", + "fieldTextDetectorSendFramesObject": "Stiskněte rámečky k připojenému pluginu, které mají být analyzovány.", + "fieldTextDetectorSendVideoLength": "Za sekundy. Délka videa, která je odeslána do vaší oznámení, jako je e -mail nebo Discord.", + "fieldTextDetectorSensitivity": "Hodnocení spolehlivosti pohybu musí tuto hodnotu překročit, aby byla vnímána jako spouštěč. Toto číslo koreluje přímo s hodnocením spolehlivosti vráceného detektorem pohybu. Tato možnost byla dříve s názvem „lhostejnost“.", + "fieldTextDetectorThreshold": "Minimální počet detekcí k vystřelení události pohybu. Detekce musí být v detektoru prahová hodnota dělena detektorem FPS sekundy. Například, pokud je detektor FPS 2 a prahová hodnota spuštění je 3, musí se do 1,5 sekundy vyskytnout tři detekce, aby se spustila událost pohybu. Tato prahová hodnota je na detekční oblast.", + "fieldTextDetectorTimeout": "Doba doba „spouštěcí záznam“ se bude spustit. Toto se čte během několika minut.", + "fieldTextDetectorTrigger": "To nařídí kameru zaznamenat, pokud je nastavena na „pouze pro sledování“, když je detekována událost.", + "fieldTextDetectorUseDetectObject": "Vytvořte rámce pro odeslání do jakéhokoli připojeného pluginu.", + "fieldTextDetectorWebhook": "Pošlete žádost GET na adresu URL s některými hodnotami z události.", + "fieldTextDetectorWebhookTimeout": "Tato hodnota je časovač, který umožňuje další spuštění vašeho webhook. Tato hodnota je za pár minut.", + "fieldTextDir": "Uložení umístění, kde budou uloženy zaznamenané soubory. Můžete nakonfigurovat více míst pomocí proměnné addStorage .", + "fieldTextEventDays": "Počet dní na udržení událostí před očištěním.", + "fieldTextEventFilters": "Umožněte, aby všechny události ctily pravidla filtru událostí.", + "fieldTextEventMonPop": "Když se objeví událost, potok proudu monitoru.", + "fieldTextEventRecordScaleX": "Šířka nahrávacího obrázku založeného na události, která je výstupem po zpracování.", + "fieldTextEventRecordScaleY": "Výška nahrávacího obrázku založeného na události, která je po zpracování výstupem.", + "fieldTextExt": "Typ souboru pro váš zaznamenaný video soubor.", + "fieldTextExtMP4": "Tento typ souboru je hratelný téměř všechny moderní webové prohlížeče, které zahrnují mobilní. Soubore má tendenci být větší, pokud nesnížíte kvalitu.", + "fieldTextExtWebM": "Malá souborovárize, nízká kompatibilita klienta. Dobré pro nahrávání na weby, jako je YouTube.", + "fieldTextFactorAuth": "Povolte sekundární požadavek na přihlášení prostřednictvím jedné z povolených metod.", + "fieldTextFatalMax": "Před nastavením monitoru na deaktivovaný počet je třeba opakovat pro připojení sítě mezi serverem a fotoaparátem. Žádné desetinné místa. Nastavit na 0, aby se znovu opakoval.", + "fieldTextFps": "Rychlost, ve které jsou rámečky zaznamenány do souborů, snímky za sekundu. Uvědomte si, že není výchozí. To může vést k velkým souborům. Nejlepší je nastavit tuto stranu fotoaparátu.", + "fieldTextHeight": "Výška obrazu proudu.", + "fieldTextHlsListSize": "Počet segmentů maximální před automaticky odstraněním starých segmentů.", + "fieldTextHlsTime": "Jak dlouho by měl být každý segment videa za pár minut. Každý segment bude nakreslen klientem prostřednictvím souboru M3U8. Kratší segmenty zabírají méně místa.", + "fieldTextHost": "Připojení adresy", + "fieldTextHwaccel": "Dekódovací motor", + "fieldTextHwaccelVcodec": "Dekódovací motor", + "fieldTextInverseTrigger": "Spuštění mimo zadané regiony. Nebude spuštěno s povolenou detekcí s plným snímkem.", + "fieldTextIp": "Rozsah nebo svobodný", + "fieldTextIrCutFilterAuto": "IR Cut Filtr je zařízením automaticky aktivován zařízením.", + "fieldTextIrCutFilterOff": "Zakázat fiter IR Cut. Obvykle noční režim.", + "fieldTextIrCutFilterOn": "Povolte IR cut firt. Obvykle denní režim.", + "fieldTextIsOnvif": "Je to kamera kompatibilní s ONVIF?", + "fieldTextLang": "Primární jazyk textových prvků. Pro úplný překlad přidejte svůj jazyk v konf.", + "fieldTextLogDays": "Počet dní pro ponechání protokolů před očištěním.", + "fieldTextLoglevel": "Množství údajů, které je třeba poskytnout při práci.", + "fieldTextLoglevelAllWarnings": "Zobrazit všechna varování. Použijte to, pokud nemůžete zjistit, co se děje s fotoaparátem.", + "fieldTextLoglevelFatal": "Zobrazit pouze fatální chyby.", + "fieldTextLoglevelOnError": "Zobrazit všechny důležité chyby. POZNÁMKA: To ne vždy ukazuje důležité informace.", + "fieldTextLoglevelSilent": "Žádný. To umlčí veškerou protokolování.", + "fieldTextMail": "Přihlášení k účtům. E -mailová adresa držitele hlavního účtu získá oznámení.", + "fieldTextMapRtspTransportAuto": "Nechte FFMPEG rozhodnout. Normálně to zkusí nejprve UDP.", + "fieldTextMapRtspTransportTCP": "Nastavte to, pokud UDP začne poskytovat nežádoucí výsledky.", + "fieldTextMapRtspTransportUDP": "FFMPEG to zkusí první.", + "fieldTextMaxKeepDays": "Počet dní, kdy se videa uchovává před očistou pro tento monitor konkrétně.", + "fieldTextMid": "Jedná se o nevýrazný identifikátor monitoru. Monitor můžete duplikovat dvojitým kliknutím na ID monitoru a jeho změnou.", + "fieldTextMode": "Toto je primární úkol monitoru.", + "fieldTextModeDisabled": "Neaktivní monitor, v tomto režimu nebude vytvořen žádný proces.", + "fieldTextModeRecord": "Nepřetržité nahrávání. Ve výchozím nastavení jsou segmenty prováděny každých 15 minut.", + "fieldTextModeWatchOnly": "Monitor bude streamovat pouze, žádné nahrávání nedojde k jinak, pokud není API nebo detektor objednáno jinak.", + "fieldTextMpass": "Heslo pro fotoaparát", + "fieldTextMuser": "Přihlášení uživatele pro fotoaparát", + "fieldTextName": "Toto je název pro monitor čitelný člověkem.", + "fieldTextNotes": "Komentáře, které chcete pro tuto kameru odejít.", + "fieldTextOnvifNonStandard": "Je to nestandardní kamera ONVIF?", + "fieldTextOnvifPort": "OnVif je obvykle spuštěn na portu 8000 . To může být 80 také v závislosti na modelu fotoaparátu.", + "fieldTextPass": "Nechte prázdné, abyste si během úpravy nastavení ponechali stejné heslo.", + "fieldTextPasswordAgain": "Pokud si přejete změnit pole hesla.", + "fieldTextPath": "Cesta k fotoaparátu", + "fieldTextPort": "Oddělit čárkami nebo rozsahem", + "fieldTextPortForce": "Použití výchozího webového portu může umožnit automatický přepínač na jiné porty pro proudy, jako je RTSP.", + "fieldTextPresetRecord": "Předvolba příznaku pro určité videokodéry videa. Pokud zjistíte, že váš fotoaparát zhroutí každých několik sekund: Zkuste jej nechat prázdné.", + "fieldTextPresetStream": "Předvolba příznaku pro určité videokodéry videa. Pokud zjistíte, že váš fotoaparát zhroutí každých několik sekund: Zkuste jej nechat prázdné.", + "fieldTextProbesize": "Určete, jak velká pro vytvoření analyzační sondy pro vstup. Nastavte na 100 000, pokud používáte RTSP a máte problémy s proudem.", + "fieldTextProtocol": "Protokol, který bude použit k konzumaci proudu videa.", + "fieldTextRecordScaleX": "Šířka obrazu proudu.", + "fieldTextRecordScaleY": "Výška obrazu proudu.", + "fieldTextRecordTimelapse": "Vytvořte timelapse založený na JPEG.", + "fieldTextRecordTimelapseMp4": "Vytvořte soubor MP4 na konci každého dne pro timelapse.", + "fieldTextRecordTimelapseWatermark": "Obrázek, který je spálen na rámečky zaznamenaného videa.", + "fieldTextRecordTimelapseWatermarkLocation": "Umístění obrázku, které bude použito jako vodoznak.", + "fieldTextRecordTimelapseWatermarkPosition": "Obrázek, který je spálen na rámečky zaznamenaného videa.", + "fieldTextRotate": "Změňte úhel záznamu proudu videa.", + "fieldTextRtmpKey": "Klíč proudu pro příchozí proudy na portu RTMP.", + "fieldTextRtspTransport": "Transportní protokol, který váš fotoaparát použije. TCP je obvykle nejlepší volbou.", + "fieldTextRtspTransportAuto": "Nechte FFMPEG rozhodnout. Normálně to zkusí nejprve UDP.", + "fieldTextRtspTransportHTTP": "Standardní metoda připojení.", + "fieldTextRtspTransportTCP": "Nastavte to, pokud UDP začne poskytovat nežádoucí výsledky.", + "fieldTextRtspTransportUDP": "FFMPEG to zkusí první.", + "fieldTextSfps": "Určete snímkovou frekvenci (FPS), ve které fotoaparát poskytuje svůj proud.", + "fieldTextSignalCheck": "Jak často váš klient zkontroluje proud, aby zjistil, zda je naživu. To se počítá v minutách.", + "fieldTextSignalCheckLog": "To je pouze pro klientskou stranu. Když dojde ke kontrole signálu na straně protokolu, zobrazí se v vlákně protokolu.", + "fieldTextSize": "Množství prostoru na disku Shinobi bude před očištěním spotřebováno. Tato hodnota se čte v megabajtech.", + "fieldTextSizeFilebinPercent": "Procento maximální částky úložiště, kterou může archiv FileBin použít.", + "fieldTextSizeTimelapsePercent": "Procento maximálního množství úložiště, na které mohou rámečky časové časové hodnoty zaznamenat.", + "fieldTextSizeVideoPercent": "Procento maximálního množství úložiště, na kterou mohou videa nahrávat.", + "fieldTextSkipPing": "Vyberte, zda je před spuštěním procesu monitoru vyžadován úspěšný ping.", + "fieldTextSnap": "Získejte nejnovější rámec v JPEG.", + "fieldTextSnapSecondsInward": "za sekundy", + "fieldTextSqllog": "Použijte to s opatrností, protože FFMPEG rád občas hází nadbytečná data, která mohou vést k mnoha datovým řádkům.", + "fieldTextSqllogNo": "Ne je výchozí.", + "fieldTextSqllogYes": "Udělejte to, pokud máte pouze opakující se problémy.", + "fieldTextStreamAcodec": "Zvukový kodek pro streamování.", + "fieldTextStreamAcodecAac": "Používá se pro video MP4.", + "fieldTextStreamAcodecAc3": "Používá se pro video MP4.", + "fieldTextStreamAcodecAuto": "Nechte FFMPEG zvolit.", + "fieldTextStreamAcodecCopy": "Používá se pro video MP4. Má velmi nízké využití CPU, ale některé zvukové kodeky potřebují vlastní příznaky jako -strict 2 pro AAC.", + "fieldTextStreamAcodecLibmp3lame": "Používá se pro video MP4.", + "fieldTextStreamAcodecLibopus": "Používá se pro webm video.", + "fieldTextStreamAcodecLibvorbis": "Používá se pro webm video.", + "fieldTextStreamAcodecNoAudio": "Žádný zvuk, jedná se o možnost, která musí být stanovena v některých částech světa z právních důvodů.", + "fieldTextStreamFlvType": "To je pouze pro řídicí panel Shinobi. Obě metody proudu jsou stále aktivní a připravené k použití.", + "fieldTextStreamFps": "Rychlost, ve které jsou snímky zobrazovány klientům, v rámcích za sekundu. Uvědomte si, že není výchozí. To může vést k využití vysoké šířky pásma.", + "fieldTextStreamLoop": "Smyčka statického souboru, takže tok souborů se chová jako živý proud.", + "fieldTextStreamQuality": "Nízký počet znamená vyšší kvalitu. Vyšší počet znamená méně kvality.", + "fieldTextStreamRotate": "Změňte úhel pohledu proudu videa.", + "fieldTextStreamScaleX": "Šířka obrazu proudu, který je výstup po zpracování.", + "fieldTextStreamScaleY": "Výška obrazu proudu, který je výstup po zpracování.", + "fieldTextStreamTimestamp": "Hodiny, které jsou spáleny na rámečky proudu videa.", + "fieldTextStreamTimestampBoxColor": "Barva pozadí Timstamp.", + "fieldTextStreamTimestampColor": "Barva textu Timstamp.", + "fieldTextStreamTimestampFont": "Soubor písma pro styling vašeho časového razítka.", + "fieldTextStreamTimestampFontSize": "Velikost písma v Pt.", + "fieldTextStreamTimestampX": "Horizontní pozice časového razítka", + "fieldTextStreamTimestampY": "Svislá poloha časového razítka", + "fieldTextStreamType": "Metoda, která se použije ke spotřebě proudu videa.", + "fieldTextStreamTypeBase64OverWebsocket": "Odesílání kódovaných rámců Base64 přes WebSocket. Tím se vyhýbá ukládání do mezipaměti, ale neexistuje žádný zvuk.", + "fieldTextStreamTypeFLV": "Odesílání FLV kódovaných rámců přes WebSocket.", + "fieldTextStreamTypeHLS(includesAudio)": "Podobná metoda jako živé streamy na Facebooku. Zahrnuje zvuk , pokud jej vstup poskytne. Existuje zpoždění asi 4-6 sekund, protože tato metoda zaznamenává segmenty a poté je tlačí k klientovi, spíše než tlačí, protože je vytváří.", + "fieldTextStreamTypeMJPEG": "Standardní obrázek JPEG. Žádný zvuk.", + "fieldTextStreamTypePoseidon": "Poseidon je postaven na zpracovatelském kódu MP4 Kevina Godella. Simuluje soubor streamování MP4, ale pomocí dat živého proudu. Zahrnuje zvuk. Některé prohlížeče to mohou hrát jako běžný soubor MP4. Streamy přes HTTP nebo WebSocket.", + "fieldTextStreamVcodec": "Video kodek pro streamování.", + "fieldTextStreamVcodecAuto": "Nechte FFMPEG zvolit.", + "fieldTextStreamVcodecCopy": "Používá se pro video MP4. Má velmi nízké využití CPU, ale nemůže používat filtry a soubory pro soubory mohou být gigantické. Nejlepší je nastavit nastavení MP4 na straně fotoaparátu při použití této možnosti.", + "fieldTextStreamVcodecLibx264": "Používá se pro video MP4.", + "fieldTextStreamVcodecLibx265": "Používá se pro video MP4.", + "fieldTextStreamVf": "Umístěte filtry videa FFMPEG do tohoto pole a ovlivňují to streamovací část. Žádné prostory.", + "fieldTextStreamWatermark": "Obrázek, který je spálen na rámečcích proudu videa.", + "fieldTextStreamWatermarkLocation": "Umístění obrázku, které bude použito jako vodoznak.", + "fieldTextStreamWatermarkPosition": "Obrázek, který je spálen na rámečcích proudu videa.", + "fieldTextTileSize": "Když je v režimu přesnosti velikost každé dlaždice v pixelech na druhou. Nižší počet bude mít vyšší přesnost, ale více využití zdrojů.", + "fieldTextTimestamp": "Hodiny, které jsou spáleny na rámečky zaznamenaného videa.", + "fieldTextTimestampBoxColor": "Barva pozadí Timstamp.", + "fieldTextTimestampColor": "Barva textu Timstamp.", + "fieldTextTimestampFont": "Soubor písma pro styling vašeho časového razítka.", + "fieldTextTimestampFontSize": "Velikost písma v Pt.", + "fieldTextTimestampX": "Horizontní pozice časového razítka", + "fieldTextTimestampY": "Svislá poloha časového razítka", + "fieldTextTvChannel": "Tento monitor bude mít povolené funkce televizního kanálu. Budete si jej moci zobrazit ve svém seznamu televizních kanálů.", + "fieldTextTvChannelGroupTitle": "Vlastní skupina pro kanál.", + "fieldTextTvChannelId": "Vlastní ID pro kanál.", + "fieldTextType": "Metoda, která se použije ke spotřebě proudu videa.", + "fieldTextTypeDashcam(StreamerV2)": "Stream P2P založený na webm založený na webm.", + "fieldTextTypeH.264/H.265/H.265+": "Čtení vysoce kvalitních videí, které někdy zahrnují zvuk.", + "fieldTextTypeHLS(.m3u8)": "Čtení vysoce kvalitních videí, které někdy zahrnují zvuk.", + "fieldTextTypeJPEG": "Čtení snímků z URL a vytvoření proudu a/nebo videa z nich.", + "fieldTextTypeLocal": "Čtení karty, webové kamery nebo integrované kamery.", + "fieldTextTypeMJPEG": "Podobně jako JPEG s výjimkou manipulace s rámečkem je prováděna FFMPEG, nikoli Shinobi.", + "fieldTextTypeMPEG4(.mp4/.ts)": "Statický soubor. Přečtěte si nižší sazbu a neměli by být použit pro skutečný živý proud.", + "fieldTextTypeMxPEG": "Stream Mobotix MJPEG", + "fieldTextTypeRTMP": "Naučte se připojit zde: Článek: Jak tlačit proudy přes RTMP na shinobi ", + "fieldTextTypeShinobiStreamer": "Stream P2P založený na WebSocket JPEG.", + "fieldTextVcodec": "Video kodek pro nahrávání.", + "fieldTextVf": "Umístěte filtry videa FFMPEG do tohoto pole a ovlivní část záznamu. Žádné prostory.", + "fieldTextWallClockTimestampIgnore": "Namísto času serveru založte všechna příchozí data kamery v době fotoaparátu.", + "fieldTextWatchdogReset": "Povolte resetování časového limitu aktivního záznamu zpět na začátek, když při nahrávání dojde k nové události.", + "fieldTextWatermark": "Obrázek, který je spálen na rámečky zaznamenaného videa.", + "fieldTextWatermarkLocation": "Umístění obrázku, které bude použito jako vodoznak.", + "fieldTextWatermarkPosition": "Obrázek, který je spálen na rámečky zaznamenaného videa.", + "fieldTextWidth": "Šířka obrazu proudu.", + "fire hydrant": "požární hydrant", + "flv": "flv", + "for Global Access": "pro globální přístup", + "fork": "Vidlička", + "frisbee": "létající talíř", + "getAMonitor": "Získejte monitor", + "getATvChannel": "Získejte televizní kanály pro monitor", + "getATvChannelText": "Získejte dostupné streamy H.264 jednoho monitoru v seznamu skladeb .m3u8.", + "getAllMonitors": "Získejte všechny monitory", + "getAllTvChannels": "Získejte všechny televizní kanály", + "getAllTvChannelsText": "Získejte všechny dostupné proudy H.264 v seznamu skladeb .m3u8. Umožněte možnosti televizního kanálu v nastavení monitoru zobrazovat jejich proudy v tomto seznamu.", + "getUserInfo": "Získejte informace o uživateli", + "getVideos": "Získejte videa", + "getVideosForMonitor": "Získejte videa pro monitor", + "giraffe": "žirafa", + "h264_cuvid": "H.264 CUVID", + "h264_mmal": "H.264 (Raspberry Pi)", + "h264_nvenc": "H.264 NVENC (NVIDIA HW Accel)", + "h264_omx": "H.264 OpenMax (Raspberry Pi)", + "h264_qsv": "H.264 (rychlé synchronizace)", + "h264_vaapi": "H.264 VA-API (Intel HW Accel)", + "h265BrowserText1": "Pokud se pokoušíte přehrávat soubor H.265, možná budete muset jej stáhnout a otevřít v jiné aplikaci, jako je VLC.", + "hair drier": "fén", + "handbag": "kabelka", + "hevc_cuvid": "H.265 CUVID", + "hevc_nvenc": "H.265 NVENC (NVIDIA HW Accel)", + "hevc_qsv": "H.265 (rychlé synchronizace)", + "hevc_vaapi": "H.265 VA-API (Intel HW Accel)", + "hlsOptions": "Možnosti HLS", + "hlsOptionsInvalid": "Možnosti HLS jsou neplatné", + "horse": "kůň", + "hot dog": "párek v rohlíku", + "hour": "hodina", + "hours": "hodiny", + "hwaccel": "Zrychlovací motor", + "hwaccel_device": "Zařízení HwaCcel", + "hwaccel_vcodec": "Video dekodér", + "in": "v", + "in Days": "za dny", + "in seconds": "za sekundy", + "keyId": "Klíčové ID", + "keyboard": "klávesnice", + "kite": "papírový drak", + "knife": "nůž", + "laptop": "notebook", + "lastLogin": "Poslední přihlášení", + "libmp3lame": "libmp3lame", + "libopus": "Libopus", + "libvorbis (Default)": "libvorbis (výchozí)", + "libvpx (Default)": "libvpx (výchozí)", + "libvpx-vp9": "libvpx-vp9", + "libx264": "libx264", + "libx264 (Default)": "libx264 (výchozí)", + "libx265": "libx265", + "liveGridDescription": "Live Grid je zobrazení více proudů pro Shinobi. Tato metoda prohlížení je navržena hlavně pro plochu.", + "loginHandleUnbound": "Přihlášení bylo z tohoto účtu nespojeno.", + "microwave": "mikrovlnná trouba", + "migrateText1": " typ vstupu nelze analyzovat. Nastavte to ručně.", + "minute": "minuta", + "minutes": "minut", + "mjpeg_cuvid": "MJPEG CUVID", + "modifyVideoText1": "Metoda neexistuje. Zkontrolujte, zda poslední hodnota URL není prázdná.", + "monSavedButNotCopied": "Váš monitor byl uložen, ale nebyl zkopírován na žádný jiný monitor.", + "monitorConfigFinderDescription": "Tento nástroj vám pomůže vyhledat konfigurace pro kamery zveřejněné komunitou. Všichni hostovali na shinobihub . Můžete také zveřejnit své, opravdu by to pomohlo komunitě :)", + "monitorEditFailedMaxReached": "Váš účet dosáhl maximálního počtu kamer, které lze vytvořit. Promluvte si s administrátorem, pokud chcete, aby se to změnilo.", + "monitorEditText1": "Neplatná data, zkontrolujte, zda se jedná o platný importní řetězec.", + "monitorEditText2": "Neplatný řetězec detailů. Zkontrolujte, zda se jedná o řetězec JSON a není propuštěn běžný objekt.", + "monitorGetText1": "Neúplný požadavek, odstraňte poslední lomítko v adrese URL nebo vložte přijatelnou hodnotu.", + "monitorStateNotEnoughChanges": "Než se pokusíte přidat do předvolby, musíte provést změnu v konfiguraci monitoru.", + "monitorStatesError": "Chyba předvoleb monitoru", + "months": "měsíce", + "motorcycle": "motocykl", + "mouse": "myš", + "mpeg2_mmal": "MPEG-2 (Raspberry Pi)", + "mpeg2_qsv": "MPEG2 (rychlé synchronizace)", + "mpeg4_cuvid": "MPEG4 CUVID", + "mpeg4_mmal": "MPEG-4 (Raspberry Pi)", + "noLoginTokensAdded": "K tomuto účtu nejsou spojeny žádné alternativní přihlášení.", + "noSpecialCharacters": "Žádné mezery nebo speciální znaky.", + "noTriggerText": "Pokud nebyl po časovém období detekován pohyb, obdržíte oznámení o neshodě.", + "noUndoForAction": "Tuto akci nemůžete zrušit.", + "notActivatedText": "Vaše instalace selhala aktivace.", + "notEnoughFramesText1": "Nedostatek snímků pro kompilaci.", + "notPermitted1": "Tato akce není povolena správcem vašeho účtu. “", + "on": "na", + "on Error": "při chybě", + "on Event": "na akci", + "onvifdeviceManagerGlobalTip": "ONVIF umožňuje úpravu interního nastavení kamery. Onvif je poněkud deštníkovým termínem, bohužel to může znamenat mnoho věcí. Vzhledem k tomu, že v tomto nástroji můžete vidět možnost, ale nemusí být upravitelná. Důvodem je obvykle proto, že dodavatel kamery tuto metodu nepřidal nebo se odchýlil od zamýšleného použití. V těchto případech budete muset zadat konfiguraci kamery prostřednictvím předepsané metody dodavatele kamery, to obecně otevírá IP adresu fotoaparátu ve webovém prohlížeči.", + "onvifdeviceSavedFoundErrorText": "Některá nastavení se mohla vrátit k předchozí hodnotě. Je možné, že modifikovaná možnost není s touto kamerou k dispozici prostřednictvím ONVIF.", + "onvifdeviceSavedText": "Interní nastavení kamery bylo uloženo. Možná budete muset restartovat kameru, aby se tyto změny projevily.", + "openImagesDownloadConfirm": "Jste si jisti, že chcete začít stahovat obrázky a ohraničující boxy (předvolební matice) z OpenImages?", + "openImagesDownloadConfirmStop": "Jste si jisti, že chcete přestat trénovat?", + "opencl": "OpenCl", + "opencvCascadesText": "Pokud zde nic nevidíte, stačí si stáhnout tento balíček kaskády . Druh je do plugins/opencv/kascades a poté stiskněte obnovení .", + "orange": "oranžový", + "oven": "trouba", + "p2pServerNotSelectedText": "Vyberte server ze seznamu a stiskněte Uložit. Počkejte 10 sekund a poté se pokuste otevřeně otevřít palubní desku.", + "p2pSettingsText1": "Pro použití změn budete muset tuto stránku obnovit.", + "parking meter": "parkovací automat", + "performanceOptimizeText1": "Váš fotoaparát poskytuje data streamu H.264. Typ proudu můžete nastavit na HLS, Poseidon a Video Codec a kopírovat.", + "person": "osoba", + "pizza": "pizza", + "possibleInternalError": "Možná interní chyba", + "postDataBroken": "Zkontrolujte formát JSON. Ujistěte se, že je napravován a definován v části „Data“", + "potted plant": "květináč", + "powerVideoEventLimit": "Nastavili jste vysoký limit události. Jste si jisti, že chcete tuto žádost podat?", + "privateKey": "Soukromý klíč", + "qsv": "QSV", + "rebootingCamera": "Restartování kamery", + "refrigerator": "lednička", + "remote": "dálkový", + "restartRequired": "Pro změny, které se projeví, je nutné restartovat jádro Shinobi.", + "sandwich": "sendvič", + "scissors": "nůžky", + "separateByCommasOrRange": "Oddělit čárkami nebo rozsahem", + "setMaxStorageAmountText": "Měli byste nastavit maximální částku úložiště do nastavení účtu umístěného vlevo. Najděte možnost v části profilu. Výchozí hodnota je 10 GB.", + "sheep": "ovce", + "sink": "dřez", + "sizePurgeLockedText": "Zdá se, že zámek pro očištění velikosti (DeleteOverMax) se nepodařilo odemknout. Odemčení nyní ...", + "skateboard": "Skateboard", + "skipPingText1": "Zkuste nastavit „přeskočit ping“ na ano.", + "skis": "lyže", + "snowboard": "snowboard", + "sorryNo": "Promiň ne", + "sorryNothingWasFound": "Omlouvám se, nic nebylo nalezeno.", + "spoon": "lžíce", + "sports ball": "sportovní míč", + "startUpText0": "Kontrola použitého disku ..", + "startUpText1": "Dokončený použitý kontrolní disk.", + "startUpText2": "Všichni uživatelé zkontrolovali, počkejte na otevření souborů a odstraňte soubory přes limit uživatele", + "startUpText3": "Čekání na poskytnutí nedokončeného videa zkontrolujte nějaký čas. 3 sekundy.", + "startUpText4": "Spuštění monitorů ... Počkejte prosím ...", + "startUpText5": "Shinobi je připraven.", + "startUpText6": "Nalezená a vložená videa osiřelá", + "stop sign": "stopka", + "subAccountManager": "Správce sub-účtování", + "substreamConnectionText": "Pokud chcete, abyste použili výše uvedenou sadu hlavních informací o připojení, můžete ponechat podrobnosti o připojení.", + "substreamOutputText": "Zde můžete nastavit konfiguraci Stream na vyžádání. Zjistěte o latence typů toků zde. ", + "substreamText": "Toto je metoda na vyžádání při prohlížení živého proudu. Můžete to udělat, takže proces sledování je k dispozici pouze tehdy, když někdo sleduje nebo musí být použit pro přepínání mezi nízkým a vysokým rozlišením.", + "suitcase": "kufr", + "superAdminText": "„Super.Json“ neexistuje. Přejmenujte prosím \"super.sample.json\" na \"super.json\".", + "superAdminTitle": "Shinobi: Super Admin", + "surfboard": "surf", + "teddy bear": "Medvídek", + "tennis racket": "tenisová raketa", + "tie": "kravata", + "toaster": "Toater", + "toilet": "toaleta", + "tokenNotUserBound": "Tato přihlašovací rukojeť není propojena s uživatelem na tomto serveru!", + "tokenNotUserBoundPt2": "Zadejte své přihlašovací údaje a poté pomocí tlačítka přihlášení Google propojte rychle.", + "toothbrush": "Kartáček na zuby", + "total": "celkový", + "traffic light": "Semafor", + "train": "vlak", + "truck": "kamion", + "tv": "televize", + "umbrella": "deštník", + "undoAllUnsaveChanges": "Jste si jisti, že to chcete udělat? Tím se vrátí všechny nezměněné změny.", + "unexpectedExitText": "Informace o tomto odchodu budou nalezeny před tímto protokolem. Navíc je zde příkaz FFMPEG, který byl použit při havárii procesu.", + "updateCamerasInternalSettings": "Aktualizovat interní nastavení kamery?", + "updateKeyText1": "„UpdateKey“ chybí v „Conf.Json“, nemůže to dělat aktualizace tímto způsobem, dokud jej nepřidáte.", + "updateKeyText2": "„UpdateKey“ je nesprávné.", + "updateNotice1": "Aktualizace Shinobi znamená přepisovací soubory. Pokud jste sami upravili jakékoli soubory, měli byste shinobi aktualizovat ručně. Vaše konfigurace a video soubory nebudou upraveny.", + "useSubStreamOnlyWhenWatching": "Pouze při sledování použijte substream", + "vaapi": "vaapi (va-api)", + "vase": "váza", + "vda": "VDA (Apple VDA Hardware Acceleration)", + "vdpau": "vdpau", + "videoBuildingText1": "Video se v současné době staví. Zkontrolujte znovu později.", + "videotoolbox": "videootoolbox", + "vp8_cuvid": "VP8 NVENC (NVIDIA HW Accel)", + "vp8_qsv": "VP8 (rychlé synchronizace)", + "vp9_cuvid": "VP9 NVENC (NVIDIA HW Accel)", + "wannaReset": "Chcete resetovat?", + "willTriggerAnEvent": "Spustí událost", + "wine glass": "sklenice na víno", + "years": "roky", + "yourFileDownloadedShortly": "Prosím, čekejte. Váš soubor bude stažen brzy.", + "zebra": "zebra" +} \ No newline at end of file diff --git a/languages/en_CA.json b/languages/en_CA.json index 8b14f7d0..b430cc25 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -1,6 +1,6 @@ { "Shinobi": "Shinobi", - "superAdminTitle": "Shinobi : Super Admin", + "superAdminTitle": "Shinobi : Super User", "failedLoginText1": "You have failed to login too many times. You must wait 15 minutes before trying again.", "failedLoginText2": "Please check your login credentials.", "Time Left": "Time Left", @@ -16,10 +16,32 @@ "accountActionFailed": "Account Action Failed", "deleteSubAccount": "Delete Sub-Account", "deleteSubAccountText": "Do you want to delete this Sub-Account? You cannot recover it.", + "Accuracy Mode": "Accuracy Mode", + "Refresh": "Refresh", + "Compress": "Compress", + "Tile Size": "Tile Size", + "fieldTextTileSize": "When in Accuracy Mode this is the size of each tile in pixels squared. A lower number will have higher accuracy but more resource use.", "Turn Speed": "Turn Speed", "Session Key": "Session Key", + "Active Monitors": "Active Monitors", + "Storage Use": "Storage Use", "Use Raw Snapshot": "Use Raw Snapshot", + "Account Edited": "Account Edited", + "Failed to Edit Account": "Failed to Edit Account", + "How to Connect": "How to Connect", "Login": "Login", + "Room ID": "Room ID", + "Substream": "Substream", + "Use Substream": "Use Substream", + "useSubStreamOnlyWhenWatching": "Only When Watching, Use Substream", + "substreamText": "This is an On-Demand method of viewing the Live Stream. You can make it so the viewing process is available only when someone is watching or to be used for switching between Low and High Resolution.", + "substreamConnectionText": "You can leave the Connection detail as-is if you want it to use the main Connection information set above.", + "substreamOutputText": "Here you can set the On-Demand Stream's configuration. Learn about latency of Stream types here.", + "Toggle Substream": "Toggle Substream", + "Output": "Output", + "SubstreamNotConfigured": "Substream not configured. Open your Monitor Settings and configure it.", + "Substream Process": "Substream Process", + "Welcome": "Welcome!", "API Key Action Failed": "API Key Action Failed", "Authenticate": "Authenticate", "Dashboard": "Dashboard", @@ -27,6 +49,7 @@ "Admin": "Admin", "Superuser": "Superuser", "Dashcam": "Dashcam", + "System Level": "System Level", "Email": "Email", "Username": "Username", "Profile": "Profile", @@ -58,6 +81,7 @@ "on": "on", "OAuth Credentials": "OAuth Credentials", "Token": "Token", + "Admin Account Settings": "Admin Account Settings", "OAuth Code": "OAuth Code", "Google Drive": "Google Drive", "Invert Y-Axis": "Invert Y-Axis", @@ -92,18 +116,18 @@ "Allow API Trigger": "Allow API Trigger", "When Detector is Off": "When Detector is Off", "When Detector is On": "When Detector is On", - "January" : "January", - "February" : "February", - "March" : "March", - "April" : "April", + "January": "January", + "February": "February", + "March": "March", + "April": "April", "May": "May", - "June" : "June", - "July" : "July", - "August" : "August", - "September" : "September", - "October" : "October", - "November" : "November", - "December" : "December", + "June": "June", + "July": "July", + "August": "August", + "September": "September", + "October": "October", + "November": "November", + "December": "December", "Sunday": "Sunday", "Monday": "Monday", "Tuesday": "Tuesday", @@ -149,13 +173,17 @@ "Motion GUI": "Motion GUI", "Motion": "Motion", "Global Detector Settings": "Global Detector Settings", + "Detector Settings": "Detector Settings", "Trigger Group to Record": "Trigger Group to Record", "Trigger Camera Groups": "Trigger Camera Groups", "Motion Detection": "Motion Detection", "Object Detection": "Object Detection", + "Hide Detection on Stream": "Hide Detection on Stream", "JPEG Mode": "JPEG Mode", "Reconnect Stream": "Reconnect Stream", "Order Streams": "Order Streams", + "Original Aspect Ratio": "Original Aspect Ratio", + "Remember Positions": "Remember Positions", "Hide Notes": "Hide Notes", "Example": "Example", "Logout": "Logout", @@ -188,6 +216,7 @@ "Videos": "Videos", "Events": "Events", "Events Found": "Events Found", + "Objects Found": "Objects Found", "Recent Events": "Recent Events", "Streams": "Streams", "Snapshot": "Snapshot", @@ -201,6 +230,7 @@ "migrateText1": "Input Type could not be parsed. Please set it manually.", "Build": "Build", "Building": "Building", + "Compressing": "Compressing", "Started Building": "Started Building", "Add": "Add", "Save": "Save", @@ -235,6 +265,9 @@ "Can Edit Monitor": "Can Edit Monitor", "Can Delete Videos": "Can Delete Videos", "Delete Video": "Delete Video", + "Delete Videos": "Delete Videos", + "Batch Download": "Batch Download", + "batchDownloadText": "Do you want to download the selected files?", "Delete Timelapse Frame": "Delete Timelapse Frame", "Can View Videos and Events": "Can View Videos and Events", "Can Delete Videos and Events": "Can Delete Videos and Events", @@ -262,6 +295,8 @@ "Type": "Type", "File Type": "File Type", "Filesize": "Filesize", + "Created": "Created", + "Size": "Size", "Video Status": "Video Status", "Custom Auto Load": "Custom Auto Load", "Easy Remote Access (P2P)": "Easy Remote Access (P2P)", @@ -297,6 +332,7 @@ "Start": "Start", "End": "End", "Archive": "Archive", + "Unarchive": "Unarchive", "Email Details": "Email Details", "Delete Matches": "Delete Matches", "Delete selected": "Delete selected", @@ -322,6 +358,7 @@ "Event Occurred": "Event Occurred", "ONVIF Port": "ONVIF Port", "ONVIF Scanner": "ONVIF Scanner", + "ONVIF Events": "ONVIF Events", "ONVIFEventsNotAvailable": "ONVIF Events not Available", "ONVIFEventsNotAvailableText1": "This service may not be available for this camera or ONVIF has not initialized yet.", "ONVIFnotCompliantProfileT": "Camera is not ONVIF Profile T Compliant", @@ -356,8 +393,16 @@ "Use Max Storage Amount": "Use Max Storage Amount", "Max Storage Amount": "Max Storage Amount", "Video Share": "Video Share", + "FileBin": "FileBin", + "File Download Ready": "File Download Ready", + "Timelapse Video Build Complete": "Timelapse Video Build Complete", + "yourFileDownloadedShortly": "Please wait. Your file will be downloaded shortly.", + "Save Built Video on Completion": "Save Built Video on Completion", + "Save Compressed Video on Completion": "Save Compressed Video on Completion", "FileBin Share": "FileBin Share", + "Timelapse Frames": "Timelapse Frames", "Timelapse Frames Share": "Timelapse Frames Share", + "Timelapse Frames Video": "Timelapse Frames Video", "Number of Days to keep": "Number of Days to keep", "Monitor Groups": "Monitor Groups", "Group Name": "Group Name", @@ -403,6 +448,7 @@ "Autosave": "Autosave", "Save Directory": "Save Directory", "CSS": "CSS Style your dashboard.", + "Don't Stretch Monitors": "Don't Stretch Monitors", "Force Monitors Per Row": "Force Monitors Per Row", "Monitors per row": "Monitors per row for Montage", "Browser Console Log": "Browser Console Log", @@ -451,9 +497,19 @@ "Delete Filter": "Delete Filter", "confirmDeleteFilter": "Do you want to delete this filter? You cannot recover it.", "Fix Video": "Fix Video", - "FixVideoMsg": "Do you want to fix this video? You cannot undo this action.", + "Archived": "Archived", + "Archive Videos": "Archive Videos", + "Compress Videos": "Compress Videos", + "Compress Completed Videos": "Compress Completed Videos", + "compressCompletedVideosFieldText": "Automatically compress videos to WebM once recorded. Doing this requires a powerful CPU or you must allow a large amount of time to allow compression. The rate in which videos are added to the database can't be faster than the rate for compression.", + "FixVideoMsg": "Do you want to fix this video? This will create a new file and overwrite the old one. You cannot undo this action.", "DeleteVideoMsg": "Do you want to delete this video? You cannot recover it.", + "CompressVideoMsg": "Do you want to compress this video? The original will be moved to your FileBin. Videos that are already completed compressing will be skipped if already queued.", + "ArchiveVideoMsg": "Do you want to Archive this video? This video won't be deleted by the automated cleanup processes.", "DeleteThisMsg": "Do you want to delete this? You cannot recover it.", + "DeleteTheseMsg": "Do you want to delete these? You cannot recover them.", + "CompressTheseMsg": "Do you want to compress these? The originals will be moved to your FileBin. Videos that are already WebM will be skipped. Videos that are already completed compressing will be skipped if ordered to compress again.", + "ArchiveTheseMsg": "Do you want to Archive these? They won't be deleted by the automated cleanup processes.", "dropBoxSuccess": "Success! Files saved to your Dropbox.", "API Key Deleted": "API Key Deleted", "APIKeyDeletedText": "Key has been deleted. It will no longer work.", @@ -496,12 +552,16 @@ "monitorEditFailedMaxReached": "Your account has reached the maximum number of cameras that can be created. Speak to an administrator if you would like this changed.", "Sub-Accounts": "Sub-Accounts", "Stream in Background": "Stream in Background", + "Carousel in Background": "Carousel in Background", + "Last": "Last", "in": "in", "ago": "ago", "a few seconds": "a few seconds", "a minute": "a minute", + "minute": "minute", "minutes": "minutes", "an hour": "an hour", + "hour": "hour", "hours": "hours", "a day": "a day", "days": "days", @@ -511,8 +571,11 @@ "a year": "a year", "years": "years", "Identity": "Identity", + "Additional Inputs": "Additional Inputs", + "Input Map": "Input Map", "Input": "Input", "Input Feed": "Input Feed", + "Input Feeds Selected": "Input Feed Selected", "Timezone": "Timezone", "Timezone Offset": "Timezone Offset", "Stream": "Stream", @@ -545,12 +608,19 @@ "accountSettingsDescription": "Manage your Profile and set options like Max Storage Amount and Max Number of Days to keep videos.", "eventFiltersDescription": "Setup filters for when Events occur.", "monitorConfigFinderDescription": "This tool will help you search for configurations for cameras posted by the community. All hosted on ShinobiHub. You can post yours too, it would really help the community :)", + "License Key": "License Key", + "License Activation": "License Activation", + "License Activation Failed": "License Activation Failed", + "License Activated": "License Activated", + "Network Manager": "Network Manager", + "Nameservers": "Nameservers", + "Interface": "Interface", + "Optional": "Optional", + "Prefix": "Prefix", "Saved": "Saved", "Not Saved": "Not Saved", "Not Connected": "Not Connected", "License Plate Detector": "License Plate Detector", - "OpenCV Cascades": "OpenCV Cascades", - "Refresh List of Cascades": "Refresh List of Cascades", "\"No Motion\" Detector": "\"No Motion\" Detector", "Control": "Control", "Grouping": "Grouping", @@ -566,6 +636,7 @@ "Creation Interval": "Creation Interval", "Plugin": "Plugin", "Plugin Manager": "Plugin Manager", + "MonitorStatesText": "You can learn about how to use this here on ShinobiHub.", "IdentityText1": "This is how the system will identify the data for this stream. You cannot change the Monitor ID once you have pressed save. If you want you can make the Monitor ID more human readable before you continue.", "IdentityText2": "You can duplicate a monitor by modifying the Monitor ID then pressing save. You cannot use the ID of a monitor that already exists or it will save over that monitor's database information.", "opencvCascadesText": "If you see nothing here then just download this package of cascades. Drop them into plugins/opencv/cascades then press refresh .", @@ -583,9 +654,7 @@ "Mode": "Mode", "Run Installer": "Run Installer", "Install": "Install", - "Enable": "Enable", "Disable": "Disable", - "Delete": "Delete", "Add All": "Add All", "Name": "Name", "Skip Ping": "Skip Ping", @@ -611,7 +680,7 @@ "HLS Preset": "Preset Template", "in seconds": "in seconds", "HLS List Size": "List Size", - "Traditional Recording": "Traditional Recording", + "Event-Based Recording": "Event-Based Recording", "Recorded Buffer": "Recorded Buffer", "Buffer Preview": "Buffer Preview", "HLS Start Number": "HLS Start Number", @@ -684,7 +753,7 @@ "Stream to YouTube": "Stream to YouTube", "Stream to YouTube Flags": "Stream to YouTube Flags", "Recording Flags": "Recording Flags", - "Traditional Recording Flags": "Traditional Recording Flags", + "Event-Based Recording Flags": "Event-Based Recording Flags", "Output Method": "Output Method", "Webhook": "Webhook", "Event Webhook Error": "Event Webhook Error", @@ -694,13 +763,14 @@ "Command": "Command", "No Trigger": "No Trigger", "noTriggerText": "If motion has not been detected after the timeout period you will recieve an Discord notification.", + "On Event": "On Event", "on Event": "on Event", "Allow Next Alert": "Allow Next Alert", "Allow Next Webhook": "Allow Next Webhook", "Allow Next Command": "Allow Next Command", "Allow Next Trigger": "Allow Next Trigger", "Save Events to SQL": "Save Events to SQL", - "Email on Trigger": "Email on Trigger Emails go to the main account holder's login address.", + "Email on Trigger": "Email on Trigger", "Attach Video Clip": "Attach Video Clip", "Error While Decoding": "Error While Decoding", "ErrorWhileDecodingText": "Your hardware may have an unstable connection to the network. Check your network connections.", @@ -716,7 +786,10 @@ "NotifyErrorText": "Sending Notification caused an Error", "Check the Channel ID": "Check the Channel ID", "Check the Recipient ID": "Check the Recipient ID", + "AppNotEnabledText": "App Not Enabled, Enable it in your Account Settings.", "DiscordNotEnabledText": "Discord Bot Not Enabled, Enable it in your Account Settings.", + "PushoverNotifyErrorText": "An error occurred while sending Pushover notification", + "PushoverNotEnabledText": "Pushover notifications are not enabled. Enable Pushover notifications in Account Settings.", "Account Settings": "Account Settings", "How to Record": "How to Record", "Trigger Record": "Trigger Record", @@ -810,9 +883,8 @@ "libvorbis (Default)": "libvorbis (Default)", "libopus": "libopus", "aac (Default)": "aac (Default)", - "Traditional (Watch-Only, Includes Buffer)": "Traditional (Watch-Only, Includes Buffer)", - "Hotswap Modes (Watch-Only)": "Hotswap Modes (Watch-Only)", - "Delete Motionless Videos (Record)": "Delete Motionless Videos (Record)", + "Event-Based Recording (For Watch-Only Mode)": "Event-Based Recording (For Watch-Only Mode)", + "Delete Motionless Videos (For Record Mode)": "Delete Motionless Videos (For Record Mode)", "US": "US", "EU": "EU", "Silent": "Silent", @@ -862,8 +934,8 @@ "Restarting": "Restarting", "Starting": "Starting", "Watching": "Watching", - "Recording": "Recording", "Stopped": "Stopped", + "Stopping": "Stopping", "Died": "Died", "Restart": "Restart", "Monitor Stopped": "Monitor Stopped", @@ -912,7 +984,13 @@ "MailError": "MAIL ERROR : Could not send email, Check conf.json. Skipping any features relying on mailing.", "updateKeyText1": "\"updateKey\" is missing from \"conf.json\", cannot do updates this way until you add it.", "updateKeyText2": "\"updateKey\" is incorrect.", + "Control Trigger Started": "Control Trigger Started", + "Control Trigger Ended": "Control Trigger Ended", + "Control Triggered": "Control Triggered", "Control Error": "Control Error", + "Timed": "Timed", + "On Release": "On Release", + "Moving to Home Preset": "Moving to Home Preset", "Database row does not exist": "Database row does not exist", "File Delete Error": "File Delete Error", "List of Videos Delete Error": "List of Videos Delete Error", @@ -937,11 +1015,15 @@ "Monitor mode is already": "Monitor mode is already", "Monitor or Key does not exist.": "Monitor or Key does not exist.", "No Group with this key exists": "No Group with this key exists", + "Downloaded!": "Downloaded!", "Downloading...": "Downloading...", + "Loading...": "Loading...", "Downloading Videos": "Downloading Videos", "Zipping Videos": "Zipping Videos", "Automatic Checking Cancelled": "Automatic Checking Cancelled", "Success": "Success", + "Done": "Done", + "Done!": "Done!", "Please Wait or Click to Stop Checking": "Please Wait or Click to Stop Checking", "Search Settings": "Search Settings", "Trigger Successful": "Trigger Successful", @@ -992,52 +1074,53 @@ "notPermitted1": "This action is not permitted by the administrator of your account.'", "Not Authorized": "Not Authorized", "Generate Subtitles": "Generate Subtitles", - "Video Limit":"Video Limit", - "Preview":"Preview", - "Websocket Connected":"Websocket Connected", - "Websocket Disconnected":"Websocket Disconnected", - "Videos Merge":"Videos Merge", - "Token":"Token", - "Channel ID":"Channel ID", - "Recipient ID":"Recipient ID", - "New Authentication Token":"New Authentication Token", - "All Logs":"All Logs", - "For Group":"For Group", - "Basic Authentication":"Basic Authentication", - "Superuser Logs":"Superuser Logs", - "Authentication Failed":"Authentication Failed", - "Max Number of Cameras":"Max Number of Cameras", - "Can edit Max Storage":"Can edit Max Storage", - "Can edit Max Days":"Can edit Max Days", - "in Days":"in Days", - "Can edit how long to keep Logs":"Can edit how long to keep Logs", - "Can use Admin Panel":"Can use Admin Panel", - "Can use Discord Bot":"Can use Discord Bot", - "Can use WebDAV":"Can use WebDAV", - "Can use Amazon S3":"Can use Amazon S3", - "Can use SFTP":"Can use SFTP", - "Can use Wasabi Hot Cloud Storage":"Can use Wasabi Hot Cloud Storage", - "Can use LDAP":"Can use LDAP", - "Can View Logs":"Can View Logs", - "Can edit how long to keep Events":"Can edit how long to keep Events", - "Leave blank for unlimited":"Leave blank for unlimited", - "privateKey":"Private Key", - "Limited":"Limited", - "All Privileges":"All Privileges", - "LDAP":"LDAP", - "LDAP Success":"LDAP Success", - "LDAP User Authenticated":"LDAP User Authenticated", - "LDAP User is New":"LDAP User is New", - "Creating New Account":"Creating New Account", - "bindDN":"bindDN", - "Bind Credentials":"Bind Credentials (Password)", - "Search Base":"Search Base", - "Configuration":"Configuration", - "Blank for No Change":"Blank for No Change", - "Pop":"Pop", - "Recording FPS Change on Start":"Recording FPS Change on Start", - "Save Frames to Events":"Save Frames to Events", - "Search Filter":"Search Filter", + "Video Limit": "Video Limit", + "Preview": "Preview", + "Websocket Connected": "Websocket Connected", + "Websocket Disconnected": "Websocket Disconnected", + "Videos Merge": "Videos Merge", + "Channel ID": "Channel ID", + "Recipient ID": "Recipient ID", + "New Authentication Token": "New Authentication Token", + "No Image": "No Image", + "All Logs": "All Logs", + "For Group": "For Group", + "Basic Authentication": "Basic Authentication", + "Superuser Logs": "Superuser Logs", + "Action Failed": "Action Failed", + "Authentication Failed": "Authentication Failed", + "Max Number of Cameras": "Max Number of Cameras", + "Can edit Max Storage": "Can edit Max Storage", + "Can edit Max Days": "Can edit Max Days", + "in Days": "in Days", + "Can edit how long to keep Logs": "Can edit how long to keep Logs", + "Can use Admin Panel": "Can use Admin Panel", + "Can use Discord Bot": "Can use Discord Bot", + "Can use WebDAV": "Can use WebDAV", + "Can use Amazon S3": "Can use Amazon S3", + "Can use SFTP": "Can use SFTP", + "Can use Wasabi Hot Cloud Storage": "Can use Wasabi Hot Cloud Storage", + "Can use LDAP": "Can use LDAP", + "Can View Logs": "Can View Logs", + "Can edit how long to keep Events": "Can edit how long to keep Events", + "Leave blank for unlimited": "Leave blank for unlimited", + "privateKey": "Private Key", + "Limited": "Limited", + "All Privileges": "All Privileges", + "LDAP": "LDAP", + "LDAP Success": "LDAP Success", + "LDAP User Authenticated": "LDAP User Authenticated", + "LDAP User is New": "LDAP User is New", + "Creating New Account": "Creating New Account", + "bindDN": "bindDN", + "Bind Credentials": "Bind Credentials (Password)", + "Search Base": "Search Base", + "Configuration": "Configuration", + "Blank for No Change": "Blank for No Change", + "Pop": "Pop", + "Recording FPS Change on Start": "Recording FPS Change on Start", + "Save Frames to Events": "Save Frames to Events", + "Search Filter": "Search Filter", "h264_cuvid": "H.264 CUVID", "hevc_cuvid": "H.265 CUVID", "mjpeg_cuvid": "MJPEG CUVID", @@ -1084,72 +1167,74 @@ "FLV": "FLV", "FLV Stream Type": "FLV Stream Type", "Link Shinobi": "Link Shinobi", - "Show Stream HUD":"Show Stream HUD", - "Call Method":"Call Method", - "Gender":"Gender", - "Emotion":"Emotion", - "Age":"Age", - "Object":"Object", - "Uniform":"Uniform", - "Pose":"Pose", - "Male":"Male", - "Female":"Female", - "Channel":"Channel", - "Stream Key":"Stream Key", - "Server URL":"Server URL", - "Video Bit Rate":"Video Bit Rate", - "Audio Bit Rate":"Audio Bit Rate", - "RTMP Stream Flags":"RTMP Stream Flags", - "RTMP Stream":"RTMP Stream", - "Stream Channel":"Stream Channel", - "Confidence":"Confidence", - "Trainer Engine":"Trainer Engine", - "Train":"Train", - "openImagesDownloadConfirm":"Are you sure you want to begin download images and bounding boxes (preset Matrices) from OpenImages?", - "openImagesDownloadConfirmStop":"Are you sure you want to stop training?", - "TrainConfirm":"Are you sure you want to begin training? This can take more than 12 hours with over 500 images. This will consume a large amount of resources, like RAM and/or CPU.", - "TrainConfirmStop":"Are you sure you want to stop training?", - "Batch":"Batch", - "Subdivision":"Subdivision", - "Map":"Map", - "Delay for Snapshot":"Delay for Snapshot", - "Add Map":"Add Map", - "Add Input Feed":"Add Input Feed", - "Add Channel":"Add Channel", - "Automatic":"Automatic", - "Max Latency":"Max Latency", - "Loop Stream":"Loop Stream", - "Object Count":"Object Count", - "Object Tag":"Object Tag", - "Noise Filter":"Noise Filter", - "Noise Filter Range":"Noise Filter Range", - "TV Channel":"TV Channel", - "Channel ID":"Channel ID", - "TV Channel ID":"TV Channel ID", - "TV Channel Group":"TV Channel Group", - "Emotion Average":"Emotion Average", - "Require Object to be in Region":"Require Object to be in Region", - "Numeric criteria unsupported for Region tests, Ignoring Conditional":"Numeric criteria unsupported for Region tests, Ignoring Conditional", - "Text criteria unsupported for Object Count tests, Ignoring Conditional":"Text criteria unsupported for Object Count tests, Ignoring Conditional", - "Show Regions of Interest":"Show Regions of Interest", - "Confidence of Detection":"Confidence of Detection", - "Edit Selected":"Edit Selected", - "Copy Stream Channels":"Copy Stream Channels", - "Copy Settings":"Copy Settings", - "Copy to Settings":"Copy to Settings", - "Copy Mode":"Copy Mode", - "Copy Group Settings":"Copy Group Settings", - "Copy Timelapse Settings":"Copy Timelapse Settings", - "Copy Connection Settings":"Copy Connection Settings", - "Copy Custom Settings":"Copy Custom Settings", - "Copy Logging Settings":"Copy Logging Settings", - "Copy JPEG API Settings":"Copy JPEG API Settings", - "Copy Input Settings":"Copy Input Settings", - "Copy Stream Settings":"Copy Stream Settings", - "Copy Stream Channel Settings":"Copy Stream Channel Settings", - "Copy Recording Settings":"Copy Recording Settings", - "Copy Detector Settings":"Copy Detector Settings", - "Monitors to Copy to":"Monitors to Copy to", + "Show Stream HUD": "Show Stream HUD", + "Call Method": "Call Method", + "Gender": "Gender", + "Emotion": "Emotion", + "Age": "Age", + "Object": "Object", + "Uniform": "Uniform", + "Pose": "Pose", + "Male": "Male", + "Female": "Female", + "Channel": "Channel", + "Stream Key": "Stream Key", + "Server URL": "Server URL", + "Video Bit Rate": "Video Bit Rate", + "Audio Bit Rate": "Audio Bit Rate", + "RTMP Stream Flags": "RTMP Stream Flags", + "RTMP Stream": "RTMP Stream", + "Stream Channel": "Stream Channel", + "Stream Channels": "Stream Channels", + "Confidence": "Confidence", + "Trainer Engine": "Trainer Engine", + "Train": "Train", + "openImagesDownloadConfirm": "Are you sure you want to begin download images and bounding boxes (preset Matrices) from OpenImages?", + "openImagesDownloadConfirmStop": "Are you sure you want to stop training?", + "TrainConfirm": "Are you sure you want to begin training? This can take more than 12 hours with over 500 images. This will consume a large amount of resources, like RAM and/or CPU.", + "TrainConfirmStop": "Are you sure you want to stop training?", + "Batch": "Batch", + "Subdivision": "Subdivision", + "Map": "Map", + "Delay for Snapshot": "Delay for Snapshot", + "Add Map": "Add Map", + "Add Input Feed": "Add Input Feed", + "Add Channel": "Add Channel", + "Automatic": "Automatic", + "Max Latency": "Max Latency", + "Loop Stream": "Loop Stream", + "Object Count": "Object Count", + "Object Tag": "Object Tag", + "Search Object Tags": "Search Object Tags", + "Noise Filter": "Noise Filter", + "Noise Filter Range": "Noise Filter Range", + "TV Channel": "TV Channel", + "TV Channel ID": "TV Channel ID", + "TV Channel Group": "TV Channel Group", + "Emotion Average": "Emotion Average", + "Require Object to be in Region": "Require Object to be in Region", + "Numeric criteria unsupported for Region tests, Ignoring Conditional": "Numeric criteria unsupported for Region tests, Ignoring Conditional", + "Text criteria unsupported for Object Count tests, Ignoring Conditional": "Text criteria unsupported for Object Count tests, Ignoring Conditional", + "Show Regions of Interest": "Show Regions of Interest", + "Confidence of Detection": "Confidence of Detection", + "Edit Selected": "Edit Selected", + "Copy Stream Channels": "Copy Stream Channels", + "Copy to Selected Monitor(s)": "Copy to Selected Monitor(s)", + "Copy Settings": "Copy Settings", + "Copy to Settings": "Copy to Settings", + "Copy Mode": "Copy Mode", + "Copy Group Settings": "Copy Group Settings", + "Copy Timelapse Settings": "Copy Timelapse Settings", + "Copy Connection Settings": "Copy Connection Settings", + "Copy Custom Settings": "Copy Custom Settings", + "Copy Logging Settings": "Copy Logging Settings", + "Copy JPEG API Settings": "Copy JPEG API Settings", + "Copy Input Settings": "Copy Input Settings", + "Copy Stream Settings": "Copy Stream Settings", + "Copy Stream Channel Settings": "Copy Stream Channel Settings", + "Copy Recording Settings": "Copy Recording Settings", + "Copy Detector Settings": "Copy Detector Settings", + "Monitors to Copy to": "Monitors to Copy to", "Video Configuration": "Video Configuration", "ONVIF Device Manager": "ONVIF Device Manager", "UseCount": "UseCount", @@ -1208,15 +1293,419 @@ "getVideos": "Get Videos", "getVideosForMonitor": "Get Videos for Monitor", "No Sound": "No Sound", - "Notification Sound":"Notification Sound", - "Alert Sound":"Alert Sound", - "Alert Sound Delay":"Alert Sound Delay", - "onvifdeviceManagerGlobalTip":"ONVIF allows modifying the camera's internal settings. ONVIF is somewhat of an umbrella term, it can mean many things unfortunately. With that being the case you may see an option in this tool but it may not be editable. This is usually because the camera vendor has not added this method or has deviated from its intended usage. In those cases you will need to enter the camera's configuration through the prescribed method of the camera vendor, this is generally opening the IP Address of the camera in your web browser.", - "onvifdeviceSavedText":"Camera's internal settings have been saved. You may need to restart the camera to have these changes take effect.", - "onvifdeviceSavedFoundErrorText":"Some settings may have reverted to a previous value. Its possible that the modified option is not available with this camera through ONVIF.", - "powerVideoEventLimit":"You have set a high event limit. Are you sure you want to make this request?", - "There are no monitors that you can view with this account.":"There are no monitors that you can view with this account.", + "Notification Sound": "Notification Sound", + "Alert Sound": "Alert Sound", + "Alert Sound Delay": "Alert Sound Delay", + "onvifdeviceManagerGlobalTip": "ONVIF allows modifying the camera's internal settings. ONVIF is somewhat of an umbrella term, it can mean many things unfortunately. With that being the case you may see an option in this tool but it may not be editable. This is usually because the camera vendor has not added this method or has deviated from its intended usage. In those cases you will need to enter the camera's configuration through the prescribed method of the camera vendor, this is generally opening the IP Address of the camera in your web browser.", + "onvifdeviceSavedText": "Camera's internal settings have been saved. You may need to restart the camera to have these changes take effect.", + "onvifdeviceSavedFoundErrorText": "Some settings may have reverted to a previous value. Its possible that the modified option is not available with this camera through ONVIF.", + "powerVideoEventLimit": "You have set a high event limit. Are you sure you want to make this request?", + "There are no monitors that you can view with this account.": "There are no monitors that you can view with this account.", "Delete Monitors and Files": "Delete Monitors and Files", "Select atleast one monitor to delete": "Select atleast one monitor to delete.", - "Use Built-In":"Use Built-In" + "Use Built-In": "Use Built-In", + "Add Cameras": "Add Cameras", + "Add Camera": "Add Camera", + "Delete Camera": "Delete Camera", + "Event Rules": "Event Rules", + "Other Devices": "Other Devices", + "Zones": "Zones", + "Information": "Information", + "Info": "Info", + "Motion Threshold": "Motion Threshold", + "Attach Snapshot": "Attach Snapshot", + "Invalid Settings": "Invalid Settings", + "Detection": "Detection", + "Playback": "Playback", + "Backup": "Backup", + "Close All Monitors": "Close All Monitors", + "Daily Events": "Daily Events", + "Send Notification": "Send Notification", + "Send to": "Send to", + "setMaxStorageAmountText": "You should set your Max Storage Amount in your Account Settings located on the left. Find the option under the Profile section. Default is 10 GB.", + "Save Events": "Save Events", + "Original Choice": "Original Choice", + "Legacy Webhook": "Legacy Webhook", + "eventFilterActionText": "These are the actions that occur from the filter conditions that have succeeded. \"Original Choice\" refers to the option you had chosen in your Monitor's Settings.", + "Telegram": "Telegram", + "Before": "Before", + "After": "After", + "Rule": "Rule", + "Event Filter Error": "Event Filter Error", + "eventFilterErrorBrackets": "You have an un-even number of brackets. They are being ignored.", + "Quick Settings": "Quick Settings", + "Copy Stream URL": "Copy Stream URL", + "willTriggerAnEvent": "will trigger an event", + "Cloud": "Cloud", + "Objects to look for": "Objects to look for", + "Common Objects": "Common Objects", + "Uncommon Objects": "Uncommon Objects", + "person": "person", + "bicycle": "bicycle", + "car": "car", + "motorcycle": "motorcycle", + "airplane": "airplane", + "bus": "bus", + "train": "train", + "truck": "truck", + "boat": "boat", + "traffic light": "traffic light", + "fire hydrant": "fire hydrant", + "stop sign": "stop sign", + "parking meter": "parking meter", + "bench": "bench", + "bird": "bird", + "cat": "cat", + "dog": "dog", + "horse": "horse", + "sheep": "sheep", + "cow": "cow", + "elephant": "elephant", + "bear": "bear", + "zebra": "zebra", + "giraffe": "giraffe", + "backpack": "backpack", + "umbrella": "umbrella", + "handbag": "handbag", + "tie": "tie", + "suitcase": "suitcase", + "frisbee": "frisbee", + "skis": "skis", + "snowboard": "snowboard", + "sports ball": "sports ball", + "kite": "kite", + "baseball bat": "baseball bat", + "baseball glove": "baseball glove", + "skateboard": "skateboard", + "surfboard": "surfboard", + "tennis racket": "tennis racket", + "bottle": "bottle", + "wine glass": "wine glass", + "cup": "cup", + "fork": "fork", + "knife": "knife", + "spoon": "spoon", + "bowl": "bowl", + "banana": "banana", + "apple": "apple", + "sandwich": "sandwich", + "orange": "orange", + "broccoli": "broccoli", + "carrot": "carrot", + "hot dog": "hot dog", + "pizza": "pizza", + "donut": "donut", + "cake": "cake", + "chair": "chair", + "couch": "couch", + "potted plant": "potted plant", + "bed": "bed", + "dining table": "dining table", + "toilet": "toilet", + "tv": "tv", + "laptop": "laptop", + "mouse": "mouse", + "remote": "remote", + "keyboard": "keyboard", + "cell phone": "cell phone", + "microwave": "microwave", + "oven": "oven", + "toaster": "toaster", + "sink": "sink", + "refrigerator": "refrigerator", + "book": "book", + "clock": "clock", + "Clock Format": "Clock Format", + "vase": "vase", + "scissors": "scissors", + "teddy bear": "teddy bear", + "hair drier": "hair drier", + "toothbrush": "toothbrush", + "Detection Event": "Detection Event", + "Monitor Edit": "Monitor Edit", + "Monitor Start": "Monitor Start", + "Monitor Stop": "Monitor Stop", + "Monitor Died": "Monitor Died", + "Account Save": "Account Save", + "User Log": "User Log", + "Frigate": "Frigate", + "Plain": "Plain", + "MQTT Error": "MQTT Error", + "MQTT Inbound": "MQTT Inbound", + "MQTT Outbound": "MQTT Outbound", + "MQTT Client": "MQTT Client", + "Buffer Time from Event": "Buffer Time from Event", + "fieldTextEventFilters": "Enable to have all Events honor your Event Filter rules.", + "fieldTextBufferTimeFromEvent": "The amount of seconds to record before the trigger happened. If this is consistently inaccurate you will need to look at the optimization guide or force encoding on the server.", + "fieldTextMode": "This is the primary task of the monitor.", + "fieldTextModeDisabled": "Inactive monitor, no process will be created in this mode.", + "fieldTextModeWatchOnly": "Monitor will only stream, no recording will occur unless otherwise ordered by API or Detector.", + "fieldTextModeRecord": "Continuous Recording. Segments are made every 15 minutes by default.", + "fieldTextMid": "This is a non-changeable identifier for the monitor. You can duplicate a monitor by double clicking the Monitor ID and changing it.", + "fieldTextName": "This is the human-readable display name for the monitor.", + "fieldTextMaxKeepDays": "The number of days to keep videos before purging for this monitor specifically.", + "fieldTextNotes": "Comments you want to leave for this camera.", + "fieldTextDir": "Location of where recorded files will be saved. You can configure more locations with the addStorage variable.", + "fieldTextType": "The method that will used to consume the video stream.", + "fieldTextTypeJPEG": "Reading snapshots from a URL and making a stream and/or video from them.", + "fieldTextTypeMJPEG": "Similar to JPEG except the frame handling is done by FFMPEG, not Shinobi.", + "fieldTextTypeH.264/H.265/H.265+": "Reading a high quality video streas that sometimes include audio.", + "fieldTextTypeHLS(.m3u8)": "Reading a high quality video streas that sometimes include audio.", + "fieldTextTypeMPEG4(.mp4/.ts)": "A static file. Read at a lower rate and should not be used for an actual live stream.", + "fieldTextTypeShinobiStreamer": "Websocket JPEG-based P2P stream.", + "fieldTextTypeDashcam(StreamerV2)": "Websocket WebM-based P2P stream.", + "fieldTextTypeLocal": "Reading Capture Cards, Webcams, or Integrated Cameras.", + "fieldTextTypeRTMP": "Learn to connect here : Article : How to Push Streams via RTMP to Shinobi", + "fieldTextTypeMxPEG": "Mobotix MJPEG Stream", + "fieldTextRtmpKey": "Stream Key for incoming streams on the RTMP port.", + "fieldTextAutoHostEnable": "Feed the individual pieces required to build a stream URL or provide the full URL and allow Shinobi to parse it for you.", + "fieldTextAutoHost": "The full Stream URL.", + "fieldTextProtocol": "The protocol that will used to consume the video stream.", + "fieldTextRtspTransport": "The transport protocol your camera will use. TCP is usually the best choice.", + "fieldTextRtspTransportAuto": "Let FFMPEG decide. Normally it will try UDP first.", + "fieldTextRtspTransportTCP": "Set it to this if UDP starts giving undesired results.", + "fieldTextRtspTransportUDP": "FFMPEG tries this first.", + "fieldTextRtspTransportHTTP": "Standard connection method.", + "fieldTextMuser": "The user login for your camera", + "fieldTextMpass": "The password for your camera", + "fieldTextHost": "Connection address", + "fieldTextPort": "Separate by Commas or a Range", + "fieldTextPortForce": "Using the default web port can allow automatic switch to other ports for streams like RTSP.", + "fieldTextPath": "The path to your camera", + "fieldTextFatalMax": "The number of times to retry for network connection between the server and camera before setting the monitor to Disabled. No decimals. Set to 0 to retry forever.", + "fieldTextSkipPing": "Choose if a successful ping is required before a monitor process is started.", + "fieldTextIsOnvif": "Is this an ONVIF compliant camera?", + "fieldTextOnvifNonStandard": "Is this a Non-Standard ONVIF camera?", + "fieldTextOnvifPort": "ONVIF is usually run on port 8000. This can be 80 as well depending on your camera model.", + "fieldTextAduration": "Specify how many microseconds are analyzed to probe the input. Set to 100000 if you are using RTSP and having stream issues.", + "fieldTextProbesize": "Specify how big to make the analyzation probe for the input. Set to 100000 if you are using RTSP and having stream issues.", + "fieldTextStreamLoop": "Loop a static file so the file stream behaves like a live stream.", + "fieldTextSfps": "Specify the Frame Rate (FPS) in which the camera is providing its stream in.", + "fieldTextWallClockTimestampIgnore": "Base all incoming camera data in camera time instead of server time.", + "fieldTextHeight": "Height of the stream image.", + "fieldTextWidth": "Width of the stream image.", + "fieldTextAccelerator": "Hardware Acceleration (HWAccel) for decoding streams.", + "fieldTextHwaccel": "Decoding Engine", + "fieldTextHwaccelVcodec": "Decoding Engine", + "fieldTextStreamType": "The method that will used to consume the video stream.", + "fieldTextStreamTypePoseidon": "Poseidon is built on Kevin Godell's MP4 processing code. It simulates a streaming MP4 file but using the data of a live stream. Includes Audio. Some browsers can play it like a regular MP4 file. Streams over HTTP or WebSocket.", + "fieldTextStreamTypeBase64OverWebsocket": "Sending Base64 encoded frames over WebSocket. This avoids caching but there is no audio.", + "fieldTextStreamTypeMJPEG": "Standard Motion JPEG image. No audio.", + "fieldTextStreamTypeFLV": "Sending FLV encoded frames over WebSocket.", + "fieldTextStreamTypeHLS(includesAudio)": "Similar method to facebook live streams. Includes audio if input provides it. There is a delay of about 4-6 seconds because this method records segments then pushes them to the client rather than push as while it creates them.", + "fieldTextStreamFlvType": "This is for the Shinobi dashboard only. Both stream methods are still active and ready to use.", + "fieldTextStreamVcodec": "Video codec for streaming.", + "fieldTextStreamVcodecAuto": "Let FFMPEG choose.", + "fieldTextStreamVcodecLibx264": "Used for MP4 video.", + "fieldTextStreamVcodecLibx265": "Used for MP4 video.", + "fieldTextStreamVcodecCopy": "Used for MP4 video. Has very low CPU usage but cannot use video filters and filesizes may be gigantic. Best to setup your MP4 settings camera-side when using this option.", + "fieldTextStreamAcodec": "Audio codec for streaming.", + "fieldTextStreamAcodecAuto": "Let FFMPEG choose.", + "fieldTextStreamAcodecNoAudio": "No Audio, this is an option that must be set in some parts of the world due to legal reasons.", + "fieldTextStreamAcodecLibvorbis": "Used for WebM video.", + "fieldTextStreamAcodecLibopus": "Used for WebM video.", + "fieldTextStreamAcodecLibmp3lame": "Used for MP4 video.", + "fieldTextStreamAcodecAac": "Used for MP4 video.", + "fieldTextStreamAcodecAc3": "Used for MP4 video.", + "fieldTextStreamAcodecCopy": "Used for MP4 video. Has very low CPU usage but some audio codecs need custom flags like -strict 2 for aac.", + "fieldTextHlsTime": "How long each video segment should be, in minutes. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.", + "fieldTextHlsListSize": "The number of segments maximum before deleting old segments automatically.", + "fieldTextPresetStream": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.", + "fieldTextStreamQuality": "Low number means higher quality. Higher number means less quality.", + "fieldTextStreamFps": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.", + "fieldTextStreamScaleX": "Width of the stream image that is output after processing.", + "fieldTextStreamScaleY": "Height of the stream image that is output after processing.", + "fieldTextStreamRotate": "Change the viewing angle of the video stream.", + "fieldTextSignalCheck": "How often your client will check the stream to see if it is alive. This is calculated in minutes.", + "fieldTextSignalCheckLog": "This is for the client side only. It will display in the log thread when client side signal checks occur.", + "fieldTextStreamVf": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.", + "fieldTextTvChannel": "This monitor will have TV Channel features enabled. You will be able to view it in your TV Channel list.", + "fieldTextTvChannelId": "A Custom ID for the Channel.", + "fieldTextTvChannelGroupTitle": "A Custom Group for the Channel.", + "fieldTextStreamTimestamp": "A clock that is burned onto the frames of the video stream.", + "fieldTextStreamTimestampFont": "Font File to style your timestamp.", + "fieldTextStreamTimestampFontSize": "Font size in pt.", + "fieldTextStreamTimestampColor": "Timstamp text color.", + "fieldTextStreamTimestampBoxColor": "Timstamp backdrop color.", + "fieldTextStreamTimestampX": "Horiztonal Position of Timestamp", + "fieldTextStreamTimestampY": "Vertical Position of Timestamp", + "fieldTextStreamWatermark": "An image that is burned onto the frames of the video stream.", + "fieldTextStreamWatermarkLocation": "Image Location that will be used as Watermark.", + "fieldTextStreamWatermarkPosition": "An image that is burned onto the frames of the video stream.", + "fieldTextDetailSubstreamInputRtspTransportAuto": "Let FFMPEG decide. Normally it will try UDP first.", + "fieldTextDetailSubstreamInputRtspTransportTCP": "Set it to this if UDP starts giving undesired results.", + "fieldTextDetailSubstreamInputRtspTransportUDP": "FFMPEG tries this first.", + "fieldTextDetailSubstreamOutputStreamType": "The method that will used to consume the video stream.", + "fieldTextDetailSubstreamOutputStreamVcodec": "Video codec for streaming.", + "fieldTextDetailSubstreamOutputStreamVcodecAuto": "Let FFMPEG choose.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx264": "Used for MP4 video.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx265": "Used for MP4 video.", + "fieldTextDetailSubstreamOutputStreamVcodecCopy": "Used for MP4 video. Has very low CPU usage but cannot use video filters and filesizes may be gigantic. Best to setup your MP4 settings camera-side when using this option.", + "fieldTextDetailSubstreamOutputStreamAcodec": "Audio codec for streaming.", + "fieldTextDetailSubstreamOutputStreamAcodecAuto": "Let FFMPEG choose.", + "fieldTextDetailSubstreamOutputStreamAcodecNoAudio": "No Audio, this is an option that must be set in some parts of the world due to legal reasons.", + "fieldTextDetailSubstreamOutputStreamAcodecLibvorbis": "Used for WebM video.", + "fieldTextDetailSubstreamOutputStreamAcodecLibopus": "Used for WebM video.", + "fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame": "Used for MP4 video.", + "fieldTextDetailSubstreamOutputStreamAcodecAac": "Used for MP4 video.", + "fieldTextDetailSubstreamOutputStreamAcodecAc3": "Used for MP4 video.", + "fieldTextDetailSubstreamOutputStreamAcodecCopy": "Used for MP4 video. Has very low CPU usage but some audio codecs need custom flags like -strict 2 for aac.", + "fieldTextDetailSubstreamOutputHlsTime": "How long each video segment should be, in minutes. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.", + "fieldTextDetailSubstreamOutputHlsListSize": "The number of segments maximum before deleting old segments automatically.", + "fieldTextDetailSubstreamOutputPresetStream": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.", + "fieldTextDetailSubstreamOutputStreamQuality": "Low number means higher quality. Higher number means less quality.", + "fieldTextDetailSubstreamOutputStreamFps": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.", + "fieldTextDetailSubstreamOutputStreamScaleX": "Width of the stream image that is output after processing.", + "fieldTextDetailSubstreamOutputStreamScaleY": "Height of the stream image that is output after processing.", + "fieldTextDetailSubstreamOutputStreamRotate": "Change the viewing angle of the video stream.", + "fieldTextDetailSubstreamOutputSvf": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.", + "fieldTextSnap": "Get the latest frame in JPEG.", + "fieldTextExt": "The file type for your recorded video file.", + "fieldTextExtMP4": "This file type is playable is almost all modern web browsers, that includes mobile. The filesize just tends to be larger unless you lower the quality.", + "fieldTextExtWebM": "Small filesize, low client compatibility. Good for uploading to sites like YouTube.", + "fieldTextVcodec": "Video codec for recording.", + "fieldTextCrf": "Low number means higher quality. Higher number means less quality.", + "fieldTextPresetRecord": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.", + "fieldTextAcodec": "Audio codec for recording.", + "fieldTextFps": "The speed in which frames are recorded to files, Frames Per Second. Be aware there is no default. This can lead to large files. Best to set this camera-side.", + "fieldTextRecordScaleY": "Height of the stream image.", + "fieldTextRecordScaleX": "Width of the stream image.", + "fieldTextCutoff": "In minutes. When to slice off and start a new video file.", + "fieldTextRotate": "Change the recording angle of the video stream.", + "fieldTextVf": "Place FFMPEG video filters in this box to affect the recording portion. No spaces.", + "fieldTextTimestamp": "A clock that is burned onto the frames of the recorded video.", + "fieldTextTimestampFont": "Font File to style your timestamp.", + "fieldTextTimestampFontSize": "Font size in pt.", + "fieldTextTimestampColor": "Timstamp text color.", + "fieldTextTimestampBoxColor": "Timstamp backdrop color.", + "fieldTextTimestampX": "Horiztonal Position of Timestamp", + "fieldTextTimestampY": "Vertical Position of Timestamp", + "fieldTextWatermark": "An image that is burned onto the frames of the recorded video.", + "fieldTextWatermarkLocation": "Image Location that will be used as Watermark.", + "fieldTextWatermarkPosition": "An image that is burned onto the frames of the recorded video.", + "fieldTextRecordTimelapse": "Create a JPEG based timelapse.", + "fieldTextRecordTimelapseMp4": "Create an MP4 file at the end of each day for the timelapse.", + "fieldTextRecordTimelapseWatermark": "An image that is burned onto the frames of the recorded video.", + "fieldTextRecordTimelapseWatermarkLocation": "Image Location that will be used as Watermark.", + "fieldTextRecordTimelapseWatermarkPosition": "An image that is burned onto the frames of the recorded video.", + "fieldTextCustInput": "Custom Flags that bind to the Input of the FFMPEG process.", + "fieldTextCustStream": "Custom Flags that bind to the Stream (client side view) of the FFMPEG process.", + "fieldTextCustSnap": "Custom Flags that bind to the Snapshots.", + "fieldTextCustRecord": "Custom Flags that bind to the recording of the FFMPEG process.", + "fieldTextCustDetect": "Custom Flags that bind to the stream Detector uses for analyzation.", + "fieldTextCustDetectObject": "Custom Flags that bind to the stream Detector uses for analyzation.", + "fieldTextCustSipRecord": "Custom Flags that bind to the output that the Event-Based Recordings siphon from.", + "fieldTextCustomOutput": "Add a custom output like JPEG frames or send data straight to another server.", + "fieldTextDetector": "This will add another output in the FFMPEG command for the motion detector.", + "fieldTextDetectorHttpApi": "Do you want to allow HTTP triggers to this camera?", + "fieldTextDetectorSendFrames": "Push frames to the connected plugin to be analyzed.", + "fieldTextDetectorFps": "How many frames a second to send to the motion detector; 2 is the default.", + "fieldTextDetectorScaleX": "Width of the image being detected. Smaller sizes take less CPU.", + "fieldTextDetectorScaleY": "Height of the image being detected. Smaller sizes take less CPU.", + "fieldTextDetectorLockTimeout": "Lockout for when the next trigger is allowed, to avoid overloading the database and receiving clients. Measured in milliseconds.", + "fieldTextDetectorSave": "Save Events in the database. This will allow display of the events over video during playback.", + "fieldTextDetectorRecordMethod": "There are multiple ways to begin recording when an event occurs, like motion. Event-Based Recording is the most user-friendly.", + "fieldTextDetectorTrigger": "This will order the camera to record if it is set to \"Watch-Only\" when an Event is detected.", + "fieldTextDetectorTimeout": "The length of time \"Trigger Record\" will run for. This is read in minutes.", + "fieldTextWatchdogReset": "Enable to reset the active Recording Timeout back to the beginning when a new event occurs while recording.", + "fieldTextDetectorWebhook": "Send a GET request to a URL with some values from the event.", + "fieldTextDetectorWebhookTimeout": "This value is a timer to allow the next running of your Webhook. This value is in minutes.", + "fieldTextDetectorCommand": "The command that will run. This is the equivalent of running a shell command from terminal.", + "fieldTextDetectorCommandTimeout": "This value is a timer to allow the next running of your script. This value is in minutes.", + "fieldTextSnapSecondsInward": "in seconds", + "fieldTextDetectorPam": "Use Kevin Godell's Motion Detector. This is built into Shinobi and requires no other configuration to activate.", + "fieldTextDetectorSensitivity": "The motion confidence rating must exceed this value to be seen as a trigger. This number correlates directly to the confidence rating returned by the motion detector. This option was previously named \"Indifference\".", + "fieldTextDetectorMaxSensitivity": "The motion confidence rating must be lower than this value to be seen as a trigger. Leave blank for no maximum. This option was previously named \"Max Indifference\".", + "fieldTextDetectorThreshold": "Minimum number of detections to fire a motion event. Detections must be within the detector the threshold divided by detector fps seconds. For example, if detector fps is 2 and trigger threshold is 3, then three detections must occur within 1.5 seconds to trigger a motion event. This threshold is per detection region.", + "fieldTextDetectorColorThreshold": "The amount of difference allowed in a pixel before it is considered motion.", + "fieldTextInverseTrigger": "To trigger outside specified regions. Will not trigger with Full Frame Detection enabled.", + "fieldTextDetectorFrame": "This will read the entire frame for pixel differences. This is the same as creating a region that covers the entire screen. If no Region is added to this Monitor this option will default to Yes.", + "fieldTextDetectorNoiseFilter": "Attempt to filter grain or repeated motion at a particular indifference.", + "fieldTextDetectorNoiseFilterRange": "The amount of difference allowed in a pixel before it is considered motion.", + "fieldTextDetectorNotrigger": "Check if motion has occured on an interval. If motion has occurred the check will be reset.", + "fieldTextDetectorNotriggerTimeout": "Timeout is calculated in minutes.", + "fieldTextDetectorNotriggerDiscord": "If motion has not been detected after the timeout period you will recieve an Discord notification.", + "fieldTextDetectorNotriggerWebhook": "Send a GET request to a URL with some values from the event.", + "fieldTextDetectorNotriggerCommand": "The command that will run. This is the equivalent of running a shell command from terminal.", + "fieldTextDetectorNotriggerCommandTimeout": "This value is a timer to allow the next running of your script. This value is in minutes.", + "fieldTextDetectorAudio": "Check if Audio has occured at a certiain decible. Decible reading may not be accurate to real-world measurement.", + "fieldTextDetectorUseDetectObject": "Create frames for sending to any connected Plugin.", + "fieldTextDetectorSendFramesObject": "Push frames to the connected plugin to be analyzed.", + "fieldTextDetectorObjCountInRegion": "Count Objects only inside Regions.", + "fieldTextDetectorLisencePlate": "Enable License Plate Recognition. OpenALPR plugin has this always enabled.", + "fieldTextDetectorLisencePlateCountry": "Choose the type of plates to recognize. Only US and EU are supported at this time.", + "fieldTextEventRecordScaleX": "Width of the Event-based Recording image that is output after processing.", + "fieldTextEventRecordScaleY": "Height of the Event-based Recording image that is output after processing.", + "fieldTextDetectorBufferHlsTime": "How long each video segment should be, in seconds. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.", + "fieldTextDetectorBufferHlsListSize": "The number of segments maximum before deleting old segments automatically.", + "fieldTextDetectorPtzFollow": "Follow the largest detected object with PTZ? Requires an Object Detector running or matrices provided with events.", + "fieldTextDetectorObjCount": "Count detected objects.", + "fieldTextControlInvertY": "For When your camera is mounted upside down or uses inverted vertical controls.", + "fieldTextDetectorSendVideoLength": "In seconds. The length of the video that gets sent to your Notification service, like Email or Discord.", + "fieldTextLoglevel": "The amount of data to provide while doing the job.", + "fieldTextLoglevelSilent": "None. This will silence all logging.", + "fieldTextLoglevelFatal": "Display only fatal errors.", + "fieldTextLoglevelOnError": "Display all important errors. Note : this doesn't always show important information.", + "fieldTextLoglevelAllWarnings": "Display all warnings. Use this if you can't find out what's wrong with your camera.", + "fieldTextSqllog": "Use this with caution as FFMPEG likes to throw up superfluous data at times which can lead to a lot of database rows.", + "fieldTextSqllogNo": "No is the default.", + "fieldTextSqllogYes": "Do this if you are having recurring issues only.", + "fieldTextFactorAuth": "Enable a secondary requirement for login through one of the enabled methods.", + "fieldTextMail": "The login for accounts. The main account holder's email address will get notifications.", + "fieldTextPass": "Leave blank to keep the same password during settings modification.", + "fieldTextPasswordAgain": "Must match Password field if you desire to change it.", + "fieldTextSize": "The amount of disk space Shinobi will allow to be consumed before purging. This value is read in megabytes.", + "fieldTextSizeVideoPercent": "Percent of Max Storage Amount the videos can record to.", + "fieldTextSizeTimelapsePercent": "Percent of Max Storage Amount the timelapse frames can record to.", + "fieldTextSizeFilebinPercent": "Percent of Max Storage Amount the FileBin archive can use.", + "fieldTextDays": "The number of days to keep videos before purging.", + "fieldTextEventDays": "The number of days to keep events before purging.", + "fieldTextLogDays": "The number of days to keep logs before purging.", + "fieldTextLang": "The primary language of text elements. For complete translation add your language in conf.json e.g:\"language\": \"en_CA\",", + "fieldTextAudioNote": "Sound when information bubble appears.", + "fieldTextAudioAlert": "Sound when Event occurs.", + "fieldTextAudioDelay": "Delay until next time an Event can start an Alert. Measured in seconds.", + "fieldTextEventMonPop": "When an Event occurs popout the monitor stream.", + "fieldTextIrCutFilterOn": "Enable Ir cut fiter. Typically Day mode.", + "fieldTextIrCutFilterOff": "Disable Ir cut fiter. Typically Night mode.", + "fieldTextIrCutFilterAuto": "Ir cut filter is automatically activated by the device.", + "fieldTextIp": "Range or Single", + "fieldTextActionsHalt": "Make the event do nothing, as if it never happened.", + "fieldTextActionsIndifference": "Modify minimum indifference required for event.", + "fieldTextActionsCommand": "You may use this to trigger a script on command.", + "fieldTextActionsRecord": "Use Event-Based Recording, Hotswap, or Delete Motionless with their currently set options in the Global Detection Settings section.", + "fieldTextMapRtspTransportAuto": "Let FFMPEG decide. Normally it will try UDP first.", + "fieldTextMapRtspTransportTCP": "Set it to this if UDP starts giving undesired results.", + "fieldTextMapRtspTransportUDP": "FFMPEG tries this first.", + "fieldTextChannelStreamType": "The method that will used to consume the video stream.", + "fieldTextChannelStreamTypePoseidon": "Poseidon is built on Kevin Godell's MP4 processing code. It simulates a streaming MP4 file but using the data of a live stream. Includes Audio. Some browsers can play it like a regular MP4 file. Streams over HTTP or WebSocket.", + "fieldTextChannelStreamTypeMJPEG": "Standard Motion JPEG image. No audio.", + "fieldTextChannelStreamTypeFLV": "Sending FLV encoded frames over WebSocket.", + "fieldTextChannelStreamTypeHLS(includesAudio)": "Similar method to facebook live streams. Includes audio if input provides it. There is a delay of about 4-6 seconds because this method records segments then pushes them to the client rather than push as while it creates them.", + "fieldTextChannelStreamVcodec": "Video codec for streaming.", + "fieldTextChannelStreamVcodecAuto": "Let FFMPEG choose.", + "fieldTextChannelStreamVcodecLibx264": "Used for MP4 video.", + "fieldTextChannelStreamVcodecLibx265": "Used for MP4 video.", + "fieldTextChannelStreamVcodecCopy": "Used for MP4 video. Has very low CPU usage but cannot use video filters and filesizes may be gigantic. Best to setup your MP4 settings camera-side when using this option.", + "fieldTextChannelStreamAcodec": "Audio codec for streaming.", + "fieldTextChannelStreamAcodecAuto": "Let FFMPEG choose.", + "fieldTextChannelStreamAcodecNoAudio": "No Audio, this is an option that must be set in some parts of the world due to legal reasons.", + "fieldTextChannelStreamAcodecLibvorbis": "Used for WebM video.", + "fieldTextChannelStreamAcodecLibopus": "Used for WebM video.", + "fieldTextChannelStreamAcodecLibmp3lame": "Used for MP4 video.", + "fieldTextChannelStreamAcodecAac": "Used for MP4 video.", + "fieldTextChannelStreamAcodecAc3": "Used for MP4 video.", + "fieldTextChannelStreamAcodecCopy": "Used for MP4 video. Has very low CPU usage but some audio codecs need custom flags like -strict 2 for aac.", + "fieldTextChannelHlsTime": "How long each video segment should be, in minutes. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.", + "fieldTextChannelHlsListSize": "The number of segments maximum before deleting old segments automatically.", + "fieldTextChannelPresetStream": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.", + "fieldTextChannelStreamQuality": "Low number means higher quality. Higher number means less quality.", + "fieldTextChannelStreamFps": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.", + "fieldTextChannelStreamScaleX": "Width of the stream image that is output after processing.", + "fieldTextChannelStreamScaleY": "Height of the stream image that is output after processing.", + "fieldTextChannelStreamRotate": "Change the viewing angle of the video stream.", + "fieldTextChannelSvf": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.", + "Last Updated": "Last Updated", + "Z-Wave Manager": "Z-Wave Manager", + "Z-Wave": "Z-Wave" } diff --git a/languages/it.json b/languages/it.json new file mode 100644 index 00000000..c67e1c41 --- /dev/null +++ b/languages/it.json @@ -0,0 +1,1652 @@ +{ + "\"No Motion\" Detector": "Riflettore \"nessun movimento\"", + "# of Allow MJPEG Clients": "# di consentire clienti mjpeg 0 per infinite ", + "'Already Installing...'": "\"Già installando ...\"", + "180 Degrees": "180 gradi", + "2-Factor Authentication": "Autenticazione a 2 fattori", + "90 Clockwise": "90 in senso orario", + "90 Clockwise and Vertical Flip": "90 Flip in senso orario e verticale", + "90 Counter Clockwise and Vertical Flip (default)": "90 Flip in senso antiorario e verticale (impostazione predefinita)", + "AND": "E", + "API": "API", + "API Key": "Chiave API", + "API Key Action Failed": "Azione chiave API non è riuscita", + "API Key Added": "Chiave API aggiunta", + "API Key Deleted": "Chiave API Eliminata", + "API Keys": "Tasti API", + "APIKeyAddedText": "Puoi usare questa chiave ora.", + "APIKeyDeletedText": "La chiave è stata eliminata. Non funzionerà più.", + "ASC": "Asc", + "Accelerator": "Acceleratore", + "Account Edited": "Account modificato", + "Account Info": "Informazioni sull'account", + "Account Information": "Informazioni account", + "Account Privileges": "Privilegi di account", + "Account Save": "Salva account", + "Account Settings": "Impostazioni dell'account", + "AccountEditText1": "Non poteva modificare. Aggiorna pagina se il problema continua.", + "Accounts": "Account", + "Action for Selected": "Azione per selezionata", + "Activated": "Attivato", + "Active Monitors": "Monitor attivi", + "Add": "Aggiungere", + "Add All": "Aggiungi tutto", + "Add Camera": "Aggiungi la fotocamera", + "Add Cameras": "Aggiungi telecamere", + "Add Channel": "Aggiungi canale", + "Add Input Feed": "Aggiungi feed di input", + "Add Map": "Aggiungi mappa", + "Add Monitor": "Aggiungi monitor", + "Add New": "Aggiungere nuova", + "AddToPreset": "Aggiungi a preimpostazione", + "Additional Inputs": "Input aggiuntivi", + "Admin": "Amministratrice", + "Advanced": "Avanzate", + "After": "Dopo", + "Again": "Ancora", + "Age": "Età", + "Alert Sound": "Suono di allerta", + "Alert Sound Delay": "Avviso il ritardo del suono", + "All Logs": "Tutti i registri", + "All Monitors": "Tutti i monitor", + "All Monitors and Privileges": "Tutti i monitor e i privilegi", + "All Privileges": "Tutti i privilegi", + "All Warnings": "Tutti gli avvertimenti", + "All streams in first feed": "Tutti i flussi nel primo feed", + "Allow API Trigger": "Consenti il trigger API", + "Allow Next Alert": "Consenti l'allerta successiva", + "Allow Next Command": "Consenti il comando successivo", + "Allow Next Discord Alert": "Consenti l'allerta discordia successiva in minuti ", + "Allow Next Email": "Consenti email successiva in minuti ", + "Allow Next Trigger": "Consenti il grilletto successivo", + "Allow Next Webhook": "Consenti il prossimo webhook", + "Allowed IPs": "Consentito IPS", + "Already Processing": "Già elaborazione", + "Already exists": "Esiste già", + "Alternate Logins": "Accessi alternativi", + "Always": "Sempre", + "Amazon S3": "Amazon S3", + "Amazon S3 Upload Error": "Errore di caricamento di Amazon S3", + "Analyzation Duration": "Durata dell'analisi", + "AppNotEnabledText": "App non abilitata, abilitalo nelle impostazioni dell'account.", + "April": "aprile", + "Archive": "Archivio", + "Are you sure?": "Sei sicuro?", + "Attach Snapshot": "Allega l'istantanea", + "Attach Video Clip": "Allega video clip", + "Audio": "Audio", + "Audio Bit Rate": "Bitrate audio", + "Audio Codec": "Codec audio", + "Audio Detection": "Rilevamento audio", + "Audio Detector": "Rilevatore audio", + "Audio stream only from first feed": "Flusso audio solo dal primo feed", + "Audio streams only": "Solo flussi audio", + "August": "agosto", + "Authenticate": "Autenticare", + "Authenticated": "Autenticata", + "Authentication Failed": "Autenticazione fallita", + "Auto": "Auto", + "Automatic": "Automatica", + "Automatic Checking Cancelled": "Controllo automatico annullato", + "Automatic Codec Repair": "Riparazione del codec automatico", + "Automatic Field Fill": "Riempimento del campo automatico", + "Autosave": "Autosave", + "Back": "Di ritorno", + "Backblaze B2": "Backblaze B2", + "Backblaze Error": "Errore di backblaze", + "BacklightCompensation": "Compensazione retroilluminazione", + "Backup": "Backup", + "Bandwidth": "Larghezza di banda", + "Base64 over Websocket": "Base64 su WebSocket", + "Basic Authentication": "Autenticazione di base", + "Batch": "Lotto", + "Before": "Prima", + "Bind Credentials": "Bind Credentials (password)", + "BitrateLimit": "Limite bitrate", + "Blank for No Change": "Vuoto per nessun cambiamento", + "Bottom Left": "In basso a sinistra", + "Bottom Right": "In basso a destra", + "Brightness": "Luminosità", + "Browser Console Log": "Registro della console del browser", + "Bucket": "Benna", + "Buffer Preview": "Anteprima del buffer", + "Build": "Costruire", + "Build Video": "Costruisci video", + "Building": "Costruzione", + "CPU": "processore", + "CPU indicator will not work. Continuing...": "L'indicatore della CPU non funzionerà. Continuando ...", + "CPU used by this stream": "CPU utilizzata da questo flusso", + "CSS": "CSS stile la tua dashboard. ", + "Calendar": "Calendario", + "Call Method": "Metodo di chiamata", + "Camera Password": "Password della fotocamera", + "Camera Username": "Nome utente della fotocamera", + "Camera is not recording": "La fotocamera non sta registrando", + "Camera is not running": "La fotocamera non funziona", + "Camera is not streaming": "La fotocamera non è in streaming", + "CameraNotRecordingText": "Le impostazioni possono essere incompatibili. Controlla gli encoder. Riavviamento ...", + "Can Authenticate Websocket": "Può autenticare WebSocket", + "Can Change User Settings": "Può modificare le impostazioni dell'utente", + "Can Control Monitors": "Può controllare i monitor", + "Can Create and Delete Monitors": "Può creare ed eliminare i monitor", + "Can Delete Videos": "Può eliminare i video", + "Can Delete Videos and Events": "Può eliminare video ed eventi", + "Can Edit Monitor": "Può modificare il monitor", + "Can Get Logs": "Può ottenere tronchi", + "Can Get Monitors": "Può ottenere monitor", + "Can View Logs": "Può visualizzare i registri", + "Can View Monitor": "Può visualizzare il monitor", + "Can View Snapshots": "Può visualizzare le istantanee", + "Can View Streams": "Può visualizzare i flussi", + "Can View Videos": "Può visualizzare i video", + "Can View Videos and Events": "Può visualizzare video ed eventi", + "Can edit Max Days": "Può modificare i giorni massimi", + "Can edit Max Storage": "Può modificare l'archiviazione massima", + "Can edit how long to keep Events": "Può modificare quanto tempo per mantenere gli eventi", + "Can edit how long to keep Logs": "Può modificare quanto tempo per mantenere i registri", + "Can use Admin Panel": "Può utilizzare il pannello di amministrazione", + "Can use Amazon S3": "Può usare Amazon S3", + "Can use Discord Bot": "Può usare Discord Bot", + "Can use LDAP": "Può usare LDAP", + "Can use SFTP": "Può usare SFTP", + "Can use Wasabi Hot Cloud Storage": "Può utilizzare Wasabi Hot Cloud Storage", + "Can use WebDAV": "Può utilizzare WebDav", + "Can't Connect": "Non posso connettersi", + "Cannot watch a monitor that isn't running.": "Non riesco a guardare un monitor che non funziona.", + "Cards": "Carte", + "Carousel in Background": "Giostra in background", + "Center": "Center Indirizzo URL ", + "Channel": "Canale", + "Channel ID": "Canale ID", + "Chat on Discord": "Chatta su discordia", + "Check": "Dai un'occhiata", + "Check Signal Interval": "Controllare l'intervallo del segnale in minuti ", + "Check for Motion First": "Controlla prima il movimento", + "Check the Channel ID": "Controlla l'ID canale", + "Check the Recipient ID": "Controlla l'ID destinatario", + "Clear": "Chiara", + "Clear Recorder Process": "Processo di registratore chiaro", + "Close": "Chiudere", + "Close All Monitors": "Chiudi tutti i monitor", + "Closed": "Chiusa", + "Cloud": "Nube", + "Codec Mismatch": "Codec Mismatch", + "Color Threshold": "Soglia di colore", + "ColorSaturation": "Saturazione del colore", + "Command": "Comando", + "Command on Trigger": "Comando su trigger", + "Common Objects": "Oggetti comuni", + "Complete Stream URL": "URL di flusso completo", + "Conditions": "Condizioni", + "Confidence": "Fiducia", + "Confidence of Detection": "Fiducia di rilevamento", + "Configuration": "Configurazione", + "Confirm": "Confermare", + "Connected": "Collegata", + "Connected Users": "Utenti connessi", + "Connection": "Connessione", + "Connection Type": "Tipo di connessione", + "Connection timed out": "Connessione Scaduta", + "Contains": "Contiene", + "Contrast": "Contrasto", + "Control": "Controllo", + "Control Error": "Errore di controllo", + "Control Trigger Started": "Trigger di controllo avviato", + "Control Triggered": "Controllo attivato", + "ControlErrorText1": "Il controllo non è abilitato", + "ControlErrorText2": "Controlla i dettagli della tua connessione. Potrebbe essere necessario puntare l'URL di base alla porta 8000 o 80. Controllare le informazioni di autenticazione.", + "Controllable": "Controllabile", + "Controls and Logs": "Controlli e registri", + "Copied": "Copiato", + "Copied to Clipboard": "Copiato negli appunti", + "Copy": "copia", + "Copy Connection Settings": "Copia impostazioni di connessione", + "Copy Custom Settings": "Copia impostazioni personalizzate", + "Copy Detector Settings": "Impostazioni del rivelatore di copia", + "Copy Group Settings": "Copia impostazioni del gruppo", + "Copy Input Settings": "Copia impostazioni di input", + "Copy JPEG API Settings": "Copia le impostazioni dell'API JPEG", + "Copy Logging Settings": "Copia impostazioni di registrazione", + "Copy Mode": "Modalità copia", + "Copy Recording Settings": "Copia impostazioni di registrazione", + "Copy Remote Link": "Copia link remoto", + "Copy Settings": "Copia impostazioni", + "Copy Stream Channel Settings": "Copia le impostazioni del canale del flusso", + "Copy Stream Channels": "Copia canali di streaming", + "Copy Stream Settings": "Copia le impostazioni del flusso", + "Copy Stream URL": "Copia URL in streaming", + "Copy Timelapse Settings": "Copia impostazioni timelapse", + "Copy to Settings": "Copia alle impostazioni", + "Cores": "Core", + "Could not create Bucket.": "Impossibile creare un secchio.", + "Count Objects": "Contare gli oggetti", + "Count Objects only inside Regions": "Contare gli oggetti solo all'interno delle regioni", + "Country of Plates": "Paese di piatti", + "Counts of Motion": "Conteggi di movimento", + "Create Sub-Accounts at /admin": "Crea sottoconunti a /admin", + "Creating New Account": "Creazione di un nuovo account", + "Creation Interval": "Intervallo di creazione", + "Current": "Attuale", + "Currently Active": "Attualmente attivo", + "Currently viewing": "Attualmente visualizzazione", + "Custom": "Costume", + "Custom Auto Load": "Carica automatica personalizzata", + "Custom Base URL": "URL di base personalizzato lascia vuoto per utilizzare l'URL host ", + "Custom Endpoint": "Endpoint personalizzato", + "DB Lost.. Retrying..": "Database perso .. Retring ..", + "DESC": "Desc", + "DHCP": "Dhcp", + "DNS": "DNS", + "Daily Events": "Eventi quotidiani", + "Dashboard": "Pannello di controllo", + "Dashboard Language": "Linguaggio della dashboard", + "Dashcam": "Dashcam", + "Dashcam (Streamer v2)": "DashCam (Streamer V2)", + "Database": "Banca dati", + "Database Not Found": "Database non trovato", + "Database row does not exist": "La riga del database non esiste", + "Date": "Data", + "Date Added": "Data aggiunta", + "Date Range": "Intervallo di date", + "Date Updated": "Data aggiornata", + "Date and Time": "Data e ora", + "DateTimeType": "Gestione della data", + "DaylightSavings": "Risparmio diurno", + "Days": "Giorni", + "Debug": "Debug", + "December": "Dicembre", + "Default": "Predefinita", + "Delay for Snapshot": "Ritardo per l'istantanea", + "Delete": "Elimina", + "Delete Camera": "Elimina la fotocamera", + "Delete Filter": "Elimina filtro", + "Delete Logs": "Elimina i registri", + "Delete Matches": "Elimina le partite", + "Delete Monitor": "Elimina il monitor", + "Delete Monitor State?": "Elimina lo stato di monitor", + "Delete Monitor States Preset": "Elimina gli stati del monitor preimpostazione", + "Delete Monitors and Files": "Elimina monitor e file", + "Delete Motionless Video": "Elimina video immobili", + "Delete Motionless Videos (Record)": "Elimina video immobili (record)", + "Delete Region": "Elimina la regione", + "Delete Schedule": "Elimina il programma", + "Delete Selected Videos": "Elimina video selezionati", + "Delete Timelapse Frame": "Elimina il telaio timelapse", + "Delete Video": "Elimina video", + "Delete selected": "Elimina selezionato", + "DeleteMonitorText": "Vuoi eliminare questo monitor? Non puoi recuperarlo. È possibile scegliere che i file rimangano nel filesystem. Se si sceglie di ricreare un monitor con lo stesso ID, i video e gli eventi diventeranno visibili nella dashboard.", + "DeleteMonitorsText": "Vuoi eliminare questi monitor? Non puoi recuperarli. Puoi scegliere di conservare i file per questi ID nel filesystem. Se si sceglie di ricreare un monitor con uno degli ID, i video e gli eventi diventeranno visibili nella dashboard.", + "DeleteSelectedVideosMsg": "Vuoi eliminare questi video? Non puoi recuperarli.", + "DeleteThisMsg": "Vuoi eliminare questo? Non puoi recuperarlo.", + "DeleteVideoMsg": "Vuoi eliminare questo video? Non puoi recuperarlo.", + "Deleted": "Cancellato", + "Deleted Schedule Configuration": "Configurazione del programma eliminato", + "Deleted State Configuration": "Configurazione dello stato eliminato", + "Detect Objects": "Rileva oggetti vedi sotto ", + "Detection": "Rilevamento", + "Detection Engine": "Motore di rilevamento", + "Detection Event": "Evento di rilevamento", + "Detector": "Rivelatore", + "Detector Buffer": "Tampone rivelatore", + "Detector Filters": "Filtri del rivelatore", + "Detector Flags": "Flag del rivelatore", + "Detector Grouping": "Raggruppamento del rivelatore aggiungi gruppi in impostazioni ", + "Detector Rate": "Tasso rilevatore (fps) ", + "Detector Recording Complete": "Registrazione del rivelatore completo", + "Detector Recording Process Exited Prematurely. Restarting.": "Il processo di registrazione del rivelatore è uscito prematuramente. Riavvio.", + "DetectorText": "

Quando vengono mostrate le caselle di larghezza e altezza, dovresti impostarle su 640x480 o inferiore. Ciò ottimizzerà la velocità di lettura dei frame.

", + "Died": "Morta", + "Digest Authentication": "Digest Autentication", + "Disable": "disattivare", + "Disable Night Vision": "Disabilita visione notturna Indirizzo URL ", + "Disable Nightvision": "Disabilita NightVision", + "Disabled": "Disabilitata", + "Discord": "Discordia", + "Discord Alert on Trigger": "Discord Alert sul grilletto", + "Discord Bot": "Discord Bot", + "Discord on No Motion": "Discord su \"No Motion\"", + "DiscordErrorText": "L'invio a discord ha causato un errore", + "DiscordFailedText": "L'invio a discord non è riuscito", + "DiscordLoggedIn": "Discord Bot autenticato", + "DiscordNotEnabledText": "Discord Bot non abilitato, abilitalo nelle impostazioni dell'account.", + "Documentation": "Documentazione", + "Does Not Contain": "Non contiene", + "Don't Show for 1 Week": "Non mostrare per 1 settimana", + "Don't Stretch Monitors": "Non allungare i monitor", + "Don't show this anymore": "Non mostrarlo più", + "DontAddToPreset": "Non aggiungere a preimpostazione", + "Double Quote Directory": "Directory a doppia citazione Alcune directory hanno spazi. Usando questo può crash alcune telecamere. ", + "Down": "DOWN Indirizzo URL ", + "Down Stop": "STOP DOWN Indirizzo URL ", + "Download": "Scarica", + "Download Bandwidth": "Scarica la larghezza di banda", + "Downloading Videos": "Download di video", + "Downloading...": "Download ...", + "Duplicate": "Duplicare", + "EU": "Unione Europea", + "Easy Remote Access (P2P)": "Facile accesso remoto (P2P)", + "Edit": "Modificare", + "Edit Configuration": "Modifica configurazione", + "Edit Selected": "Modifica selezionata", + "Edited Schedule Configuration": "Configurazione del programma modificato", + "Edited State Configuration": "Configurazione dello stato modificato", + "Email": "E-mail", + "Email Details": "Dettagli e -mail", + "Email address is in use.": "L'indirizzo e -mail è in uso.", + "Email and Password fields cannot be empty": "I campi e -mail e password non possono essere vuoti", + "Email on No Motion": "Email su \"No Motion\"", + "Email on Trigger": "Invia un'e -mail su trigger", + "Emotion": "Emozione", + "Emotion Average": "Media emozione", + "Enable": "Abilitare", + "Enable Night Vision": "Abilita visione notturna Indirizzo URL ", + "Enable Nightvision": "Abilita NightVision", + "Enabled": "Abilitata", + "Encoding": "Codifica", + "EncodingInterval": "I-frame", + "End": "Fine", + "End Time": "Tempo scaduto", + "Ended": "Conclusa", + "Endpoint": "Endpoint", + "Endpoint Address": "Indirizzo endpoint", + "Enlarge": "Ingrandire", + "Enter at least one IP": "Immettere almeno un IP", + "Enter this code to proceed": "Immettere questo codice per procedere", + "Equal to": "Uguale a", + "Error Connecting": "Connessione all'errore", + "Error While Decoding": "Errore durante la decodifica", + "ErrorWhileDecodingText": "L'hardware potrebbe avere una connessione instabile alla rete. Controlla le connessioni di rete.", + "ErrorWhileDecodingTextAudio": "La fotocamera fornisce dati rotti. Prova a disabilitare l'audio nelle impostazioni interne della fotocamera.", + "Event": "Evento", + "Event Counts": "Conta degli eventi", + "Event Filter Error": "Errore del filtro dell'evento", + "Event Filters": "Filtri degli eventi", + "Event Limit": "Limite di eventi", + "Event Occurred": "Si è verificato un evento", + "Event Rules": "Regole degli eventi", + "Event Webhook Error": "ERRORE WebHook Event", + "EventText1": "Ha innescato un evento a", + "EventText2": "Non poteva e -mail immagine, il file non era accessibile", + "Events": "Eventi", + "Events Found": "Eventi trovati", + "Example": "Esempio", + "Execute Command": "Eseguire il comando", + "Executed": "Eseguito", + "Export": "Esportare", + "Export Selected Videos": "Esporta video selezionati", + "Export Video": "Video di esportazione", + "ExportSelectedVideosMsg": "Vuoi esportare questi video? Potrebbe volerci del tempo per chiudere e scaricare.", + "Exposure": "Esposizione", + "FFmpegCantStart": "FFMPEG non poteva iniziare", + "FFmpegCantStartText": "Il motore di registrazione per questa fotocamera non è stato possibile avviarsi. Potrebbe esserci qualcosa di sbagliato nella configurazione della fotocamera. Se ci sono registri diversi da questo, pubblicali nei problemi su GitHub.", + "FFmpegTip": "FFPROBE è un semplice analizzatore di flussi multimediali. È possibile utilizzarlo per produrre tutti i tipi di informazioni su un input che include durata, velocità del frame, dimensioni del frame, ecc.", + "FFprobe": "Sonda", + "FLV": "Flv", + "FLV Stream Type": "Tipo di flusso FLV", + "FactorAuthText1": "Il codice sarà attivo solo per 15 minuti. Se si accede di nuovo, il timer verrà ripristinato a 15 minuti con lo stesso codice.", + "Failed to Edit Account": "Impossibile modificare l'account", + "Fatal": "Fatale", + "Fatal Maximum Reached": "La fotocamera di arresto massimo fatale, di arresto.", + "FatalMaximumReachedText": "L'errore JPEG è stato fatale.", + "February": "febbraio", + "Feed-in Image Height": "Altezza dell'immagine alimentare", + "Feed-in Image Width": "Larghezza dell'immagine di alimentazione", + "Female": "Femmina", + "Field Missing Value": "Valore mancante del campo", + "Fields cannot be empty": "I campi non possono essere vuoti", + "File Delete Error": "Errore di eliminazione del file", + "File Not Exist": "Il file non esiste", + "File Not Found": "File non trovato", + "File Not Found in Database": "File non trovato nel database", + "File Not Found in Filesystem": "File non trovato nel filesystem", + "File Type": "Tipo di file", + "FileBin Share": "FileBin Share", + "FileNotExistText": "Impossibile salvare file non esistenti. Qualcosa è andato storto.", + "Filename": "Nome del file", + "Filesize": "Dimensione del file", + "Filter ID": "Filtro ID", + "Filter Matches": "Filtro corrispondenze", + "Filter Name": "Nome filtro", + "Filter for Objects only": "Filtrare solo gli oggetti", + "FilterMatchesText1": "Questo filtro ha incontrato le condizioni.", + "FilterMatchesText2": "video trovati.", + "Filters": "Filtri", + "Filters Updated": "Filtri aggiornati", + "FiltersUpdatedText": "Le tue modifiche sono state salvate e applicate.", + "Find Where": "Trova dove", + "First stream in feed": "Primo flusso in feed", + "Fix": "Aggiustare", + "Fix Video": "Correggi video", + "FixVideoMsg": "Vuoi sistemare questo video? Non puoi annullare questa azione.", + "Flush PM2 Logs": "Flush Logs PM2", + "Font Path": "PATURA DI FONT", + "Font Size": "Dimensione del font", + "For Group": "Per il gruppo", + "Force Monitors Per Row": "Monitor di forza per riga", + "Force Port": "Porta di forza", + "Form Data Not Found": "Modulo dati non trovati", + "Found Devices": "Dispositivi trovati", + "Frame Rate": "Frequenza dei fotogrammi", + "FrameRateLimit": "Limite di rate fotogrammi (FPS)", + "Frames": "Cornici", + "Friday": "Venerdì", + "Frigate": "Fregata", + "Full Frame Detection": "Rilevamento completo del telaio", + "Full Stream URL": "URL flusso completo", + "Full URL Path": "Percorso URL completo", + "Fullscreen": "A schermo intero", + "Gateway": "Gateway", + "Gender": "Genere", + "Generate Subtitles": "Generare sottotitoli", + "Get Code": "Ottenere il codice", + "Get Logs to Client": "Ottieni tronchi al client", + "Global Detector Settings": "Impostazioni del rivelatore globale", + "Google Drive": "Google Drive", + "GovLength": "Gov", + "Greater Than": "Più grande di", + "Greater Than or Equal to": "Maggiore o uguale a", + "Grid": "Griglia", + "Group Key": "Chiave di gruppo", + "Group Key is in use.": "La chiave di gruppo è in uso.", + "Group Name": "Nome del gruppo", + "Grouping": "Raggruppamento", + "H.264 / H.265 / H.265+": "H.264 / H.265 / H.265+", + "H264Profile": "Profilo H264", + "HEVC (H.265)": "HEVC (H.265)", + "HLS (.m3u8)": "HLS (.M3U8)", + "HLS (includes Audio)": "HLS (include audio)", + "HLS Audio Encoder": "Encoder audio", + "HLS List Size": "Dimensione dell'elenco", + "HLS Live Start Index": "HLS Live Start Index", + "HLS Preset": "Modello preimpostata", + "HLS Segment Length": "Lunghezza del segmento in secondi ", + "HLS Start Number": "Numero di inizio HLS", + "HLS Video Encoder": "Encoder video", + "HTTP": "Http", + "HTTPS": "Https", + "Hardware Accelerated": "Hardware accelerato", + "Height": "Altezza", + "Help": "Aiuto", + "Hide List": "Nascondi elenco", + "Hide Notes": "Nascondere le note", + "Home": "Casa", + "Host": "Ospite", + "Host Type": "Tipo host", + "Hostname": "Nome host", + "Hotswap Modes (Watch-Only)": "Modalità Hotswap (solo watch)", + "How to Record": "Come registrare", + "IP Address": "Indirizzo IP", + "Identity": "Identità", + "IdentityText1": "Ecco come il sistema identificherà i dati per questo flusso. Non è possibile modificare l'ID monitor una volta premuto Salva. Se vuoi puoi rendere l'ID Monitor più leggibile umano prima di continuare.", + "IdentityText2": "È possibile duplicare un monitor modificando l'ID monitor , quindi premendo Salva. non è possibile utilizzare l'ID di un monitor che esiste già o risparmierà le informazioni sul database di quel monitor.", + "Idle": "Oziare", + "Image Height": "Altezza dell'immagine", + "Image Location": "Posizione dell'immagine", + "Image Position": "Posizione dell'immagine", + "Image Width": "Larghezza dell'immagine", + "Imaging": "Imaging", + "Import": "Importare", + "Import Monitor Configuration": "Configurazione del monitor di importazione", + "ImportMonitorConfigurationText": "In questo modo si scriverà eventuali modifiche attualmente non salvate. Le modifiche importate verranno applicate solo quando si preme Salva .", + "ImportMultiMonitorConfigurationText": "In questo modo si scruterà eventuali monitor con ID esistenti nel file di importazione.", + "In": "Nella", + "Incorrect Settings Chosen": "Impostazioni errate scelte", + "Indifference": "Indifferenza", + "Info": "Informazioni", + "Information": "Informazione", + "Input": "Ingresso", + "Input Feed": "Feed di ingresso", + "Input Feeds Selected": "Feed di ingresso selezionato", + "Input Flags": "Flag di input", + "Input Map": "Mappa di input", + "Input Selector": "Selettore di input", + "Input Settings": "Impostazioni di input", + "Input Type": "Tipo di input", + "InputText1": "Questa sezione dice a Shinobi come consumare un flusso. Per prestazioni ottimali, provare a sintonizzare le impostazioni interne della fotocamera. Per trovare la fotocamera puoi utilizzare incorporato scanner Onvif di shinobi. Per aprire lo scanner Onvif fai clic sul nome utente in alto a sinistra e quindi onvif.", + "InputText2": "Scopri la configurazione e la sintonizzazione delle telecamere qui .", + "InputText3": "Se hai bisogno di aiuto per capire quale tipo di input è la tua fotocamera, puoi dare un'occhiata nell'elenco URLS della fotocamera Il sito web Shinobi.", + "Inserted Schedule Configuration": "Configurazione del programma inserita", + "Inserted State Configuration": "Configurazione dello stato inserito", + "Install": "Installare", + "Interface": "Interfaccia", + "Invalid Data": "Dati non validi", + "Invalid JSON": "JSON non valido", + "Invalid Settings": "Impostazioni non valide", + "InvalidJSONText": "Assicurati che questa sia una stringa JSON valida per la configurazione del monitor Shinobi.", + "Inverse Trigger": "Trigger inverso", + "Invert Y-Axis": "Inverti asse y", + "IrCutFilter": "Visione notturna", + "JPEG": "Jpeg", + "JPEG (Auto Enables JPEG API)": "JPEG (Auto abilita JPEG API)", + "JPEG API": "JPEG API", + "JPEG Error": "Errore JPEG", + "JPEG Mode": "Modalità jpeg", + "JPEGErrorText": "C'è stato un problema per ottenere dati dalla fotocamera.", + "January": "Gennaio", + "July": "Luglio", + "June": "Giugno", + "LDAP": "Ldap", + "LDAP Success": "Successo di LDAP", + "LDAP User Authenticated": "Utente LDAP autenticato", + "LDAP User is New": "L'utente LDAP è nuovo", + "Landing Page": "Pagina di destinazione", + "Last": "Scorsa", + "Last Modified": "Ultima modifica", + "Launch in New Window": "Avvia nella nuova finestra", + "Leave blank for random.": "Lasciare vuoto per casuale.", + "Leave blank for unlimited": "Lascia vuoto per illimitato", + "Left": "A sinistra indirizzo URL ", + "Left Stop": "Stop sinistro Indirizzo URL ", + "Legacy Webhook": "Legacy Webhook", + "Less Than": "Meno di", + "Less Than or Equal to": "Minore o uguale a", + "License Activated": "Licenza attivata", + "License Activation": "Attivazione della licenza", + "License Activation Failed": "L'attivazione della licenza non è riuscita", + "License Key": "Chiave di licenza", + "License Plate Detector": "Rilevatore di targhe", + "Like": "Piace", + "Limited": "Limitata", + "Link Google Account": "Link Account Google", + "Link LDAP Account": "Link Account LDAP", + "Link Shinobi": "Link Shinobi", + "List Toggle": "Elenco azionamento", + "List of Videos Delete Error": "Elenco dei video Elimina errore", + "Live Grid": "Griglia dal vivo", + "Live Stream Toggle": "Live Stream azionario", + "Live View": "Dal vivo", + "Local": "Locale", + "Log Level": "Livello di registro", + "Log Signal Event": "Evento del segnale di registro solo lato client ", + "Log Stream": "Flusso di registro", + "Logging": "Registrazione", + "Login": "Login", + "Logout": "Disconnettersi", + "Logs": "Tronchi", + "Loop Stream": "Flusso di loop", + "MB": "MB", + "MJPEG": "Mjpeg", + "MP4 (copy, libx264, libx265)": "MP4 (copia, libx264, libx265)", + "MPEG-4 (.mp4 / .ts)": "MPEG-4 (.mp4 / .ts)", + "MPEG-DASH (includes Audio)": "MPEG-DASH (include audio)", + "MQTT Client": "Client MQTT", + "MQTT Error": "Errore MQTT", + "MQTT Inbound": "MQTT INBOUND", + "MQTT Outbound": "MQTT Outbound", + "MailError": "Errore di posta: non è stato possibile inviare e -mail, controllare conf.json. Saltare qualsiasi funzionalità che si basa sulla mailing.", + "Main": "Principale", + "Male": "Maschia", + "Manual": "Manuale", + "Map": "Carta geografica", + "March": "Marzo", + "Matches": "Corrispondenze", + "Matrices": "Matrici", + "Max Indifference": "Max Indifferenza", + "Max Latency": "Latenza massima", + "Max Number of Cameras": "Numero massimo di telecamere", + "Max Storage Amount": "Importo di archiviazione massimo", + "MaxExposureTime": "Tempo di esposizione massima", + "MaxGain": "Guadagno massimo", + "Maximum Change": "Modifica massima", + "Maximum dB": "DB massimo", + "May": "Maggio", + "Merge Selected Videos": "Unisci video selezionati", + "Merge Video": "Unisci video", + "Merge and Download": "Unire e download", + "MergeSelectedVideosMsg": "Vuoi unire questi video? Potrebbe volerci del tempo per unire e scaricare. Nel momento in cui la connessione viene chiusa, il file verrà eliminato. Assicurati di tenere aperto il browser fino al completamento.", + "Methods": "Metodi", + "Migrator": "Migratore", + "MinExposureTime": "Tempo di esposizione minima", + "MinGain": "Guadagno minimo", + "Minimum Change": "Cambiamento minimo", + "Minimum dB": "DB minimo", + "Minutes": "Minuti", + "Mode": "Modalità", + "Monday": "Lunedi", + "Monitor": "Tenere sotto controllo", + "Monitor Added by user": "Monitor aggiunto dall'utente.", + "Monitor Capture Rate": "Monitora la velocità di acquisizione (fps) ", + "Monitor Died": "Monitor è morto", + "Monitor Edit": "Monitorare la modifica", + "Monitor Groups": "Monitorare i gruppi", + "Monitor ID": "Monitor ID", + "Monitor Idling": "Monitorare il minimo", + "Monitor Name": "Nome monitor", + "Monitor Settings": "Monitorare le impostazioni", + "Monitor Start": "Monitor Avvia", + "Monitor States": "Monitorare gli stati", + "Monitor States and Schedules": "Monitorare stati e programmi", + "Monitor Stop": "Monitor Stop", + "Monitor Stopped": "Monitor si è fermato", + "Monitor Updated by user": "Monitorare aggiornato dall'utente.", + "Monitor is now Disabled": "Monitor è ora disabilitato", + "Monitor is now Idle": "Monitor è ora inattivo", + "Monitor is now Recording": "Monitor sta ora registrando", + "Monitor is now Watching": "Monitor sta ora guardando", + "Monitor mode changed": "Modalità monitor modificata", + "Monitor mode is already": "La modalità Monitor è già", + "Monitor or Key does not exist.": "Monitoraggio o chiave non esiste.", + "MonitorIdlingText": "La sessione di monitoraggio è stata ordinata al minimo.", + "MonitorStatesText": "Puoi imparare come usare questo qui su shinobihub .", + "MonitorStoppedText": "La sessione di monitoraggio è stata ordinata di interrompere.", + "Monitors": "Monitor", + "Monitors per row": "Monitor per riga per montaggio ", + "Monitors to Copy to": "Monitora da copiare a", + "Montage": "Montaggio", + "Motion": "Movimento", + "Motion Detection": "Rilevamento del movimento", + "Motion GUI": "GUI di movimento", + "Motion Meter": "METTORE", + "Motion Threshold": "Soglia di movimento", + "Mp4Frag": "Mp4frag", + "Must be atleast one row": "Deve essere almeno una fila", + "Mute Audio": "Audio muto", + "NTP": "Ntp", + "NTP Servers": "Server NTP", + "NVIDIA": "Nvidia", + "Name": "Nome", + "Name cannot be empty.": "Il nome non può essere vuoto.", + "Nameservers": "Nameservers", + "Network": "Rete", + "Network Manager": "Responsabile del network", + "Never": "Mai", + "New Authentication Token": "Nuovo token di autenticazione", + "New Monitor": "Nuovo monitor", + "Newest": "Più recente", + "Next Video": "Video successivo", + "No": "No", + "No API Key": "Nessuna chiave API", + "No Audio": "Nessun audio", + "No Data": "Nessun dato", + "No Events found for this video": "Nessun evento trovato per questo video", + "No Group with this key exists": "Non esiste alcun gruppo con questa chiave", + "No Monitor Exists with this ID.": "Non esiste alcun monitor con questo ID.", + "No Monitor Found, Ignoring Request": "Nessun monitor trovato, ignorando la richiesta", + "No Monitor ID Present in Form": "Nessun ID monitor presente nel modulo", + "No Monitors Selected": "Nessun monitor selezionato", + "No Region": "Nessuna regione", + "No Rotation": "Nessuna rotazione", + "No Sound": "Nessun suono", + "No Trigger": "Nessun grilletto", + "No Videos Found": "Nessun video trovato", + "No such file": "Nessun file del genere", + "NoLogsFoundForDateRange": "Nessun registro trovato in questo intervallo di date. Prova ad ampliare l'intervallo di date.", + "NoMotionEmailText1": "Nessuna mozione per", + "NoMotionEmailText2": "Non è stato rilevato alcun movimento sulla fotocamera", + "NoVideosFoundForDateRange": "Nessun video trovato in questo intervallo di date. Prova a impostare la data di inizio più indietro.", + "Noise Filter": "Filtro del rumore", + "Noise Filter Range": "Gamma di filtri di rumore", + "Non-Standard ONVIF": "Non standard onvif", + "Not Activated": "Non attivato", + "Not Authorized": "Non autorizzato", + "Not Connected": "Non collegata", + "Not Equal to": "Non uguale a", + "Not Found": "Non trovato", + "Not In": "Non in", + "Not Matches": "Non corrisponde", + "Not Permitted": "Non consentito", + "Not Saved": "Non salvato", + "Not an Administrator Account": "Non un account amministratore", + "NotAuthorizedText1": "Non autorizzato, invia comando init con \"autenticazione\", \"Ke\" e \"uid\"", + "Notes": "Appunti", + "NotesPlacholder": "Commenti che vuoi lasciare per le impostazioni delle telecamere.", + "Nothing exists": "Niente esiste", + "Notice": "Avviso", + "Notification Sound": "Suono di notifica", + "Notification Video Length": "Lunghezza del video di notifica", + "Notifications": "Notifiche", + "NotifyErrorText": "L'invio della notifica ha causato un errore", + "November": "novembre", + "Number of Days to keep": "Numero di giorni da mantenere", + "Numeric criteria unsupported for Region tests, Ignoring Conditional": "Criteri numerici non supportati per i test della regione, ignorando il condizionale", + "OAuth Code": "Codice OAuth", + "OAuth Credentials": "Credenziali di oauth", + "ONVIF": "Onvif", + "ONVIF Device Manager": "OnVif Device Manager", + "ONVIF Port": "Porta onvif", + "ONVIF Scanner": "Scanner Onvif", + "ONVIFErr400": "Trovato la porta Onvif ma l'autorizzazione non è riuscita quando si è recuperato l'URL del flusso. Controlla il nome utente e la password utilizzati per la scansione. Assicurati che il tempo della fotocamera e il tempo del server siano sincronizzati.", + "ONVIFErr404": "Non trovato. Questo potrebbe essere solo il pannello Web per un dispositivo di rete.", + "ONVIFErr405": "Operazione non permessa. Controlla il nome utente e la password utilizzati per la scansione.", + "ONVIFEventsNotAvailable": "Eventi Onvif non disponibili", + "ONVIFEventsNotAvailableText1": "Questo servizio potrebbe non essere disponibile per questa fotocamera o ONVIF non ha ancora inizializzato.", + "ONVIFnotCompliantProfileT": "La fotocamera non è conforme al profilo T.", + "ONVIFnote": "Scopri i dispositivi ONVIF su reti al di fuori del proprio o lascialo vuoto per scansionare la tua rete attuale.
Nome utente e password possono essere lasciati vuoti.", + "OR": "O", + "Object": "Oggetto", + "Object Count": "Conteggio degli oggetti", + "Object Detection": "Rilevamento di oggetti", + "Object Detector Flags": "Flag del rivelatore di oggetti", + "Object Tag": "Tag dell'oggetto", + "Objects to look for": "Oggetti da cercare", + "October": "ottobre", + "Off": "Spento", + "Oldest": "Il più vecchio", + "On": "SU", + "On Unexpected Exit": "All'uscita inaspettata", + "Open": "Aprire", + "Open All Monitors": "Apri tutti i monitor", + "Open Remote Dashboard": "Apri Dashboard remoto", + "OpenCV Cascades": "OpenCv Cascades", + "Operating Hours": "Ore di servizio", + "Optional": "Opzionale", + "Options": "Opzioni", + "Order Streams": "Ordina flussi", + "Original Choice": "Scelta originale", + "Other Devices": "Altri dispositivi", + "Output": "Produzione", + "Output Method": "Metodo di output", + "P2P API Key": "Chiave API P2P", + "P2P Host": "Host P2P", + "P2P Server Not Selected": "Server p2p non selezionato", + "P2P Settings Applied": "Impostazioni P2P applicate", + "PTZ Tracking": "Tracciamento PTZ", + "PTZ Tracking Target": "Target di tracciamento PTZ", + "Password": "Parola d'ordine", + "Password Again": "Di nuovo password", + "Passwords don't match": "Le password non corrispondono", + "Paste JSON here.": "Incolla JSON qui o Carica file.", + "Path": "Sentiero", + "Pause": "Pausa", + "Per Monitor": "Per monitor", + "Performance Optimization Possible": "Ottimizzazione delle prestazioni possibile", + "Permissions": "Autorizzazioni", + "Ping Failed": "Il ping non è riuscito", + "Plain": "Pianura", + "Play": "Giocare a", + "Playback": "Riproduzione", + "Please Check Your Settings": "Si prega di controllare le tue impostazioni", + "Please Wait for Completion": "Si prega di attendere il completamento, a seconda del numero di file selezionati, ciò potrebbe richiedere del tempo.", + "Please Wait or Click to Stop Checking": "Si prega di attendere o fare clic per smettere di controllare", + "Please Wait...": "Attendere prego...", + "Plugin": "Collegare", + "Plugin Manager": "Plugin Manager", + "Points": "Punti", + "Pop": "Pop", + "Popout Monitor on Event": "Popout Monitor on Event", + "Port": "Porta", + "Pose": "Posa", + "Poseidon": "Poseidone", + "Position X": "Posizione x", + "Position Y": "Posizione y", + "Power Video Viewer": "Power Video Viewer", + "Power Viewer": "Power Viewer", + "Preferences": "Preferenze", + "Prefix": "Prefisso", + "Preset": "Preimpostazione", + "Preset Name": "Nome preimpostazione", + "Presets": "Preset", + "Preview": "Anteprima", + "Previous Video": "Video precedente", + "Primary Engine": "Motore primario", + "Primary Input": "Input primario", + "Privileges": "Privilegi", + "Probe Size": "Dimensione della sonda", + "Process Already Running": "Processo già in esecuzione", + "Process Crashed for Monitor": "Il processo si è arrestato per il monitor", + "Process Not Running": "Processo non in esecuzione", + "Process Started": "Processo avviato", + "Process Unexpected Exit": "Processo di uscita inaspettata", + "Processor": "Processore", + "Profile": "Profilo", + "Protocol": "Protocollo", + "Public on ShinobiHub": "Pubblico su Shinobihub", + "Quality": "Qualità", + "Query": "Domanda", + "Quick Settings": "Impostazioni rapide", + "Quick Sync Video": "Video di sincronizzazione rapida", + "RAM": "RAM", + "RTMP": "Rtmp", + "RTMP Stream": "Stream RTMP", + "RTMP Stream Flags": "Flag di flusso RTMP", + "RTMPS": "RTMPS", + "RTSP": "Rtsp", + "RTSP Transport": "Trasporto RTSP", + "Range or Single": "Gamma o singolo", + "Raspberry Pi": "Raspberry Pi", + "Rate": "Velocità (fps) ", + "Raw": "Cruda", + "Raw H.264 Stream": "RAW H.264 Stream", + "Reason": "Motivo", + "Reboot": "Riavviare", + "Reboot Camera": "Riavvia la fotocamera", + "Recent Events": "Eventi recenti", + "Recent Videos": "Video recenti", + "Recipient ID": "Destinatario ID", + "Recommended": "Consigliata", + "Reconnect Stream": "Riconnettere il flusso", + "Record": "Disco", + "Record File Type": "Registra il tipo di file", + "Record Height": "Altezza del record", + "Record Video Filter": "Registra il filtro video", + "Record Width": "Larghezza dei record", + "Recorded Buffer": "Buffer registrato", + "Recording": "Registrazione", + "Recording FPS": "Registrazione FPS", + "Recording FPS Change on Start": "Registrazione della modifica FPS all'inizio", + "Recording Flags": "Registrazione di bandiere", + "Recording Segment Interval": "Intervallo di segmento di registrazione in minuti ", + "Recording Timeout": "Timeout di registrazione in minuti ", + "Recording Timestamp": "Timestamp di registrazione", + "Recording Watermark": "Registrazione di filigrana", + "RecordingText": "Si consiglia di impostare Registra Tipo di file su webm mp4 codec video a libvpx copia o libx264 perché il tuo tipo di input è impostato su .", + "Refresh List of Cascades": "Aggiorna elenco di Cascades", + "Region": "Regione", + "Region Editor": "Editor della regione", + "Region Name": "Nome regione", + "RegionNote": "Quando si aggiungono punti, fare clic sul bordo del poligono. Fare clic con il tasto destro a un punto per rimuovere.", + "Regions": "Regioni", + "Registered": "Registrata", + "Registered Servers": "Server registrati", + "Remember Me": "Ricordati di me", + "Request": "Richiesta", + "Require Object to be in Region": "Richiedere che l'oggetto sia nella regione", + "Reset": "Ripristina", + "Reset Form": "Reimposta il modulo", + "Reset Timer": "Ripristina il timer", + "Resolution": "Risoluzione", + "Restart": "Ricomincia", + "Restart CRON": "Riavvia Cron", + "Restart Core": "Riavvia core", + "Restarting": "Riavvio", + "Restarting Process": "Processo di riavvio", + "Retry Connection": "Retry Connection numero di volte consentito fallire ", + "Retrying...": "Riprovare ...", + "Right": "A destra Indirizzo URL ", + "Right Stop": "Stop destro Indirizzo URL ", + "Rotate": "Ruotare", + "Rule": "Regola", + "Run Installer": "Esegui installatore", + "S3-Based Network Storage": "Archiviazione di rete basata su S3", + "SFTP": "Sftp", + "SFTP (SSH File Transfer)": "SFTP (trasferimento di file ssh)", + "SFTP Error": "Errore SFTP", + "Saturday": "Sabato", + "Save": "Salva", + "Save Changes": "Salvare le modifiche", + "Save Directory": "Salva directory", + "Save Events": "Salva eventi", + "Save Events to SQL": "Salva eventi su SQL", + "Save Frames to Events": "Salva frame agli eventi", + "Save Links to Database": "Salva collegamenti al database", + "Save Log in SQL": "Salva accesso in sql questo può riempirsi rapidamente. ", + "Save New": "Salva nuovo", + "Save as": "Salva come", + "Saved": "Salvato", + "Saved Filters": "Filtri salvati", + "Saved Logs": "Registri salvati", + "Saved Presets": "Preset salvati", + "Saved Schedules": "Orari salvati", + "Scan Settings": "Impostazioni di scansione", + "Schedule": "Programma", + "Schedule Configuration Not Found": "Configurazione del programma non trovata", + "Schedules": "Orari", + "Search": "Ricerca", + "Search Base": "Base di ricerca", + "Search Filter": "Filtro di ricerca", + "Search Images": "Immagini di ricerca", + "Search Settings": "Impostazioni di ricerca", + "Second stream in feed": "Secondo flusso nel feed", + "Secure": "Sicura", + "Select a Monitor": "Seleziona un monitor", + "Select atleast one monitor to delete": "Seleziona almeno un monitor da eliminare.", + "Selected": "Selezionata", + "Send Frames": "Invia frame frame di spinta da analizzare ", + "Send Notification": "Invia notifica", + "Send to": "Inviare a", + "Separate with commas, no spaces": "Separare con virgole, nessun spazi", + "September": "settembre", + "Server URL": "URL del server", + "Session Key": "Chiave di sessione", + "Set Home": "Mettere a casa", + "Set Home Position (ONVIF-only)": "Imposta posizione home (solo onvif)", + "Set Mode": "Modalità impostata", + "Set to Watch Only": "Impostato solo per guardare", + "Settings": "Impostazioni", + "Settings Changed": "Impostazioni modificate", + "SettingsChangedText": "Le tue impostazioni sono state salvate e applicate. Alcune impostazioni potrebbero richiedere un aggiornamento di questa pagina.", + "Sharpness": "Nitidezza", + "Shinobi": "Shinobi", + "Shinobi Ordered to Update": "Aggiornamento di Shinobi completato", + "Shinobi Streamer": "Shinobi streamer", + "ShinobiHub": "Shinobihub", + "Show Logs": "Mostra i registri", + "Show Matrices": "Mostra matrici", + "Show Matrix": "Mostra matrice", + "Show Regions of Interest": "Mostra regioni di interesse", + "Show Stream HUD": "Mostra lo stream HUD", + "Show Thumbnails in Video List": "Mostra miniature nell'elenco video", + "Silent": "Silenziosa", + "Simple": "Semplice", + "Size (mb)": "Dimensione (MB)", + "Skip Ping": "Salta ping", + "Snapshot": "Istantanea", + "Snapshot Flags": "Flag di istantanee", + "Snapshots": "Istantanee", + "Sort By": "Ordina per", + "Space Used": "Spazio usato", + "Start": "Inizio", + "Start Recording": "Inizia a registrare", + "Start Time": "Ora di inizio", + "Start Time cannot be empty.": "L'ora di inizio non può essere vuoto.", + "Started": "Cominciato", + "Started Building": "Ha iniziato a costruire", + "Starting": "Di partenza", + "State Configuration Not Found": "Configurazione dello stato non trovata", + "State Configuration has no monitors associated": "La configurazione dello stato non ha monitor associati", + "Status Changed": "Stato cambiato", + "Status Indicator": "Indicatore di stato", + "Stop": "Fermare", + "Stop Command": "Comando di arresto", + "Stop URL": "Smettere di url", + "Stopped": "Fermato", + "Stopping": "Fermarsi", + "Storage Location": "Posizione di archiviazione", + "Storage Use": "Uso di archiviazione", + "Stream": "Flusso", + "Stream Channel": "Canale di flusso", + "Stream Flags": "Flag di streaming", + "Stream Key": "Chiave di streaming", + "Stream Timestamp": "Timestamp di streaming", + "Stream Type": "Tipo di flusso", + "Stream Watermark": "Filigrana in streaming", + "Stream in Background": "Flusso in background", + "Stream to YouTube": "Streaming su YouTube", + "Stream to YouTube Flags": "Stream sulle flag di YouTube", + "StreamText": "

Questa sezione designerà il metodo principale di streaming e le sue impostazioni. Questo flusso verrà visualizzato nella dashboard. Se si sceglie di utilizzare HLS, JPEG o MJPEG, è possibile consumare il flusso attraverso altri programmi.

Usando il flusso JPEG essenzialmente disattiva il flusso primario e utilizza il bidone di Snapshot per ottenere frame.

", + "Streamed Logs": "Registri in streaming", + "Streamer": "Streamer", + "Streams": "Flussi", + "Sub-Accounts": "Sottoconti", + "Subdivision": "Suddivisione", + "Substream": "Substream", + "Substream Process": "Processo substream", + "SubstreamNotConfigured": "Substream non configurato. Apri le impostazioni del monitor e configurale.", + "Subtitle": "Sottotitolo", + "Success": "Successo", + "Sunday": "Domenica", + "Superuser": "Superutente", + "Superuser Logs": "Registri superutenti", + "Switch on for Still Image": "Accendi per l'immagine fissa", + "System": "Sistema", + "System Level": "Livello di sistema", + "TCP": "TCP", + "TV Channel": "Canale tv", + "TV Channel Group": "Gruppo di canali televisivi", + "TV Channel ID": "ID canale TV", + "Telegram": "Telegramma", + "Text Box Color": "Colore della casella di testo", + "Text Color": "Colore del testo", + "Text criteria unsupported for Object Count tests, Ignoring Conditional": "Criteri di testo non supportati per i test del conteggio degli oggetti, ignorando il condizionale", + "Themes": "Temi", + "There are no monitors that you can view with this account.": "Non ci sono monitor che puoi visualizzare con questo account.", + "Threads": "Discussioni", + "Thumbnail": "Miniatura", + "Thursday": "Giovedì", + "Time": "Volta", + "Time Created": "Tempo creato", + "Time Left": "Tempo rimasto", + "Time Occurred": "Il tempo si è verificato", + "Time-lapse": "Lasso di tempo", + "Time-lapse Tool": "Strumento time-lapse", + "TimeZone": "Fuso orario", + "Timelapse": "Lasso di tempo", + "Timelapse Frames Share": "Condividi delle cornici timelapse", + "Timelapse Watermark": "Filigrana timelapse", + "Timeout": "Tempo scaduto", + "Timeout Reset on Next Event": "Timeout Reset al prossimo evento", + "Timeout Reset on Next Motion": "Timeout Ripristina il prossimo movimento", + "Timezone": "Fuso orario", + "Timezone Offset": "Offset del fuso orario", + "Title": "Titolo", + "Today": "In data odierna", + "Toggle Sidebar": "Attiva a disattivazione della barra laterale", + "Toggle Substream": "Attiva il substream", + "Token": "Gettone", + "Top Left": "In alto a sinistra", + "Top Right": "In alto a destra", + "Traditional (Watch-Only, Includes Buffer)": "Tradizionale (solo orologio, include buffer)", + "Traditional Recording": "Registrazione tradizionale", + "Traditional Recording Flags": "Bandiere di registrazione tradizionali", + "Train": "Treno", + "TrainConfirm": "Sei sicuro di voler iniziare ad allenarti? Questo può richiedere più di 12 ore con oltre 500 immagini. Ciò consumerà una grande quantità di risorse, come RAM e/o CPU.", + "TrainConfirmStop": "Sei sicuro di voler smettere di allenarsi?", + "Trainer Engine": "Motore del trainer", + "Trigger Blocked": "Grilletto bloccato", + "Trigger Camera Groups": "Gruppi di telecamere trigger", + "Trigger Event": "Evento scatenante", + "Trigger Group to Record": "Trigger Group da registrare", + "Trigger Record": "Record di grilletto", + "Trigger Successful": "Innescare successo", + "Trigger Threshold": "Soglia di grilletto", + "Tuesday": "Martedì", + "Turn Speed": "Girare la velocità", + "Type": "Tipa", + "UDP": "UDP", + "URL": "URL", + "URL Stop Timeout": "URL Stop timeout run stop URL dopo x millisecondi ", + "US": "NOI", + "UTCDateTime": "Data", + "Unable to Launch": "Impossibile avviare", + "UnabletoLaunchText": "Si prega di salvare prima un nuovo monitor. Quindi tenta di lanciare l'editor della regione.", + "Uncommon Objects": "Oggetti non comuni", + "Uniform": "Uniforme", + "Unlink": "Unkin", + "Unlink Login": "Accesso di accesso?", + "Unlinked": "Non incazzato", + "Up": "UP Indirizzo URL ", + "Up Stop": "STOP UP Indirizzo URL ", + "Update": "Aggiornare", + "Update to Development": "Aggiornamento allo sviluppo", + "Update to Master": "Aggiornamento a Master", + "Upload Bandwidth": "Carica larghezza di banda", + "Upload File": "Caricare un file", + "Uploaded Only": "Solo caricato", + "Uploaders": "Uploader", + "Use Built-In": "Usa integrato", + "Use Camera Timestamps": "Utilizzare i timestamp della fotocamera", + "Use Global Amazon S3 Video Storage": "Usa l'archiviazione video Global Amazon S3", + "Use Global Backblaze B2 Video Storage": "Utilizzare l'archiviazione video globale BAINTLAZE B2", + "Use Global Wasabi Hot Cloud Storage Video Storage": "Utilizzare l'archiviazione video di archiviazione cloud hot globale wasabi", + "Use Global WebDAV Video Storage": "Usa l'archiviazione video WebDav globale", + "Use HTML5 Play Method": "Usa il metodo di gioco HTML5", + "Use Max Storage Amount": "Utilizzare l'importo di archiviazione massimo", + "Use Raw Snapshot": "Usa l'istantanea grezza", + "Use Substream": "Usa il substream", + "Use coProcessor": "Usa il coprocessore", + "UseCount": "Usecount", + "User Log": "Registro utente", + "User Not Found": "Utente non trovato", + "Username": "Nome utente", + "VA-API": "Va-api", + "Value": "Valore", + "Video": "video", + "Video Bit Rate": "Bitrate video", + "Video Codec": "Video codec", + "Video Configuration": "Configurazione video", + "Video Filter": "Filtro video", + "Video Finished": "Video finito", + "Video Length (minutes) and Motion Count per video": "Lunghezza del video (minuti) e conteggio dei movimenti per video", + "Video Limit": "Limite video", + "Video Record Rate": "Velocità di registrazione video", + "Video Set": "Set video", + "Video Share": "Condivisione video", + "Video Status": "Stato video", + "Video and Time Span (Minutes)": "Video e tempo (minuti)", + "Video stream only from first feed": "Stream video solo dal primo feed", + "Video streams only": "Solo flussi video", + "Videos": "Video", + "Videos List": "Elenco video", + "Videos Merge": "I video si uniscono", + "Viewing Server Stats": "Visualizzazione delle statistiche del server", + "Warning": "Avvertimento", + "Wasabi Hot Cloud Storage": "Wasabi Hot Cloud Storage", + "Wasabi Hot Cloud Storage Upload Error": "Wasabi Hot Cloud Storage Caricamento Errore", + "Watch": "Guadare", + "Watch Only": "Guarda solo", + "Watch-Only": "Watchy", + "Watching": "Guardando", + "Web Page": "Pagina web", + "WebDAV": "Webdav", + "WebM (libvpx)": "Webm (libvpx)", + "Webdav Error": "Errore WebDAV", + "WebdavErrorTextCreatingDir": "Impossibile creare la directory.", + "WebdavErrorTextTryCreatingDir": "Non può salvare. Cercando di creare la directory.", + "Webhook": "Webhook", + "Webhook URL": "Webhook URL", + "Websocket": "WebSocket", + "Websocket Connected": "WebSocket connesso", + "Websocket Disconnected": "WebSocket disconnesso", + "Wednesday": "Mercoledì", + "Welcome": "Benarrivata!", + "When Detector is Off": "Quando il rivelatore è spento", + "When Detector is On": "Quando il rivelatore è acceso", + "WhiteBalance": "Bilanciamento del bianco", + "WideDynamicRange": "Ampia gamma dinamica", + "Width": "Larghezza", + "X Point": "X punto", + "Y Point": "Y punto", + "Yes": "sì", + "Zip and Download": "Zip e download", + "Zipping Videos": "Video zipping", + "Zones": "Zone", + "Zoom In": "Ingrandire", + "Zoom In Stop": "Indirizzo URL di zoom in stop ", + "Zoom Out": "Zoom out", + "Zoom Out Stop": "Zoom out stop indirizzo URL ", + "a day": "un giorno", + "a few seconds": "pochi secondi", + "a minute": "un minuto", + "a month": "un mese", + "a year": "un anno", + "aac": "AAC", + "aac (Default)": "AAC (impostazione predefinita)", + "ac3": "AC3", + "accountActionFailed": "Azione dell'account non riuscita", + "accountAdded": "Account aggiunto", + "accountAddedText": "L'account è stato aggiunto.", + "accountDeleted": "Account cancellato", + "accountDeletedText": "L'account è stato eliminato.", + "accountId": "Account ID", + "accountSettingsDescription": "Gestisci il tuo profilo e imposta opzioni come un importo di archiviazione massimo e il numero massimo di giorni per conservare i video.", + "accountSettingsError": "Errore di impostazioni dell'account", + "activatedText": "La tua installazione è stata attivata.", + "ago": "fa", + "airplane": "aereo", + "alreadyLinked": "Già collegato a un account", + "an hour": "un'ora", + "apple": "mela", + "applicationKey": "Chiave dell'applicazione", + "aws_accessKeyId": "ID chiave di accesso", + "aws_secretAccessKey": "Chiave di accesso segreto", + "backpack": "zaino", + "banana": "Banana", + "baseball bat": "Mazza da baseball", + "baseball glove": "guanto da baseball", + "bear": "orsa", + "bed": "letta", + "bench": "panca", + "bicycle": "bicicletta", + "bindDN": "binddn", + "bird": "uccello", + "blankPassword": "Lascia vuoto per mantenere la stessa password", + "boat": "barca", + "book": "prenotare", + "bottle": "bottiglia", + "bowl": "ciotola", + "broccoli": "broccole", + "bus": "autobus", + "cake": "torta", + "car": "macchina", + "carrot": "carota", + "cat": "gatta", + "cell phone": "cellulare", + "chair": "sedia", + "clientStreamFailedattemptingReconnect": "Controllo del flusso lato client non riuscito, tentando di riconnettersi.", + "clock": "orologio", + "coProcess Crashed for Monitor": "Il coprocesso si è schiantato per il monitor", + "coProcess Unexpected Exit": "Coprocess uscita inaspettata", + "coProcessor": "coprocessore", + "coProcessor Started": "Il coprocessore è iniziato", + "coProcessor Stopped": "Il coprocessore si fermò", + "coProcessorTextStarted": "Il coprocessore ha iniziato solo per le uscite della CPU.", + "coProcessorTextStopped": "Il coprocessore è finito.", + "codecMismatchText1": "La fotocamera fornisce dati di streaming H.265 (HEVC) e si utilizza Copy come codec video per la sezione Stream. Il tuo flusso di Shinobi potrebbe non apparire su dispositivi che non possono utilizzare questo codec. L'app mobile Shinobi può visualizzare questi flussi.", + "codecMismatchText2": "Il codec video selezionato non è applicabile. La fotocamera fornisce dati di flusso MJPEG e si utilizza Copy come codec video per la sezione Stream. Modificato il tipo di flusso in MJPEG.", + "codecMismatchText3": "Il codec video selezionato non è applicabile. La fotocamera fornisce dati di flusso MJPEG e si utilizza Copy come codec video per la sezione di registrazione. Modificato il codec video in libx264.", + "confirmDeleteFilter": "Vuoi eliminare questo filtro? Non puoi recuperarlo.", + "contactAdmin": "Contatta il manutentore dell'installazione di Shinobi.", + "copy": "copia", + "couch": "divano", + "cow": "mucca", + "cuda": "CUDA (Nvidia NVENC)", + "cup": "tazza", + "cuvid": "Cuvid (Nvidia Nvenc)", + "days": "giorni", + "deleteApiKey": "Elimina la chiave API", + "deleteApiKeyText": "Vuoi eliminare questa chiave API? Non puoi recuperarlo.", + "deleteMonitorStateText1": "Vuoi eliminare questo preimpostazione degli stati del monitor? Le configurazioni del monitor non possono essere recuperate.", + "deleteMonitorStateText2": "Vuoi eliminare il preimpostazione di questo monitor?", + "deleteScheduleText": "Vuoi eliminare questo programma? Monitorare i preset associati non verranno modificati ..", + "deleteSubAccount": "Elimina sotto-conto", + "deleteSubAccountText": "Vuoi eliminare questo sotto-conto? Non puoi recuperarlo.", + "dining table": "tavolo da pranzo", + "dog": "cagna", + "donut": "ciambella", + "drm": "Condivisione di oggetti DRM", + "dropBoxSuccess": "Successo! File salvati sul tuo dropbox.", + "dxva2": "DXVA2 (Video DirectX, Windows)", + "elephant": "elefantessa", + "eventFilterActionText": "Queste sono le azioni che si verificano dalle condizioni del filtro che sono riuscite. La \"scelta originale\" si riferisce all'opzione che hai scelto nelle impostazioni del monitor.", + "eventFilterErrorBrackets": "Hai un numero non uniforme di parentesi. Vengono ignorati.", + "eventFiltersDescription": "Filtri di configurazione per quando si verificano eventi.", + "failedLoginText1": "Non sei riuscito a accedere troppe volte. Devi aspettare 15 minuti prima di riprovare.", + "failedLoginText2": "Si prega di controllare le credenziali di accesso.", + "fieldMissingValueText1": "La fotocamera fornisce dati di flusso MJPEG. È necessario impostare il tasso di acquisizione del monitor. Shinobi tenterà di rilevarlo e riempirlo automaticamente.", + "fieldTextAccelerator": "Accelerazione hardware (HWACCEL) per i flussi di decodifica.", + "fieldTextAcodec": "Codec audio per la registrazione.", + "fieldTextActionsCommand": "È possibile utilizzarlo per attivare uno script su comando.", + "fieldTextActionsHalt": "Fai non fare all'evento nulla, come se non fosse mai successo.", + "fieldTextActionsIndifference": "Modifica l'indifferenza minima richiesta per l'evento.", + "fieldTextActionsRecord": "Utilizzare la registrazione tradizionale, HotSwap o Elimina immobile con le loro opzioni attualmente impostate nella sezione Impostazioni di rilevamento globali.", + "fieldTextAduration": "Specificare quanti microsecondi vengono analizzati per sondare l'ingresso. Impostare su 100000 se si utilizza RTSP e hai problemi di streaming.", + "fieldTextAudioAlert": "Suono quando si verifica un evento.", + "fieldTextAudioDelay": "Ritardare fino alla prossima volta un evento può iniziare un avviso. Misurato in secondi.", + "fieldTextAudioNote": "Suono quando appare la bolla di informazioni.", + "fieldTextAutoHost": "L'URL a flusso completo.", + "fieldTextAutoHostEnable": "Nutri i singoli pezzi necessari per costruire un URL di flusso o fornire l'intero URL e consentire a Shinobi di analizzarlo per te.", + "fieldTextChannelHlsListSize": "Il numero di segmenti massimo prima di eliminare automaticamente i vecchi segmenti.", + "fieldTextChannelHlsTime": "Per quanto tempo dovrebbe essere ogni segmento video, in pochi minuti. Ogni segmento verrà disegnato dal client tramite un file M3U8. I segmenti più corti assumono meno spazio.", + "fieldTextChannelPresetStream": "Flag preimpostata per alcuni coder video. Se scopri che la tua fotocamera si blocca ogni pochi secondi: prova a lasciarla vuota.", + "fieldTextChannelStreamAcodec": "Codec audio per lo streaming.", + "fieldTextChannelStreamAcodecAac": "Utilizzato per il video MP4.", + "fieldTextChannelStreamAcodecAc3": "Utilizzato per il video MP4.", + "fieldTextChannelStreamAcodecAuto": "Lascia che FFMPEG scelga.", + "fieldTextChannelStreamAcodecCopy": "Utilizzato per il video MP4. Ha un utilizzo della CPU molto basso, ma alcuni codec audio necessitano di flag personalizzati come -strict 2 per AAC.", + "fieldTextChannelStreamAcodecLibmp3lame": "Utilizzato per il video MP4.", + "fieldTextChannelStreamAcodecLibopus": "Utilizzato per il video WebM.", + "fieldTextChannelStreamAcodecLibvorbis": "Utilizzato per il video WebM.", + "fieldTextChannelStreamAcodecNoAudio": "Nessun audio, questa è un'opzione che deve essere impostata in alcune parti del mondo per motivi legali.", + "fieldTextChannelStreamFps": "La velocità in cui i frame vengono visualizzati ai client, in frame al secondo. Essere consapevoli che non ci sono impostazioni predefinite. Questo può portare ad un elevato utilizzo della larghezza di banda.", + "fieldTextChannelStreamQuality": "Numero basso significa qualità superiore. Numero più alto significa meno qualità.", + "fieldTextChannelStreamRotate": "Cambia l'angolo di visualizzazione del flusso video.", + "fieldTextChannelStreamScaleX": "Larghezza dell'immagine del flusso che viene emessa dopo l'elaborazione.", + "fieldTextChannelStreamScaleY": "Altezza dell'immagine del flusso che viene utilizzata dopo l'elaborazione.", + "fieldTextChannelStreamType": "Il metodo utilizzato per consumare il flusso video.", + "fieldTextChannelStreamTypeFLV": "Invio di frame codificati FLV su WebSocket.", + "fieldTextChannelStreamTypeHLS(includesAudio)": "Metodo simile ai flussi live di Facebook. Include audio Se l'input lo fornisce. C'è un ritardo di circa 4-6 secondi perché questo metodo registra i segmenti li spinge al cliente piuttosto che spingere come mentre li crea.", + "fieldTextChannelStreamTypeMJPEG": "MOVIMENTO STANDARD JPEG Immagine. Nessun audio.", + "fieldTextChannelStreamTypePoseidon": "Poseidon è costruito sul codice di elaborazione MP4 di Kevin Godell. Simula un file MP4 in streaming ma utilizzando i dati di un flusso live. Include audio. Alcuni browser possono riprodurlo come un normale file MP4. Flussi su HTTP o WebSocket.", + "fieldTextChannelStreamVcodec": "Codec video per lo streaming.", + "fieldTextChannelStreamVcodecAuto": "Lascia che FFMPEG scelga.", + "fieldTextChannelStreamVcodecCopy": "Utilizzato per il video MP4. Ha un utilizzo della CPU molto basso, ma non è possibile utilizzare filtri video e file file possono essere giganteschi. Meglio impostare le impostazioni MP4 sul lato della fotocamera quando si utilizza questa opzione.", + "fieldTextChannelStreamVcodecLibx264": "Utilizzato per il video MP4.", + "fieldTextChannelStreamVcodecLibx265": "Utilizzato per il video MP4.", + "fieldTextChannelSvf": "Posizionare i filtri video FFMPEG in questa casella per influenzare la porzione di streaming. No spazi.", + "fieldTextControlInvertY": "Per quando la fotocamera è montata a testa in giù o utilizza controlli verticali invertiti.", + "fieldTextCrf": "Numero basso significa qualità superiore. Numero più alto significa meno qualità.", + "fieldTextCustDetect": "Flag personalizzati che si legano al rivelatore del flusso utilizza per l'analisi.", + "fieldTextCustDetectObject": "Flag personalizzati che si legano al rivelatore del flusso utilizza per l'analisi.", + "fieldTextCustInput": "Flag personalizzati che si legano all'input del processo FFMPEG.", + "fieldTextCustRecord": "Flag personalizzati che si legano alla registrazione del processo FFMPEG.", + "fieldTextCustSipRecord": "Flag personalizzati che si legano all'output da cui le registrazioni basate su eventi sifon.", + "fieldTextCustSnap": "Flag personalizzati che si legano alle istantanee.", + "fieldTextCustStream": "Flag personalizzati che si legano al flusso (vista laterale client) del processo FFMPEG.", + "fieldTextCustomOutput": "Aggiungi un output personalizzato come frame JPEG o invia i dati direttamente a un altro server.", + "fieldTextCutoff": "In pochi minuti. Quando tagliare e avviare un nuovo file video.", + "fieldTextDays": "Il numero di giorni per mantenere i video prima di spurgare.", + "fieldTextDetailSubstreamInputRtspTransportAuto": "Lascia decidere FFMPEG. Normalmente proverà prima UDP.", + "fieldTextDetailSubstreamInputRtspTransportTCP": "Impostalo su questo se UDP inizia a dare risultati indesiderati.", + "fieldTextDetailSubstreamInputRtspTransportUDP": "FFMPEG ci prova prima.", + "fieldTextDetailSubstreamOutputHlsListSize": "Il numero di segmenti massimo prima di eliminare automaticamente i vecchi segmenti.", + "fieldTextDetailSubstreamOutputHlsTime": "Per quanto tempo dovrebbe essere ogni segmento video, in pochi minuti. Ogni segmento verrà disegnato dal client tramite un file M3U8. I segmenti più corti assumono meno spazio.", + "fieldTextDetailSubstreamOutputPresetStream": "Flag preimpostata per alcuni coder video. Se scopri che la tua fotocamera si blocca ogni pochi secondi: prova a lasciarla vuota.", + "fieldTextDetailSubstreamOutputStreamAcodec": "Codec audio per lo streaming.", + "fieldTextDetailSubstreamOutputStreamAcodecAac": "Utilizzato per il video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecAc3": "Utilizzato per il video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecAuto": "Lascia che FFMPEG scelga.", + "fieldTextDetailSubstreamOutputStreamAcodecCopy": "Utilizzato per il video MP4. Ha un utilizzo della CPU molto basso, ma alcuni codec audio necessitano di flag personalizzati come -strict 2 per AAC.", + "fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame": "Utilizzato per il video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecLibopus": "Utilizzato per il video WebM.", + "fieldTextDetailSubstreamOutputStreamAcodecLibvorbis": "Utilizzato per il video WebM.", + "fieldTextDetailSubstreamOutputStreamAcodecNoAudio": "Nessun audio, questa è un'opzione che deve essere impostata in alcune parti del mondo per motivi legali.", + "fieldTextDetailSubstreamOutputStreamFps": "La velocità in cui i frame vengono visualizzati ai client, in frame al secondo. Essere consapevoli che non ci sono impostazioni predefinite. Questo può portare ad un elevato utilizzo della larghezza di banda.", + "fieldTextDetailSubstreamOutputStreamQuality": "Numero basso significa qualità superiore. Numero più alto significa meno qualità.", + "fieldTextDetailSubstreamOutputStreamRotate": "Cambia l'angolo di visualizzazione del flusso video.", + "fieldTextDetailSubstreamOutputStreamScaleX": "Larghezza dell'immagine del flusso che viene emessa dopo l'elaborazione.", + "fieldTextDetailSubstreamOutputStreamScaleY": "Altezza dell'immagine del flusso che viene utilizzata dopo l'elaborazione.", + "fieldTextDetailSubstreamOutputStreamType": "Il metodo utilizzato per consumare il flusso video.", + "fieldTextDetailSubstreamOutputStreamVcodec": "Codec video per lo streaming.", + "fieldTextDetailSubstreamOutputStreamVcodecAuto": "Lascia che FFMPEG scelga.", + "fieldTextDetailSubstreamOutputStreamVcodecCopy": "Utilizzato per il video MP4. Ha un utilizzo della CPU molto basso, ma non è possibile utilizzare filtri video e file file possono essere giganteschi. Meglio impostare le impostazioni MP4 sul lato della fotocamera quando si utilizza questa opzione.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx264": "Utilizzato per il video MP4.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx265": "Utilizzato per il video MP4.", + "fieldTextDetailSubstreamOutputSvf": "Posizionare i filtri video FFMPEG in questa casella per influenzare la porzione di streaming. No spazi.", + "fieldTextDetector": "Ciò aggiungerà un altro output nel comando FFMPEG per il rilevatore di movimento.", + "fieldTextDetectorAudio": "Controlla se l'audio si è verificato con una certificazione. La lettura decisa potrebbe non essere accurata alla misurazione del mondo reale.", + "fieldTextDetectorBufferHlsListSize": "Il numero di segmenti massimo prima di eliminare automaticamente i vecchi segmenti.", + "fieldTextDetectorBufferHlsTime": "Per quanto tempo dovrebbe essere ogni segmento video, in pochi secondi. Ogni segmento verrà disegnato dal client tramite un file M3U8. I segmenti più corti assumono meno spazio.", + "fieldTextDetectorColorThreshold": "La quantità di differenza consentita in un pixel prima che sia considerato moto.", + "fieldTextDetectorCommand": "Il comando che verrà eseguito. Questo è l'equivalente dell'esecuzione di un comando shell dal terminale.", + "fieldTextDetectorCommandTimeout": "Questo valore è un timer per consentire la prossima esecuzione del tuo script. Questo valore è in pochi minuti.", + "fieldTextDetectorFps": "Quanti cornici un secondo da inviare al rilevatore di movimento; 2 è il valore predefinito.", + "fieldTextDetectorFrame": "Questo leggerà l'intero fotogramma per le differenze di pixel. Questo è lo stesso della creazione di una regione che copre l'intero schermo.", + "fieldTextDetectorHttpApi": "Vuoi consentire i trigger HTTP a questa fotocamera?", + "fieldTextDetectorLisencePlate": "Abilita il riconoscimento della targa. Il plugin OpenAlPR ha sempre abilitato.", + "fieldTextDetectorLisencePlateCountry": "Scegli il tipo di piastre da riconoscere. Solo noi e l'UE sono supportati in questo momento.", + "fieldTextDetectorLockTimeout": "Lockout per quando è consentito il trigger successivo, per evitare di sovraccaricare il database e ricevere client. Misurato in millisecondi.", + "fieldTextDetectorMaxSensitivity": "La valutazione della fiducia del movimento deve essere inferiore a questo valore da vedere come un trigger. Lascia vuoto per nessun massimo. Questa opzione era precedentemente denominata \"Max Indifference\".", + "fieldTextDetectorNoiseFilter": "Tentare di filtrare il grano o un movimento ripetuto a una particolare indifferenza.", + "fieldTextDetectorNoiseFilterRange": "La quantità di differenza consentita in un pixel prima che sia considerato moto.", + "fieldTextDetectorNotrigger": "Controllare se si è verificato un movimento su un intervallo. Se si è verificato il movimento, il controllo verrà ripristinato.", + "fieldTextDetectorNotriggerCommand": "Il comando che verrà eseguito. Questo è l'equivalente dell'esecuzione di un comando shell dal terminale.", + "fieldTextDetectorNotriggerCommandTimeout": "Questo valore è un timer per consentire la prossima esecuzione del tuo script. Questo valore è in pochi minuti.", + "fieldTextDetectorNotriggerDiscord": "Se il movimento non è stato rilevato dopo il periodo di timeout, riceverai una notifica di discordia.", + "fieldTextDetectorNotriggerTimeout": "Il timeout è calcolato in pochi minuti.", + "fieldTextDetectorNotriggerWebhook": "Invia una richiesta GET a un URL con alcuni valori dall'evento.", + "fieldTextDetectorObjCount": "Contare gli oggetti rilevati.", + "fieldTextDetectorObjCountInRegion": "Contare gli oggetti solo all'interno delle regioni.", + "fieldTextDetectorPam": "Usa il rilevatore di movimento di Kevin Godell. Questo è integrato in Shinobi e non richiede altra configurazione per attivare.", + "fieldTextDetectorPtzFollow": "Seguire l'oggetto più grande rilevato con PTZ? Richiede un rilevatore di oggetti in esecuzione o matrici fornite di eventi.", + "fieldTextDetectorRecordMethod": "Esistono diversi modi per iniziare a registrare quando si verifica un evento, come il movimento. La registrazione tradizionale è la più facile da usare.", + "fieldTextDetectorSave": "Salva eventi di movimento in SQL. Ciò consentirà la visualizzazione del movimento rispetto ai video durante i tempi che gli eventi si sono verificati in Power Viewer.", + "fieldTextDetectorScaleX": "Larghezza dell'immagine rilevata. Le dimensioni più piccole richiedono meno CPU.", + "fieldTextDetectorScaleY": "Altezza dell'immagine rilevata. Le dimensioni più piccole richiedono meno CPU.", + "fieldTextDetectorSendFrames": "Spingere i frame al plug -in collegato da analizzare.", + "fieldTextDetectorSendFramesObject": "Spingere i frame al plug -in collegato da analizzare.", + "fieldTextDetectorSendVideoLength": "In secondi. La durata del video che viene inviato al servizio di notifica, come e -mail o discordia.", + "fieldTextDetectorSensitivity": "La valutazione della fiducia del movimento deve superare questo valore per essere visto come un trigger. Questo numero è correlato direttamente alla valutazione della confidenza restituita dal rilevatore di movimento. Questa opzione era precedentemente denominata \"indifferenza\".", + "fieldTextDetectorThreshold": "Numero minimo di rilevamenti per licenziare un evento di movimento. I rilevamenti devono essere all'interno del rilevatore la soglia divisa per i secondi del rivelatore FPS. Ad esempio, se il rivelatore FPS è 2 e la soglia del trigger è 3, è necessario che tre rilevamenti devono verificarsi entro 1,5 secondi per attivare un evento di movimento. Questa soglia è per regione di rilevamento.", + "fieldTextDetectorTimeout": "Il periodo di tempo \"record di trigger\" verrà eseguito per. Questo è letto in pochi minuti.", + "fieldTextDetectorTrigger": "Ciò ordinerà alla fotocamera di registrare se è impostato su \"solo guardia\" quando viene rilevato un evento.", + "fieldTextDetectorUseDetectObject": "Crea frame per l'invio a qualsiasi plug -in connesso.", + "fieldTextDetectorWebhook": "Invia una richiesta GET a un URL con alcuni valori dall'evento.", + "fieldTextDetectorWebhookTimeout": "Questo valore è un timer per consentire la prossima esecuzione del tuo webhook. Questo valore è in pochi minuti.", + "fieldTextDir": "Posizione di dove verranno salvati i file registrati. È possibile configurare più posizioni con la variabile AddStorage .", + "fieldTextEventDays": "Il numero di giorni per mantenere gli eventi prima dello spurgo.", + "fieldTextEventMonPop": "Quando si verifica un evento popolare il flusso di monitor.", + "fieldTextEventRecordScaleX": "Larghezza dell'immagine di registrazione basata su eventi che viene emessa dopo l'elaborazione.", + "fieldTextEventRecordScaleY": "Altezza dell'immagine di registrazione basata su eventi che viene utilizzata dopo l'elaborazione.", + "fieldTextExt": "Il tipo di file per il file video registrato.", + "fieldTextExtMP4": "Questo tipo di file è giocabile è quasi tutti i browser Web moderni, che includono dispositivi mobili. Il filesize tende ad essere più grande a meno che non si abbassa la qualità.", + "fieldTextExtWebM": "Piccolo fileszeze, bassa compatibilità client. Buono per caricare su siti come YouTube.", + "fieldTextFactorAuth": "Abilita un requisito secondario per l'accesso attraverso uno dei metodi abilitati.", + "fieldTextFatalMax": "Il numero di volte da riprovare per la connessione di rete tra il server e la fotocamera prima di impostare il monitor su disabilitato. Nessun decimali. Impostato su 0 per riprovare per sempre.", + "fieldTextFps": "La velocità in cui i frame sono registrati in file, frame al secondo. Essere consapevoli che non ci sono impostazioni predefinite. Questo può portare a file di grandi dimensioni. Meglio impostare questo lato della fotocamera.", + "fieldTextHeight": "Altezza dell'immagine del flusso.", + "fieldTextHlsListSize": "Il numero di segmenti massimo prima di eliminare automaticamente i vecchi segmenti.", + "fieldTextHlsTime": "Per quanto tempo dovrebbe essere ogni segmento video, in pochi minuti. Ogni segmento verrà disegnato dal client tramite un file M3U8. I segmenti più corti assumono meno spazio.", + "fieldTextHost": "Indirizzo di connessione", + "fieldTextHwaccel": "Motore di decodifica", + "fieldTextHwaccelVcodec": "Motore di decodifica", + "fieldTextInverseTrigger": "Per innescare le regioni esterne specifiche. Non si attiverà con il rilevamento del frame completo abilitato.", + "fieldTextIp": "Gamma o singolo", + "fieldTextIrCutFilterAuto": "Il filtro Cut IR viene attivato automaticamente dal dispositivo.", + "fieldTextIrCutFilterOff": "Disabilita IR Cut Fiter. In genere modalità notturna.", + "fieldTextIrCutFilterOn": "Abilita IR Cut Fiter. In genere modalità giorno.", + "fieldTextIsOnvif": "È una fotocamera conforme a Onvif?", + "fieldTextLang": "La lingua principale degli elementi di testo. Per una traduzione completa Aggiungi la tua lingua in conf.json E.G: \"lingua\": \"en_ca\", ", + "fieldTextLogDays": "Il numero di giorni per mantenere i tronchi prima dello spurgo.", + "fieldTextLoglevel": "La quantità di dati da fornire durante il lavoro.", + "fieldTextLoglevelAllWarnings": "Mostra tutti gli avvertimenti. Usalo se non riesci a scoprire cosa c'è che non va nella tua fotocamera.", + "fieldTextLoglevelFatal": "Visualizza solo errori fatali.", + "fieldTextLoglevelOnError": "Visualizza tutti gli errori importanti. Nota: questo non mostra sempre informazioni importanti.", + "fieldTextLoglevelSilent": "Nessuno. Questo silenzio di tutte le registrazioni.", + "fieldTextMail": "L'accesso per gli account. L'indirizzo e -mail del titolare dell'account principale riceverà notifiche.", + "fieldTextMapRtspTransportAuto": "Lascia decidere FFMPEG. Normalmente proverà prima UDP.", + "fieldTextMapRtspTransportTCP": "Impostalo su questo se UDP inizia a dare risultati indesiderati.", + "fieldTextMapRtspTransportUDP": "FFMPEG ci prova prima.", + "fieldTextMaxKeepDays": "Il numero di giorni per conservare i video prima di spurgare in modo specifico questo monitor.", + "fieldTextMid": "Questo è un identificatore non variabile per il monitor. È possibile duplicare un monitor facendo doppio clic sull'ID monitor e modificandolo.", + "fieldTextMode": "Questo è il compito principale del monitor.", + "fieldTextModeDisabled": "Monitor inattivo, non verrà creato alcun processo in questa modalità.", + "fieldTextModeRecord": "Registrazione continua. I segmenti vengono realizzati ogni 15 minuti per impostazione predefinita.", + "fieldTextModeWatchOnly": "Monitor si trasformerà solo in streaming, non si verificherà alcuna registrazione se non diversamente ordinato dall'API o dal rivelatore.", + "fieldTextMpass": "La password per la tua fotocamera", + "fieldTextMuser": "L'utente di accesso per la fotocamera", + "fieldTextName": "Questo è il nome visualizzato leggibile dall'uomo per il monitor.", + "fieldTextNotes": "Commenti che vuoi lasciare per questa fotocamera.", + "fieldTextOnvifNonStandard": "È una fotocamera Onvif non standard?", + "fieldTextOnvifPort": "ONVIF viene generalmente eseguito sulla porta 8000 . Questo può essere 80 anche a seconda del modello della fotocamera.", + "fieldTextPass": "Lasciare vuoto per mantenere la stessa password durante la modifica delle impostazioni.", + "fieldTextPasswordAgain": "Devi abbinare il campo della password se desideri cambiarlo.", + "fieldTextPath": "Il percorso per la tua fotocamera", + "fieldTextPort": "Separato da virgole o un intervallo", + "fieldTextPortForce": "L'uso della porta Web predefinita può consentire l'interruttore automatico ad altre porte per flussi come RTSP.", + "fieldTextPresetRecord": "Flag preimpostata per alcuni coder video. Se scopri che la tua fotocamera si blocca ogni pochi secondi: prova a lasciarla vuota.", + "fieldTextPresetStream": "Flag preimpostata per alcuni coder video. Se scopri che la tua fotocamera si blocca ogni pochi secondi: prova a lasciarla vuota.", + "fieldTextProbesize": "Specificare quanto è grande per creare la sonda di analisi per l'input. Impostare su 100000 se si utilizza RTSP e hai problemi di streaming.", + "fieldTextProtocol": "Il protocollo che consumerà il flusso video.", + "fieldTextRecordScaleX": "Larghezza dell'immagine del flusso.", + "fieldTextRecordScaleY": "Altezza dell'immagine del flusso.", + "fieldTextRecordTimelapse": "Crea un timelapse basato su JPEG.", + "fieldTextRecordTimelapseMp4": "Creare un file MP4 alla fine di ogni giorno per il timelapse.", + "fieldTextRecordTimelapseWatermark": "Un'immagine che viene bruciata sui fotogrammi del video registrato.", + "fieldTextRecordTimelapseWatermarkLocation": "Posizione dell'immagine che verrà utilizzata come filigrana.", + "fieldTextRecordTimelapseWatermarkPosition": "Un'immagine che viene bruciata sui fotogrammi del video registrato.", + "fieldTextRotate": "Cambia l'angolo di registrazione del flusso video.", + "fieldTextRtmpKey": "Tasto di streaming per flussi in arrivo sulla porta RTMP.", + "fieldTextRtspTransport": "Il protocollo di trasporto utilizzerà la fotocamera. TCP è di solito la scelta migliore.", + "fieldTextRtspTransportAuto": "Lascia decidere FFMPEG. Normalmente proverà prima UDP.", + "fieldTextRtspTransportHTTP": "Metodo di connessione standard.", + "fieldTextRtspTransportTCP": "Impostalo su questo se UDP inizia a dare risultati indesiderati.", + "fieldTextRtspTransportUDP": "FFMPEG ci prova prima.", + "fieldTextSfps": "Specificare il frame rate (FPS) in cui la fotocamera fornisce il suo flusso.", + "fieldTextSignalCheck": "Quante volte il tuo cliente controllerà lo streaming per vedere se è vivo. Questo è calcolato in minuti.", + "fieldTextSignalCheckLog": "Questo è solo per lato client. Verrà visualizzato nel thread di registro quando si verificano controlli del segnale lato client.", + "fieldTextSize": "La quantità di spazio su disco Shinobi consentirà di essere consumato prima dello spurgo. Questo valore viene letto in Megabyte.", + "fieldTextSizeFilebinPercent": "Percentuale dell'importo di archiviazione massimo L'archivio filebin può utilizzare.", + "fieldTextSizeTimelapsePercent": "Percentuale dell'importo di archiviazione massimo a cui i frame timelapse possono registrare.", + "fieldTextSizeVideoPercent": "Percentuale di importo di archiviazione massimo a cui i video possono registrare.", + "fieldTextSkipPing": "Scegli se è richiesto un ping riuscito prima di iniziare un processo di monitoraggio.", + "fieldTextSnap": "Ottieni l'ultimo fotogramma in JPEG.", + "fieldTextSnapSecondsInward": "in secondi", + "fieldTextSqllog": "Usalo con cautela poiché a FFMPEG piace lanciare dati superflui a volte, il che può portare a molte righe del database.", + "fieldTextSqllogNo": "No è il valore predefinito.", + "fieldTextSqllogYes": "Fallo se hai solo problemi ricorrenti.", + "fieldTextStreamAcodec": "Codec audio per lo streaming.", + "fieldTextStreamAcodecAac": "Utilizzato per il video MP4.", + "fieldTextStreamAcodecAc3": "Utilizzato per il video MP4.", + "fieldTextStreamAcodecAuto": "Lascia che FFMPEG scelga.", + "fieldTextStreamAcodecCopy": "Utilizzato per il video MP4. Ha un utilizzo della CPU molto basso, ma alcuni codec audio necessitano di flag personalizzati come -strict 2 per AAC.", + "fieldTextStreamAcodecLibmp3lame": "Utilizzato per il video MP4.", + "fieldTextStreamAcodecLibopus": "Utilizzato per il video WebM.", + "fieldTextStreamAcodecLibvorbis": "Utilizzato per il video WebM.", + "fieldTextStreamAcodecNoAudio": "Nessun audio, questa è un'opzione che deve essere impostata in alcune parti del mondo per motivi legali.", + "fieldTextStreamFlvType": "Questo è solo per il cruscotto Shinobi. Entrambi i metodi di flusso sono ancora attivi e pronti per l'uso.", + "fieldTextStreamFps": "La velocità in cui i frame vengono visualizzati ai client, in frame al secondo. Essere consapevoli che non ci sono impostazioni predefinite. Questo può portare ad un elevato utilizzo della larghezza di banda.", + "fieldTextStreamLoop": "Loop un file statico in modo che il flusso di file si comporti come un live streaming.", + "fieldTextStreamQuality": "Numero basso significa qualità superiore. Numero più alto significa meno qualità.", + "fieldTextStreamRotate": "Cambia l'angolo di visualizzazione del flusso video.", + "fieldTextStreamScaleX": "Larghezza dell'immagine del flusso che viene emessa dopo l'elaborazione.", + "fieldTextStreamScaleY": "Altezza dell'immagine del flusso che viene utilizzata dopo l'elaborazione.", + "fieldTextStreamTimestamp": "Un orologio che viene bruciato sui fotogrammi del flusso video.", + "fieldTextStreamTimestampBoxColor": "TimStamp Donkrop Color.", + "fieldTextStreamTimestampColor": "Timstamp Testo Colore.", + "fieldTextStreamTimestampFont": "FILE FONT per modellare il tuo timestamp.", + "fieldTextStreamTimestampFontSize": "Dimensione del carattere in Pt.", + "fieldTextStreamTimestampX": "Posizione orizztonale del timestamp", + "fieldTextStreamTimestampY": "Posizione verticale del timestamp", + "fieldTextStreamType": "Il metodo utilizzato per consumare il flusso video.", + "fieldTextStreamTypeBase64OverWebsocket": "Invio di frame codificati Base64 su WebSocket. Questo evita la memorizzazione nella cache ma non c'è audio.", + "fieldTextStreamTypeFLV": "Invio di frame codificati FLV su WebSocket.", + "fieldTextStreamTypeHLS(includesAudio)": "Metodo simile ai flussi live di Facebook. Include audio Se l'input lo fornisce. C'è un ritardo di circa 4-6 secondi perché questo metodo registra i segmenti li spinge al cliente piuttosto che spingere come mentre li crea.", + "fieldTextStreamTypeMJPEG": "MOVIMENTO STANDARD JPEG Immagine. Nessun audio.", + "fieldTextStreamTypePoseidon": "Poseidon è costruito sul codice di elaborazione MP4 di Kevin Godell. Simula un file MP4 in streaming ma utilizzando i dati di un flusso live. Include audio. Alcuni browser possono riprodurlo come un normale file MP4. Flussi su HTTP o WebSocket.", + "fieldTextStreamVcodec": "Codec video per lo streaming.", + "fieldTextStreamVcodecAuto": "Lascia che FFMPEG scelga.", + "fieldTextStreamVcodecCopy": "Utilizzato per il video MP4. Ha un utilizzo della CPU molto basso, ma non è possibile utilizzare filtri video e file file possono essere giganteschi. Meglio impostare le impostazioni MP4 sul lato della fotocamera quando si utilizza questa opzione.", + "fieldTextStreamVcodecLibx264": "Utilizzato per il video MP4.", + "fieldTextStreamVcodecLibx265": "Utilizzato per il video MP4.", + "fieldTextStreamVf": "Posizionare i filtri video FFMPEG in questa casella per influenzare la porzione di streaming. No spazi.", + "fieldTextStreamWatermark": "Un'immagine che viene bruciata sui frame del flusso video.", + "fieldTextStreamWatermarkLocation": "Posizione dell'immagine che verrà utilizzata come filigrana.", + "fieldTextStreamWatermarkPosition": "Un'immagine che viene bruciata sui frame del flusso video.", + "fieldTextTimestamp": "Un orologio che viene bruciato sui fotogrammi del video registrato.", + "fieldTextTimestampBoxColor": "TimStamp Donkrop Color.", + "fieldTextTimestampColor": "Timstamp Testo Colore.", + "fieldTextTimestampFont": "FILE FONT per modellare il tuo timestamp.", + "fieldTextTimestampFontSize": "Dimensione del carattere in Pt.", + "fieldTextTimestampX": "Posizione orizztonale del timestamp", + "fieldTextTimestampY": "Posizione verticale del timestamp", + "fieldTextTvChannel": "Questo monitor avrà abilitato le funzionalità del canale TV. Sarai in grado di visualizzarlo nell'elenco dei canali TV.", + "fieldTextTvChannelGroupTitle": "Un gruppo personalizzato per il canale.", + "fieldTextTvChannelId": "Un ID personalizzato per il canale.", + "fieldTextType": "Il metodo utilizzato per consumare il flusso video.", + "fieldTextTypeDashcam(StreamerV2)": "WebSocket Stream P2P basato su WebM.", + "fieldTextTypeH.264/H.265/H.265+": "Leggendo un video di alta qualità che a volte include audio.", + "fieldTextTypeHLS(.m3u8)": "Leggendo un video di alta qualità che a volte include audio.", + "fieldTextTypeJPEG": "Leggere le istantanee da un URL e fare un flusso e/o un video da loro.", + "fieldTextTypeLocal": "Lettura di schede di acquisizione, webcam o telecamere integrate.", + "fieldTextTypeMJPEG": "Simile a JPEG tranne che la maneggevolezza del telaio viene eseguita da FFMPEG, non da Shinobi.", + "fieldTextTypeMPEG4(.mp4/.ts)": "Un file statico. Leggi a un ritmo inferiore e non dovrebbe essere utilizzato per un effettivo streaming live.", + "fieldTextTypeMxPEG": "MoboTix MJPEG Stream", + "fieldTextTypeRTMP": "Impara a connetterti qui: articolo: Come spingere i flussi tramite RTMP a Shinobi ", + "fieldTextTypeShinobiStreamer": "Stream P2P basato su JPEG di WebSocket.", + "fieldTextVcodec": "Codec video per la registrazione.", + "fieldTextVf": "Posizionare i filtri video FFMPEG in questa casella per influenzare la parte di registrazione. No spazi.", + "fieldTextWallClockTimestampIgnore": "Basare tutti i dati della fotocamera in arrivo nel tempo della fotocamera anziché nel tempo del server.", + "fieldTextWatchdogReset": "Se c'è una sovrapposizione nel record di trigger in caso di ripristino.", + "fieldTextWatermark": "Un'immagine che viene bruciata sui fotogrammi del video registrato.", + "fieldTextWatermarkLocation": "Posizione dell'immagine che verrà utilizzata come filigrana.", + "fieldTextWatermarkPosition": "Un'immagine che viene bruciata sui fotogrammi del video registrato.", + "fieldTextWidth": "Larghezza dell'immagine del flusso.", + "fire hydrant": "idrante", + "flv": "flv", + "for Global Access": "per accesso globale", + "fork": "forchetta", + "frisbee": "frisbee", + "getAMonitor": "Ottieni un monitor", + "getATvChannel": "Ottieni canali TV per il monitor", + "getATvChannelText": "Ottieni i flussi H.264 disponibili di un singolo monitor in una playlist .M3U8.", + "getAllMonitors": "Ottieni tutti i monitor", + "getAllTvChannels": "Ottieni tutti i canali TV", + "getAllTvChannelsText": "Ottieni tutti i flussi H.264 in una playlist .M3U8. Abilita l'opzione del canale TV nelle impostazioni del monitor per vedere i loro flussi in questo elenco.", + "getUserInfo": "Ottieni informazioni sull'utente", + "getVideos": "Ottieni video", + "getVideosForMonitor": "Ottieni video per Monitor", + "giraffe": "giraffa", + "h264_cuvid": "H.264 Cuvid", + "h264_mmal": "H.264 (Raspberry Pi)", + "h264_nvenc": "H.264 NVENC (NVIDIA HW Accel)", + "h264_omx": "H.264 OpenMax (Raspberry Pi)", + "h264_qsv": "H.264 (video di sincronizzazione rapida)", + "h264_vaapi": "H.264 VA-API (Intel HW Accel)", + "h265BrowserText1": "Se stai cercando di riprodurre un file H.265, potrebbe essere necessario scaricarlo e aprirlo in un'altra applicazione come VLC.", + "hair drier": "asciugacapelli", + "handbag": "borsetta", + "hevc_cuvid": "H.265 Cuvid", + "hevc_nvenc": "H.265 NVENC (NVIDIA HW Accel)", + "hevc_qsv": "H.265 (video di sincronizzazione rapida)", + "hevc_vaapi": "H.265 VA-API (Intel HW Accel)", + "hlsOptions": "Opzioni HLS", + "hlsOptionsInvalid": "Le opzioni HLS non sono valide", + "horse": "cavalla", + "hot dog": "hot dog", + "hour": "ora", + "hours": "ore", + "hwaccel": "Motore di accelerazione", + "hwaccel_device": "Dispositivo HWACCEL", + "hwaccel_vcodec": "Decodificatore video", + "in": "nella", + "in Days": "in giorni", + "in seconds": "in secondi", + "keyId": "ID chiave", + "keyboard": "tastiera del computer", + "kite": "aquilone", + "knife": "coltello", + "laptop": "computer portatile", + "lastLogin": "Ultimo accesso", + "libmp3lame": "Libmp3lamy", + "libopus": "libopus", + "libvorbis (Default)": "libvorbis (impostazione predefinita)", + "libvpx (Default)": "libvpx (impostazione predefinita)", + "libvpx-vp9": "libvpx-vp9", + "libx264": "libx264", + "libx264 (Default)": "libx264 (impostazione predefinita)", + "libx265": "libx265", + "liveGridDescription": "Live Grid è il display a flusso multiplo per Shinobi. Questo metodo di visualizzazione è progettato principalmente per il desktop.", + "loginHandleUnbound": "Il login è stato non collegato da questo account.", + "microwave": "microonde", + "migrateText1": " Tipo di input Non è stato possibile analizzare. Si prega di impostarlo manualmente.", + "minute": "minuta", + "minutes": "minuti", + "mjpeg_cuvid": "Mjpeg cuvid", + "modifyVideoText1": "Il metodo non esiste. Controlla per assicurarsi che l'ultimo valore dell'URL non sia vuoto.", + "monSavedButNotCopied": "Il monitor è stato salvato ma non copiato su nessun altro monitor.", + "monitorConfigFinderDescription": "Questo strumento ti aiuterà a cercare configurazioni per le telecamere pubblicate dalla comunità. Tutti ospitati su shinobihub . Puoi pubblicare anche il tuo, aiuterebbe davvero la comunità :)", + "monitorEditFailedMaxReached": "Il tuo account ha raggiunto il numero massimo di telecamere che possono essere create. Parla con un amministratore se desideri che questo sia cambiato.", + "monitorEditText1": "Dati non validi, verificare che questa sia una stringa di importazione valida.", + "monitorEditText2": "Stringa di dettagli non validi. Controlla per vedere che è una stringa JSON e non un oggetto normale che viene superato.", + "monitorGetText1": "Richiesta incompleta, rimuovere l'ultima barra nell'URL o mettere un valore accettabile.", + "monitorStateNotEnoughChanges": "È necessario apportare una modifica alla configurazione del monitor prima di tentare di aggiungerla a un preimpostazione.", + "monitorStatesError": "Monitorare l'errore dei preset", + "months": "mesi", + "motorcycle": "motociclo", + "mouse": "topo", + "mpeg2_mmal": "MPEG-2 (Raspberry Pi)", + "mpeg2_qsv": "MPEG2 (video di sincronizzazione rapida)", + "mpeg4_cuvid": "MPEG4 CUVID", + "mpeg4_mmal": "MPEG-4 (Raspberry Pi)", + "noLoginTokensAdded": "Non ci sono accessi alternativi associati a questo account.", + "noSpecialCharacters": "Niente spazi o caratteri speciali.", + "noTriggerText": "Se il movimento non è stato rilevato dopo il periodo di timeout, riceverai una notifica di discordia.", + "noUndoForAction": "Non puoi annullare questa azione.", + "notActivatedText": "La tua installazione ha fallito l'attivazione.", + "notEnoughFramesText1": "Non abbastanza cornici per la compilazione.", + "notPermitted1": "Questa azione non è consentita dall'amministratore del tuo account. \"", + "on": "Su", + "on Error": "su errore", + "on Event": "su evento", + "onvifdeviceManagerGlobalTip": "ONVIF consente di modificare le impostazioni interne della fotocamera. OnVif è in qualche modo un termine ombrello, può significare molte cose sfortunatamente. In questo caso potresti vedere un'opzione in questo strumento, ma potrebbe non essere modificabile. Ciò è di solito dovuto al fatto che il fornitore della fotocamera non ha aggiunto questo metodo o si è deviato dall'uso previsto. In questi casi dovrai inserire la configurazione della fotocamera tramite il metodo prescritto del fornitore della fotocamera, questo sta generalmente aprendo l'indirizzo IP della fotocamera nel browser Web.", + "onvifdeviceSavedFoundErrorText": "Alcune impostazioni potrebbero essere tornate a un valore precedente. È possibile che l'opzione modificata non sia disponibile con questa fotocamera tramite ONVIF.", + "onvifdeviceSavedText": "Le impostazioni interne della fotocamera sono state salvate. Potrebbe essere necessario riavviare la fotocamera per avere queste modifiche.", + "openImagesDownloadConfirm": "Sei sicuro di voler iniziare a scaricare immagini e scatole di delimitazione (matrici preimpostate) da OpenImages?", + "openImagesDownloadConfirmStop": "Sei sicuro di voler smettere di allenarsi?", + "opencl": "OpenCl", + "opencvCascadesText": "Se non vedi nulla qui, basta scaricare questo pacchetto di cascades . Rilasciali in plugins/opencv/cascades Quindi premi aggiorna .", + "orange": "arancia", + "oven": "forno", + "p2pServerNotSelectedText": "Seleziona un server dall'elenco e premi Salva. Aspetta 10 secondi, quindi prova ad aprire la dashboard in remoto.", + "p2pSettingsText1": "Dovrai aggiornare questa pagina per applicare le modifiche.", + "parking meter": "parchimetro", + "performanceOptimizeText1": "La fotocamera fornisce dati sul flusso H.264. È possibile impostare il tipo di streaming su HLS, Poseidon e Codec video da copiare.", + "person": "persona", + "pizza": "Pizza", + "possibleInternalError": "Possibile errore interno", + "postDataBroken": "Controlla il formato del JSON. Assicurarsi che sia stringa e definito in \"dati\"", + "potted plant": "pianta in vaso", + "powerVideoEventLimit": "Hai impostato un limite di evento elevato. Sei sicuro di voler fare questa richiesta?", + "privateKey": "Chiave privata", + "qsv": "QSV", + "rebootingCamera": "Riavvia la fotocamera", + "refrigerator": "frigorifero", + "remote": "a distanza", + "restartRequired": "Il riavvio di Shinobi Core è necessario affinché le modifiche abbiano effetto.", + "sandwich": "Sandwich", + "scissors": "forbici", + "separateByCommasOrRange": "Separato da virgole o un intervallo", + "setMaxStorageAmountText": "Dovresti impostare l'importo di archiviazione massimo nelle impostazioni dell'account situate a sinistra. Trova l'opzione nella sezione Profilo. L'impostazione predefinita è di 10 GB.", + "sheep": "pecora", + "sink": "lavello", + "sizePurgeLockedText": "Il blocco di spurgo delle dimensioni (elimina OVERMAX) sembra non essere sbloccato. Sblocco ora ...", + "skateboard": "skateboard", + "skipPingText1": "Prova a impostare \"salta ping\" su sì.", + "skis": "sci", + "snowboard": "snowboard", + "sorryNo": "Scusa no", + "sorryNothingWasFound": "Scusa, niente è stato trovato.", + "spoon": "cucchiaio", + "sports ball": "palla sportiva", + "startUpText0": "Controllo del disco usato ..", + "startUpText1": "Disco di controllo completato utilizzato.", + "startUpText2": "Tutti gli utenti controllati, attendi di chiudere i file aperti e rimuovere i file oltre il limite dell'utente", + "startUpText3": "in attesa di dare video incompiuti controlla un po 'di tempo. 3 secondi.", + "startUpText4": "Monitor di inizio ... per favore aspetta ...", + "startUpText5": "Shinobi è pronto.", + "startUpText6": "Video orfani trovati e inseriti", + "stop sign": "segnale di stop", + "subAccountManager": "Manager di sotto-conto", + "substreamConnectionText": "È possibile lasciare il dettaglio della connessione così com'è se si desidera utilizzare le informazioni di connessione principali impostate sopra.", + "substreamOutputText": "Qui puoi impostare la configurazione del flusso su richiesta. Scopri latenza dei tipi di flusso qui. ", + "substreamText": "Questo è un metodo su richiesta per visualizzare il live streaming. Puoi farlo in modo che il processo di visualizzazione sia disponibile solo quando qualcuno sta guardando o da utilizzare per passare da una risoluzione bassa e alta.", + "suitcase": "valigia", + "superAdminText": "\"Super.json\" non esiste. Si prega di rinominare \"Super.sample.json\" in \"Super.json\".", + "superAdminTitle": "Shinobi: Super Admin", + "surfboard": "tavola da surf", + "teddy bear": "orsacchiotto di peluche", + "tennis racket": "racchetta da tennis", + "tie": "legare", + "toaster": "tostapane", + "toilet": "gabinetto", + "tokenNotUserBound": "Questa handle di accesso non è collegata a un utente su questo server!", + "tokenNotUserBoundPt2": "Digita le credenziali, quindi utilizza il pulsante di accesso Google per collegarsi rapidamente.", + "toothbrush": "spazzolino", + "total": "totale", + "traffic light": "semaforo", + "train": "treno", + "truck": "camion", + "tv": "tv", + "umbrella": "ombrello", + "undoAllUnsaveChanges": "Sei sicuro di volerlo fare? Ciò annulla tutti i cambiamenti non salvati.", + "unexpectedExitText": "Le informazioni su questa uscita saranno trovate prima di questo registro. Inoltre, ecco il comando FFMPEG che è stato utilizzato quando il processo si è schiantato.", + "updateCamerasInternalSettings": "Aggiornare le impostazioni interne della fotocamera?", + "updateKeyText1": "\"UpdateKey\" manca da \"Conf.json\", non può fare aggiornamenti in questo modo fino a quando non lo aggiungi.", + "updateKeyText2": "\"UpdateKey\" non è corretto.", + "updateNotice1": "L'aggiornamento di Shinobi significa sovrascrivere i file. Se hai modificato i file da solo, dovresti aggiornare shinobi manualmente. Le configurazioni e i file video non saranno modificati.", + "useSubStreamOnlyWhenWatching": "Solo quando si guarda, usa il substream", + "vaapi": "Vaapi (Va-api)", + "vase": "vaso", + "vda": "VDA (Apple VDA Hardware Acceleration)", + "vdpau": "vdpau", + "videoBuildingText1": "Il video sta attualmente costruendo. Controlla di nuovo più tardi.", + "videotoolbox": "Videotoolbox", + "vp8_cuvid": "VP8 NVENC (NVIDIA HW Accel)", + "vp8_qsv": "VP8 (video di sincronizzazione rapida)", + "vp9_cuvid": "VP9 NVENC (NVIDIA HW Accel)", + "wannaReset": "Vuoi ripristinare?", + "willTriggerAnEvent": "attiverà un evento", + "wine glass": "bicchiere di vino", + "years": "anni", + "zebra": "zebra" +} \ No newline at end of file diff --git a/languages/ru.json b/languages/ru.json index bd7866ec..8b171527 100644 --- a/languages/ru.json +++ b/languages/ru.json @@ -1,497 +1,1650 @@ { - "\"No Motion\" Detector": "Детектор \"Нет Движения\"", - "# of Allow MJPEG Clients": "Кол-во разрешённых MJPEG клиентов 0 для неограниченного", - "180 Degrees": "180 градусов", - "2-Factor Authentication": "Двухфакторная аутентификация", - "90 Clockwise": "90 по часовой стрелке", - "90 Clockwise and Vertical Flip": "90 по часовой стрелке и отражение по вертикали", - "90 Counter Clockwise and Vertical Flip (default)": "90 против часовой стрелки и отражение по вертикали (по умолчанию)", - "API": "API-интерфейс", - "API Key Added": "API-ключ добавлен", - "API Key Deleted": "API-ключ удалён", - "API Keys": "Ключи API", - "APIKeyAddedText": "Ключ добалвен. Вы уже можете использовать этот ключ.", - "APIKeyDeletedText": "Ключ был удален. Он больше не будет работать.", - "ASC": "Возрастание", - "Account Info": "Информация об аккаунте", - "AccountEditText1": "Невозможно изменить. Обновить страницу, если проблема повторяется.", - "Accounts": "Аккаунты", - "Action for Selected": "Действие для выбранных", - "Add": "Добавить", - "Add Monitor": "Добавить Монитор", - "Add New": "Добавить Новый", - "Admin": "Админ", - "Advanced": "Расширенный", - "Again": "Снова", - "All Monitors": "Все Мониторы", - "All Monitors and Privileges": "Все мониторы и привилегии", - "All Warnings": "Все Предупреждения", - "Allow Next Command": "Разрешить следующую команды в минутах", - "Allow Next Email": "Разрешить следующую электронную почту в минутах", - "Allow Next Trigger": "Разрешить следующий триггер в миллисекундах", - "Allowed IPs": "Допустимые IP-адреса", - "Analyzation Duration": "Продолжительность анализа", - "Archive": "Архив", - "Audio Codec": "Аудио-кодек", - "Authenticate": "Аутентифицировать", - "Auto": "Авто", - "Autosave": "Автосохранение", - "Base64 over Websocket": "Base64 через websocket", - "Bottom Left": "Внизу Слева", - "Bottom Right": "Внизу Справа", - "Browser Console Log": "Журнал консоли браузера", - "CPU": "ЦП", - "CPU indicator will not work. Continuing...": "Индикатор ЦП не будет работать. Продолжение...", - "CSS": "CSS Стиль вашей приборной панели.", - "Calendar": "Календарь", - "Camera Password": "Пароль камеры", - "Camera Username": "Логин камеры", - "Camera is not recording": "Камера не записывает", - "CameraNotRecordingText": "Параметры могут быть несовместимы. Проверьте кодировку потока. Перезапуск...", - "Can Control Monitors": "Может управлять мониторами", - "Can Delete Videos": "Может удалять видео", - "Can Delete Videos and Events": "Может удалять видео и события", - "Can Edit Monitor": "Может редактировать мониторы", - "Can Get Logs": "Может получать журнал", - "Can Get Monitors": "Может получать мониторы", - "Can View Monitor": "Может смотреть мониторы", - "Can View Snapshots": "Может смотрет снимки", - "Can View Streams": "Может смотреть потоки", - "Can View Videos": "Может смотреть видео", - "Can View Videos and Events": "Может смотреть видео и события", - "Can't Connect": "Не удалось подключиться", - "Center": "Центр URL-Адрес", - "Chat on Discord": "Чат в Discord", - "Check": "Проверить", - "Check Signal Interval": "Интервал проверки сигнала в минутах", - "Check for Motion First": "Сначала проверить движение", - "Close": "Закрыть", - "Closed": "Закрытые", - "Command": "Команда", - "Command on Trigger": "Команда на триггер", - "Complete Stream URL": "Полный URL потока", - "Confirm": "Подтвердить", - "Connected": "Подключен", - "Connection Type": "Тип Подключения", - "Control": "Управление", - "Control Error": "Ошибка управления", - "ControlErrorText1": "Управление не включено", - "Controllable": "Управляемая", - "Country of Plates": "Страна номерного знака", - "Counts of Motion": "Количество движений", - "Current": "Текущий", - "Currently viewing": "Сейчас просматривают", - "Custom": "Пользовательский", - "Custom Base URL": "Базовый пользовательский URL оставьте пустым, чтобы использовать URL узла", - "DB Lost.. Retrying..": "Подключение к БД утеряно. Повтор...", - "DESC": "Убывание", - "Dashboard": "Приборной панели", - "Dashboard Language": "Язык Приборной Панели", - "Dashcam": "Видеорегистратор", - "Dashcam (Streamer v2)": "Видеорегистратор (Streamer v2)", - "Date Range": "Диапазон дат", - "Debug": "Отладка", - "Default": "По умолчанию", - "Delete": "Удалить", - "Delete Filter": "Удалить фильтр", - "Delete Matches": "Удалить совпадения", - "Delete Monitor": "Удалить монитор", - "Delete Motionless Video": "Удалить видео без движения", - "Delete Motionless Videos (Record)": "Удалить видео без движения (запись)", - "Delete Selected Videos": "Удалить выбранные видео", - "Delete Video": "Удалить видео", - "Delete selected": "Удалить выбранные", - "DeleteMonitorText": "Вы действительно хотите удалить этот монитор? Это действие нельзя отменить. Файлы для этого ID останутся в файловой системе. Если позже вы создадите монитор с таким-же ID, события станут видны в личном кабинете.", - "DeleteSelectedVideosMsg": "Вы действительно хотите удалить эти видео? Это действие нельзя отменить.", - "DeleteVideoMsg": "Вы действительно хотите удалить это видео? Это действие нельзя отменить.", - "Deleted": "Удален", - "Detect Objects": "Обнаруживать объекты ниже", - "Detector": "Детектор", - "Detector Flags": "Флаги детектора", - "Detector Rate": "Скорость детектора (кадры в секунду)", - "DetectorText": "

Когда показана ширина и высота области, установите её размер в 640х480 или меньше. Это позволит оптимизировать скорость чтения кадров.

", - "Disable Night Vision": "Отключение ночного видения URL", - "Disable Nightvision": "Отключить ночное видение", - "Disabled": "Отключен", - "Documentation": "Документация", - "Don't show this anymore": "Больше не показывать это", - "Double Quote Directory": "Двойные кавычки в имени каталога у некоторых каталогов есть пробелы в имени. Из-за этого может произойти сбой некоторых камер.", - "Down": "Движение вниз URL", - "Down Stop": "Остановка движения вниз URL", - "Download": "Скачать", - "EU": "Европа", - "Edit": "Редактировать", - "Email": "Электронная почта", - "Email Details": "Детали на электронную почту", - "Email on No Motion": "Письмо при отсутствии движения", - "Email on Trigger": "Письмо при срабатывании триггера письма будут приходить на почту, утсановленную как логин для аккаунта", - "Enable Night Vision": "Включение ночного видения URL", - "Enable Nightvision": "Включение ночного видения", - "Enabled": "Включен", - "End": "Конец", - "End Time": "Время окончания", - "Ended": "Закончился", - "Enlarge": "Увеличить", - "Enter this code to proceed": "Введите этот код, чтобы продолжить", - "Equal to": "Равно", - "Error Connecting": "Ошибка подключения", - "Event": "Событие", - "Event Limit": "Ограничение событий", - "EventText1": "Сработало событие движения в", - "EventText2": "Не удалось отправить изображение по электронной почте - файл не был доступен", - "Events": "События", - "Example": "Пример", - "Execute Command": "Выполнить команду", - "Executed": "Выполнен", - "Export": "Экспорт", - "FFmpegCantStart": "Не удалось запустить FFmpeg", - "FFmpegCantStartText": "Ядро записи для этой камеры не удалось запустить. Это может быть вызвано ошибочной настройкой камеры. Если, помимо этой, есть другие записи в журнале, пожалуйста, пришлите их в Issues на GitLab.", - "FFmpegTip": "FFprobe - это простой анализатор мултимедийных потоков. Вы можете использовать его для вывода всевозможной информации о потоке, включая продолжительность, частоту кадров, размер кадра и т. д.", - "FFprobe": "Зонд FFprobe", - "FactorAuthText1": "Код будет активен только в течение 15 минут. Если вы войдёте снова, таймер будет сброшен до 15 минут с тем же кодом.", - "Fatal": "Фатальная", - "Fatal Maximum Reached": "Фатальный максимум достигнут, камера останавливается.", - "FatalMaximumReachedText": "Ошибка JPEG была фатальной.", - "Feed-in Image Height": "Входная высота изображения", - "Feed-in Image Width": "Входная ширина изображения", - "Fields cannot be empty": "Поля не могут быть пустыми", - "File Not Exist": "Файл не существует", - "File Not Found": "Файл не найден", - "File Type": "Тип файла", - "FileNotExistText": "Невозможно сохранить несуществущий файл. Что-то пошло не так.", - "Filename": "Имя файла", - "Filesize": "Размер файла", - "Filter ID": "ID фильтра", - "Filter Matches": "Совпадение фильтра", - "Filter Name": "Название фильтра", - "FilterMatchesText1": "Этот фильтр удовлетворил условия.", - "FilterMatchesText2": "видео найдено.", - "Filters": "Фильтры", - "Filters Updated": "Фильтры обновлены", - "FiltersUpdatedText": "Ваши изменения были сохранены и применены.", - "Find Where": "Найти где", - "Fix": "Исправить", - "Fix Video": "Исправить видео", - "FixVideoMsg": "Вы действительно хотите исправить это видео? Это действие нельзя отменить.", - "Font Path": "Путь к шрифту", - "Font Size": "Размер шрифта", - "Force Port": "Порт (принудительно)", - "Found Devices": "Найдено устройств", - "Frame Rate": "Частота кадров (кадров / с)", - "Full Frame Detection": "Обнаружение в полном Кадре", - "Fullscreen": "Полноэкранный", - "Greater Than": "Больше", - "Greater Than or Equal to": "Больше или равно", - "Group Key": "Ключ группы", - "Group Name": "Название группы", - "Grouping": "Группировка ", - "H.264 / H.265 / H.265+": "Формат H.264 / H.265 / H.265+", - "HLS (.m3u8)": "Формат HLS (.m3u8)", - "HLS (includes Audio)": "Формат HLS со звуком", - "HLS Audio Encoder": "HLS Аудио кодировщик", - "HLS List Size": "Размер списка HLS", - "HLS Preset": "Предустановка HLS", - "HLS Segment Length": "Длина сегмента HLS в секундах", - "HLS Video Encoder": "HLS Видео кодировщик", - "HTTP": "Протокол http", - "HTTPS": "Протоколу https", - "Height": "Высота", - "Help": "Помощь", - "Hide List": "Скрыть список", - "Hide Notes": "Скрыть заметки", - "Host": "Хост", - "Hotswap Modes (Watch-Only)": "Режим горячей замены (только просмотр)", - "How to Record": "Как записать", - "IP Address": "IP-адрес", - "Identity": "Идентификация", - "IdentityText1": "Здесь определяется, как система будет идентифийироват данные этого потока. После сохранения вы не сможете изменить ID монитора. Если хотите, можете сделать ID монитора понятным для людей перед прожолжением.", - "IdentityText2": "Вы можете сделать дубликат монитора, изменив ID монитора и нажав \"Сохранить\". Запрещено использовать ID существующего монитора - в противном случае его информация в БД будет перезаписана.", - "Idle": "Простаивающий", - "Image Height": "Высота изображения", - "Image Location": "Путь к изображению абсолютный путь к файлу, или оставьте поле пустым для использования глобального", - "Image Position": "Положение изображения", - "Image Width": "Ширина изображения", - "Import": "Импорт", - "Import Monitor Configuration": "Импорт конфигурации монитора", - "ImportMonitorConfigurationText": "Это перезапишет любые не сохранённые изменения. Импортированные настройки будут применены только при нажатии Сохранить.", - "In": "В", - "Incorrect Settings Chosen": "Выбраны неправильные установки", - "Indifference": "Безразличие", - "Input": "Ввод", - "Input Flags": "Флаги ввода", - "Input Type": "Тип ввода", - "InputText1": "Этот раздел описывает, как использовать поток. Для оптимальной производительности, измените внутренние настройки камеры. Найдите соответствующие настройки и установите их, как показано ниже. Чтобы обнаружить вашу камеру, вы можете использовать встроенный ONVIF-сканер. Некоторые ONVIF камеры требуют использования специального инструмента управления для изменения внутренних настроек. Если Вы не можете обнаружить ваши, попробуйте ONVIF Device Manager для Windows.", - "InputText2": "
  • Кадров в секунду (FPS) :10-15 FPS, минимум 2-5 FPS
  • Интервал I-кадра: 80
  • Тип битрейта: CBR (постоянный)
  • Битрейт: от 256Кбит/с до 500 Кбит/с
", - "InputText3": "Если вам нужна помощь в определении потока вашей камеры, обратитесь к странице Списка URL-адресов камер на сайте Shinobi.", - "Invalid JSON": "Недопустимый формат JSON", - "InvalidJSONText": "Пожалуйста, убедитесь, что для конфигурации монитора указана корректная JSON-строка.", - "JPEG": "В формате JPEG", - "JPEG (Auto Enables JPEG API)": "JPEG ('Авто'' включает JPEG API)", - "JPEG API": "JPEG API cgi-bin", - "JPEG Error": "Ошибка JPEG", - "JPEG Mode": "Режим JPEG", - "JPEGErrorText": "Ошибка получения JPEG данных с камеры.", - "Leave blank for random.": "Оставьте пустым для случайного значения.", - "Left": "Влево URL", - "Left Stop": "Остановка влево URL", - "Less Than": "Меньше", - "Less Than or Equal to": "Меньше или равно", - "Like": "Наподобие", - "Lisence Plate Detector": "Распознавание номерных знаков", - "List Toggle": "Переключение списка", - "Live Stream Toggle": "Переключение живого вида", - "Live View": "Живой вид", - "Local": "Локальный", - "Log Level": "Уровень журналирования", - "Log Signal Event": "Журналировать сигнальные события только на стороне клиента", - "Logging": "Журналирование", - "Login": "Логин", - "Logout": "Выход", - "Logs": "Журналы", - "MB": "Мб", - "MJPEG": "Формат MJPEG", - "MP4 (copy, libx264, libx265)": "Формат MP4 (копирование, libx264, libx265)", - "MPEG-4 (.mp4 / .ts)": "Формат MPEG-4 (.mp4 / .ts)", - "MailError": "Ошибка электронной почты: не удалось отправить письмо, проверьте настройки в conf.json.", - "Matches": "Совпадения", - "Max Storage Amount": "Максимально занимаемое место в мегабайтах", - "Mode": "Режим", - "Monitor": "Монитор", - "Monitor Added by user": "Монитор добавлен пользователем.", - "Monitor Capture Rate": "Скорость захвата монитора(FPS)", - "Monitor Groups": "Группы мониторов", - "Monitor ID": "ID монитора", - "Monitor Idling": "Простаивание монитора", - "Monitor Name": "Имя монитора", - "Monitor Settings": "Настройки монитора", - "Monitor Stopped": "Монитор остановился", - "Monitor Updated by user": "Монитор обновлён пользователем.", - "Monitor mode changed": "Изменён режим монитора", - "Monitor mode is already": "Режим монитора уже установлен в", - "Monitor or Key does not exist.": "Монитор или ключ не существует.", - "MonitorIdlingText": "Сессия монитора изменена на простаивающую.", - "MonitorStoppedText": "Сессия монитора остановлена.", - "Monitors": "Мониторы", - "Monitors per row": "Мониторов на строку для монтажа", - "Montage": "Монтаж", - "Motion GUI": "Интерфейс движения", - "Motion Meter": "Шкала движения", - "Name": "Название", - "No": "Нет", - "No Audio": "Без звука", - "No Data": "Нет данных", - "No Events found for this video": "Для этого видео событий не найдено", - "No Group with this key exists": "Групп с таким ключом не существует", - "No Monitor Found, Ignoring Request": "Не найден монитор Нашли, запрос проигнорирован", - "No Rotation": "Без вращения", - "No such file": "Нет такого файла", - "NoMotionEmailText1": "Нет движения", - "NoMotionEmailText2": "Не было никакого движения в камере", - "NoVideosFoundForDateRange": "Не найдено видео в указанном промежутке. Попробуйте расширить диапазон дат.", - "Not Authorized": "Не разрешено", - "Not Connected": "Не подключен", - "Not Equal to": "Не равно", - "Not In": "Не входит", - "Not Matches": "Не соответствует", - "Not Permitted": "Не допускается", - "Not an Administrator Account": "Не является аккаунтом администратора", - "NotAuthorizedText1": "Не разрешено, подайте команду init с \"auth\",\"ke\", and \"uid\"", - "Notes": "Примечания", - "NotesPlacholder": "Комментарии для этих настроек камеры.", - "Number of Days to keep": "Количество дней хранения", - "ONVIF": "Стандарт ONVIF", - "ONVIF Scanner": "Сканер ONVIF", - "ONVIFnote": "Обнаружте ONVIF-совместимые в сетях за пределами вашей, или оставьте поле пустым для сканирования вашей текущей сети.
Логин и пароль можно оставить пустым.", - "OpenCV Cascades": "Каскады OpenCV", - "Order Streams": "Порядок потоков", - "Output Method": "Способ вывода", - "Password": "Пароль", - "Password Again": "Пароль ещё раз", - "Passwords don't match": "Пароли не совпадают", - "Paste JSON here.": "Вставьте JSNON здесь.", - "Path": "Путь", - "Permissions": "Разрешения", - "Points": "Точки для добавления точки, щелкните на границе фигуры.", - "Port": "Порт", - "Position X": "Положение по X", - "Position Y": "Положение по Y", - "Power Video Viewer": "Продвинутый просмотр видео", - "Power Viewer": "Продвинутый просмотр", - "Preferences": "Предпочтения", - "Preset": "Предустановка", - "Probe Size": "Размер зонда", - "Process Crashed for Monitor": "Процесс завершился аварийно для монитора", - "Process Unexpected Exit": "Неожиданное завершение процесса", - "Profile": "Профиль", - "Quality": "Качество 1 - высокий, 23 - низкий", - "Query": "Запрос", - "RAM": "ОЗУ", - "RTSP": "Протокол RTSP", - "RTSP Transport": "Транспорт RTSP", - "Range or Single": "Диапазон или одиночный", - "Rate": "Скорость (кадров в секунду)", - "Record": "Запись", - "Record File Type": "Тип файла записи", - "Record Height": "Высота записи", - "Record Video Filter": "Видео-фильтр записи", - "Record Width": "Ширина записи", - "Recording": "Запись", - "Recording Flags": "Флаги записи", - "Recording Segment Interval": "Интервал сегмента записи в минутах", - "Recording Timeout": "Тайм-аут записи в минутах", - "Recording Timestamp": "Временная метка записи", - "Recording Watermark": "Водяной знак на записи", - "RecordingText": "Рекоменжуется установить Тип файла записи в WebMMP4 и Видео-кодек в libvpxcopy or libx264, потому что Тип ввода установлен в .", - "Refresh List of Cascades": "Обновить список каскадов", - "Region Editor": "Редактор областей", - "Region Name": "Название области", - "RegionNote": "Точки сохраняются только при нажатии Сохранить в окне настройки монитора.", - "Regions": "Области", - "Remember Me": "Запомнить меня", - "Reset Timer": "Сброс таймера", - "Restarting Process": "Процесс перезагрузки", - "Retry Connection": "Количество попыток подключения допустимое количество неудачных попыток", - "Retrying...": "Повтор...", - "Right": "Вправо URL", - "Right Stop": "Остановить вправо URL", - "Rotate": "Повернуть", - "Save": "Сохранить", - "Save Directory": "Каталог для сохранения", - "Save Events to SQL": "Сохранять события в БД", - "Save Log in SQL": "Сохранить журнал в БД Следите щза размером БД, т.к. она может быстро заполниться", - "Save as": "Сохранить как", - "Saved Filters": "Сохраненные фильтры", - "Scan Settings": "Параметры сканирования", - "Search": "Поиск", - "Send Frames": "Отправить кадры для анализа", - "Separate with commas, no spaces": "Отделить запятыми, без пробелов", - "Set to Watch Only": "Установить только для просмотра", - "Settings": "Настройки", - "Settings Changed": "Настройки изменены", - "SettingsChangedText": "Ваши настройки были сохранены и применены.", - "Shinobi": "Shinobi", - "Shinobi Streamer": "Трансляция Shinobi Streamer", - "Show Logs": "Показать журнал", - "Silent": "Молча", - "Simple": "Простой", - "Size (mb)": "Размер (Мб)", - "Snapshot": "Снимок", - "Snapshot Flags": "Флаги снимка", - "Snapshots": "Снимки", - "Sort By": "Сортировать по", - "Start": "Начало", - "Start Recording": "Начало записи", - "Start Time": "Время начала", - "Started": "Начато", - "Status Indicator": "Индикатор состояния", - "Stop URL": "URL остановки", - "Stream": "Трансляция", - "Stream Flags": "Флаги трансляции", - "Stream Timestamp": "Временная метка трансляции", - "Stream Type": "Тип потока", - "Stream Watermark": "Водяной знак трансляции", - "Stream to YouTube": "Транслировать на YouTube", - "Stream to YouTube Flags": "Флаги трансляции на YouTube", - "StreamText": "

Этот раздел описывает основной метод вещания и его настройки. Трансляция будет отображена на приборной доске. Если вы выберете HLS, JPEG, или MJPEG, то вы сможете просматривать поток в других программах.

Использование JPEG-потока по сути выключит основную трансляцию и приведёт к использованию корзины снимков для получения кадров.

", - "Streamer": "Вещатель", - "Streams": "Потоки", - "Superuser": "Суперпользователь", - "Switch on for Still Image": "Переключиться на неподвижное изображение", - "TCP": "Протокол TCP", - "Text Box Color": "Цвет текстового поля", - "Text Color": "Цвет текста", - "Time-lapse": "Интервальная съёмка", - "Time-lapse Tool": "Инструменты интервальной съёмки", - "Timeout": "Таймаут", - "Timeout Reset on Next Motion": "Сброс таймаута при следующем движении", - "Toggle Sidebar": "Переключение боковой панели", - "Top Left": "Верхний Левый", - "Top Right": "Верхний Правый", - "Trigger Record": "Триггер записи", - "Trigger Successful": "Триггер успешен", - "UDP": "Протокол UDP", - "URL": "URL-адрес", - "URL Stop Timeout": "Таймаут URL остановки вызвать URL после X миллисекунд", - "US": "США", - "Unable to Launch": "Не удалось запустить", - "UnabletoLaunchText": "Пожалуйста, сначала сохраните монитор а затем используйте редактор областей.", - "Up": "Вверх URL", - "Up Stop": "Остановить вверх URL", - "Username": "Логин", - "Value": "Значение", - "Video": "Видео", - "Video Codec": "Видео-кодек", - "Video Filter": "Видео-фильтр", - "Video Finished": "Видео окончено", - "Video Length (minutes) and Motion Count per video": "Продолжительность видео (минут) и кол-во движений на видео", - "Video Record Rate": "Скорость видео (кадров в секунду)", - "Video Status": "Статус видео", - "Video and Time Span (Minutes)": "Видео и отрезок времени (минут)", - "Videos": "Видео", - "Videos List": "Список видео", - "Watch": "Смотреть", - "Watch Only": "Только просмотр", - "WebDAV": "Протокол WebDAV", - "WebM (libvpx)": "WebM (libvpx)", - "Webdav Error": "Ошибка WebDAV", - "WebdavErrorText": "Не удается сохранить. Попробуйте сначала создать каталог.", - "Webhook": "Вебхук", - "Webhook URL": "URL вебхука", - "Width": "Ширина", - "Yes": "Да", - "Zoom In": "Увеличение URL", - "Zoom In Stop": "Остановка увеличениеURL", - "Zoom Out": "Уменьшить URL", - "Zoom Out Stop": "Остановка уменьшения URL-Адрес", - "a day": "день", - "a few seconds": "несколько секунд", - "a minute": "минуту", - "a month": "месяц", - "a year": "год", - "aac": "ААС", - "aac (Default)": "ААС (по умолчанию)", - "ac3": "АС3", - "ago": "назад", - "an hour": "час", - "blankPassword": "Оставьте поле пустым, чтобы сохранить текущий пароль", - "calendar": "календарь", - "clientStreamFailedattemptingReconnect": "Проверка потока на стороне клиента провалилась, попытка переподключения", - "confirmDeleteFilter": "Вы действительно хотите удалить этот фильтр? Это действие нельзя отменить.", - "copy": "копия", - "days": "дней", - "dropBoxSuccess": "Успех! Файлы сохранены в Dropbox.", - "for Global Access": "для глобального доступа", - "hours": "часов", - "in": "в", - "libmp3lame": "libmp3lame", - "libopus": "libopus", - "libvorbis (Default)": "libvorbis (по умолчанию)", - "libvpx (Default)": "libvpx (по умолчанию)", - "libvpx-vp9": "libvpx-vp9", - "libx264": "libx264", - "libx264 (Default)": "libx264 (по умолчанию)", - "libx265": "libx265", - "minutes": "минут", - "modifyVideoText1": "Метод не существует. Убкдитесь, что последнее значение URL не пустое.", - "monitorEditFailedMaxReached": "Достигнт предел создаваемых камер для вашего аккаунта. Свяжитесь с вашим администратором для изменения предела.", - "monitorEditText1": "Неверные данные - убедитесь, что введена корректная строка для импорта", - "monitorEditText2": "Неверная строка деталей. Убедитесь, что введа корректная JSON-строка вместо обычного объекта.", - "monitorGetText1": "неполный запрос, уберите слэш в конце URL или введите приемлимое значение.", - "months": "месяцев", - "noSpecialCharacters": "Без пробелов или специальных символов.", - "on": "на", - "on Error": "в случае ошибки", - "startUpText0": "Проверка используемого диска...", - "startUpText1": "Проверка используемого диска окончена.", - "startUpText2": "все пользователи проверены, ожидаю закрытия открытых файлов и удаления файлов, превышающих ограничения пользователей", - "startUpText3": "даю немного времени на завершение проверки видео. 3 секунды.", - "startUpText4": "Запуск мониторов... Пожалуйста, ожидайте...", - "startUpText5": "Shinobi готов.", - "startUpText6": "Несвязанные видео найдены и добавлены", - "superAdminText": "Файл \"super.json\" не существует. Переименуйте \"super.sample.json\" в \"super.json\".", - "superAdminTitle": "Shinobi: Супер Админ", - "total": "итого", - "updateKeyText1": "\"updateKey\" отсутствует в \"conf.json\", невозможно провести обновление, пока он не будет добавлен.", - "updateKeyText2": "\"updateKey\" неверный.", - "years": "лет" -} + "\"No Motion\" Detector": "Детектор \"Нет Движения\"", + "# of Allow MJPEG Clients": "Кол-во разрешённых MJPEG клиентов 0 для неограниченного", + "'Already Installing...'": "'Уже установка ...'", + "180 Degrees": "180 градусов", + "2-Factor Authentication": "Двухфакторная аутентификация", + "90 Clockwise": "90 по часовой стрелке", + "90 Clockwise and Vertical Flip": "90 по часовой стрелке и отражение по вертикали", + "90 Counter Clockwise and Vertical Flip (default)": "90 против часовой стрелки и отражение по вертикали (по умолчанию)", + "AND": "И", + "API": "API-интерфейс", + "API Key": "API -ключ", + "API Key Action Failed": "Действие ключа API не удалось", + "API Key Added": "API-ключ добавлен", + "API Key Deleted": "API-ключ удалён", + "API Keys": "Ключи API", + "APIKeyAddedText": "Ключ добалвен. Вы уже можете использовать этот ключ.", + "APIKeyDeletedText": "Ключ был удален. Он больше не будет работать.", + "ASC": "Возрастание", + "Accelerator": "Ускоритель", + "Account Info": "Информация об аккаунте", + "Account Information": "Информация об учетной записи", + "Account Privileges": "Привилегии учетной записи", + "Account Save": "Сохранить учетную запись", + "Account Settings": "Настройки учетной записи", + "AccountEditText1": "Невозможно изменить. Обновить страницу, если проблема повторяется.", + "Accounts": "Аккаунты", + "Action for Selected": "Действие для выбранных", + "Activated": "Активирован", + "Active Monitors": "Активные мониторы", + "Add": "Добавить", + "Add All": "Добавить все", + "Add Camera": "Добавить камеру", + "Add Cameras": "Добавить камеры", + "Add Channel": "Добавить канал", + "Add Input Feed": "Добавьте входной подачу", + "Add Map": "Добавить карту", + "Add Monitor": "Добавить Монитор", + "Add New": "Добавить Новый", + "AddToPreset": "Добавить в предустановку", + "Additional Inputs": "Дополнительные входы", + "Admin": "Админ", + "Advanced": "Расширенный", + "After": "После", + "Again": "Снова", + "Age": "Возраст", + "Alert Sound": "Предупреждающий звук", + "Alert Sound Delay": "Предупреждение задержки звука", + "All Logs": "Все журналы", + "All Monitors": "Все Мониторы", + "All Monitors and Privileges": "Все мониторы и привилегии", + "All Privileges": "Все привилегии", + "All Warnings": "Все Предупреждения", + "All streams in first feed": "Все потоки в первом корме", + "Allow API Trigger": "Разрешить API триггер", + "Allow Next Alert": "Разрешить следующее предупреждение", + "Allow Next Command": "Разрешить следующую команды в минутах", + "Allow Next Discord Alert": "Разрешить следующее предупреждение о раздорах за считанные минуты
", + "Allow Next Email": "Разрешить следующую электронную почту в минутах", + "Allow Next Trigger": "Разрешить следующий триггер в миллисекундах", + "Allow Next Webhook": "Разрешить следующий веб -крюк", + "Allowed IPs": "Допустимые IP-адреса", + "Already Processing": "Уже обработка", + "Already exists": "Уже существует", + "Alternate Logins": "Альтернативные логины", + "Always": "Всегда", + "Amazon S3": "Amazon S3", + "Amazon S3 Upload Error": "Ошибка загрузки Amazon S3", + "Analyzation Duration": "Продолжительность анализа", + "AppNotEnabledText": "Приложение не включено, включите его в настройки учетной записи.", + "April": "апрель", + "Archive": "Архив", + "Are you sure?": "Уверены ли вы?", + "Attach Snapshot": "Прикрепите снимок", + "Attach Video Clip": "Прикрепите видеоклип", + "Audio": "Аудио", + "Audio Bit Rate": "Битрейт аудио", + "Audio Codec": "Аудио-кодек", + "Audio Detection": "Обнаружение звука", + "Audio Detector": "Аудио детектор", + "Audio stream only from first feed": "Аудиочаст только от первого корма", + "Audio streams only": "Только аудиотоки", + "August": "Август", + "Authenticate": "Аутентифицировать", + "Authenticated": "Аутентифицирован", + "Authentication Failed": "Аутентификация не удалась", + "Auto": "Авто", + "Automatic": "Автоматический", + "Automatic Checking Cancelled": "Автоматическая проверка отменена", + "Automatic Codec Repair": "Автоматический ремонт кодека", + "Automatic Field Fill": "Автоматическое поле заполнения", + "Autosave": "Автосохранение", + "Back": "Назад", + "Backblaze B2": "Backblaze B2", + "Backblaze Error": "Ошибка Backblaze", + "BacklightCompensation": "Компенсация подсветки", + "Backup": "Резервный", + "Bandwidth": "Пропускная способность", + "Base64 over Websocket": "Base64 через websocket", + "Basic Authentication": "Основная аутентификация", + "Batch": "Партия", + "Before": "До", + "Bind Credentials": "Связайте учетные данные (пароль)", + "BitrateLimit": "Битратный предел", + "Blank for No Change": "Пусто без изменений", + "Bottom Left": "Внизу Слева", + "Bottom Right": "Внизу Справа", + "Brightness": "Яркость", + "Browser Console Log": "Журнал консоли браузера", + "Bucket": "Ведро", + "Buffer Preview": "Буферный предварительный просмотр", + "Build": "Строить", + "Build Video": "Построить видео", + "Building": "Строительство", + "CPU": "ЦП", + "CPU indicator will not work. Continuing...": "Индикатор ЦП не будет работать. Продолжение...", + "CPU used by this stream": "ЦП, используемый этим потоком", + "CSS": "CSS Стиль вашей приборной панели.", + "Calendar": "Календарь", + "Call Method": "Метод вызова", + "Camera Password": "Пароль камеры", + "Camera Username": "Логин камеры", + "Camera is not recording": "Камера не записывает", + "Camera is not running": "Камера не работает", + "Camera is not streaming": "Камера не потокосится", + "CameraNotRecordingText": "Параметры могут быть несовместимы. Проверьте кодировку потока. Перезапуск...", + "Can Authenticate Websocket": "Может аутентифицировать WebSocket", + "Can Change User Settings": "Может изменить настройки пользователя", + "Can Control Monitors": "Может управлять мониторами", + "Can Create and Delete Monitors": "Может создавать и удалять мониторы", + "Can Delete Videos": "Может удалять видео", + "Can Delete Videos and Events": "Может удалять видео и события", + "Can Edit Monitor": "Может редактировать мониторы", + "Can Get Logs": "Может получать журнал", + "Can Get Monitors": "Может получать мониторы", + "Can View Logs": "Может просматривать журналы", + "Can View Monitor": "Может смотреть мониторы", + "Can View Snapshots": "Может смотрет снимки", + "Can View Streams": "Может смотреть потоки", + "Can View Videos": "Может смотреть видео", + "Can View Videos and Events": "Может смотреть видео и события", + "Can edit Max Days": "Может отредактировать максимальные дни", + "Can edit Max Storage": "Может редактировать Max Storage", + "Can edit how long to keep Events": "Можно отредактировать, как долго продолжать мероприятия", + "Can edit how long to keep Logs": "Можно отредактировать, как долго вести журналы", + "Can use Admin Panel": "Может использовать панель администратора", + "Can use Amazon S3": "Может использовать Amazon S3", + "Can use Discord Bot": "Может использовать Discord Bot", + "Can use LDAP": "Может использовать LDAP", + "Can use SFTP": "Может использовать SFTP", + "Can use Wasabi Hot Cloud Storage": "Может использовать wasabi Hot Cloud Storage", + "Can use WebDAV": "Может использовать WebDav", + "Can't Connect": "Не удалось подключиться", + "Cannot watch a monitor that isn't running.": "Не могу смотреть монитор, который не работает.", + "Cards": "Открытки", + "Carousel in Background": "Карусель на фоне", + "Center": "Центр URL-Адрес", + "Channel": "Канал", + "Channel ID": "Идентификатор канала", + "Chat on Discord": "Чат в Discord", + "Check": "Проверить", + "Check Signal Interval": "Интервал проверки сигнала в минутах", + "Check for Motion First": "Сначала проверить движение", + "Check the Channel ID": "Проверьте идентификатор канала", + "Check the Recipient ID": "Проверьте идентификатор получателя", + "Clear": "Прозрачный", + "Clear Recorder Process": "Четкий процесс рекордера", + "Close": "Закрыть", + "Close All Monitors": "Закройте все мониторы", + "Closed": "Закрытые", + "Cloud": "Облако", + "Codec Mismatch": "Кодекс несоответствие", + "Color Threshold": "Цветовой порог", + "ColorSaturation": "Насыщение цвета", + "Command": "Команда", + "Command on Trigger": "Команда на триггер", + "Common Objects": "Общие объекты", + "Complete Stream URL": "Полный URL потока", + "Conditions": "Условия", + "Confidence": "Уверенность", + "Confidence of Detection": "Уверенность в обнаружении", + "Configuration": "Конфигурация", + "Confirm": "Подтвердить", + "Connected": "Подключен", + "Connected Users": "Подключенные пользователи", + "Connection": "Связь", + "Connection Type": "Тип Подключения", + "Connection timed out": "Время соединения истекло", + "Contains": "Содержит", + "Contrast": "Контраст", + "Control": "Управление", + "Control Error": "Ошибка управления", + "ControlErrorText1": "Управление не включено", + "ControlErrorText2": "Проверьте данные вашего подключения. Вам может потребоваться указание базовый URL на порт 8000 или 80. Проверьте информацию о аутентификации.", + "Controllable": "Управляемая", + "Controls and Logs": "Управления и журналы", + "Copied": "Скопированный", + "Copied to Clipboard": "Скопировано в буфер обмена", + "Copy": "Копия", + "Copy Connection Settings": "Скопируйте настройки подключения", + "Copy Custom Settings": "Скопируйте пользовательские настройки", + "Copy Detector Settings": "Настройки детектора копирования", + "Copy Group Settings": "Копировать настройки группы", + "Copy Input Settings": "Скопируйте настройки ввода", + "Copy JPEG API Settings": "Скопируйте настройки API JPEG", + "Copy Logging Settings": "Скопируйте настройки журнала", + "Copy Mode": "Копия режима", + "Copy Recording Settings": "Скопируйте настройки записи", + "Copy Remote Link": "Скопируйте удаленную ссылку", + "Copy Settings": "Настройки копирования", + "Copy Stream Channel Settings": "Скопировать настройки канала потока", + "Copy Stream Channels": "Копировать каналы потока", + "Copy Stream Settings": "Скопируйте настройки потока", + "Copy Stream URL": "Копировать потоковой URL", + "Copy Timelapse Settings": "Скопируйте настройки времени", + "Copy to Settings": "Скопируйте в настройки", + "Cores": "Ядер", + "Could not create Bucket.": "Не мог создать ведро.", + "Count Objects": "Считайте объекты", + "Count Objects only inside Regions": "Считать объекты только внутри регионов", + "Country of Plates": "Страна номерного знака", + "Counts of Motion": "Количество движений", + "Create Sub-Accounts at /admin": "Создать суб-аккинги в /admin", + "Creating New Account": "Создание новой учетной записи", + "Creation Interval": "Интервал создания", + "Current": "Текущий", + "Currently Active": "В настоящее время активно", + "Currently viewing": "Сейчас просматривают", + "Custom": "Пользовательский", + "Custom Auto Load": "Пользовательская автоматическая нагрузка", + "Custom Base URL": "Базовый пользовательский URL оставьте пустым, чтобы использовать URL узла", + "Custom Endpoint": "Пользовательская конечная точка", + "DB Lost.. Retrying..": "Подключение к БД утеряно. Повтор...", + "DESC": "Убывание", + "DHCP": "DHCP", + "DNS": "DNS", + "Daily Events": "Ежедневные события", + "Dashboard": "Приборной панели", + "Dashboard Language": "Язык Приборной Панели", + "Dashcam": "Видеорегистратор", + "Dashcam (Streamer v2)": "Видеорегистратор (Streamer v2)", + "Database": "База данных", + "Database Not Found": "База данных не найдена", + "Database row does not exist": "Ряд базы данных не существует", + "Date": "Дата", + "Date Added": "Дата Добавлена", + "Date Range": "Диапазон дат", + "Date Updated": "Дата обновлена", + "Date and Time": "Дата и время", + "DateTimeType": "Управление датой", + "DaylightSavings": "Дневного сбережения", + "Days": "Дни", + "Debug": "Отладка", + "December": "Декабрь", + "Default": "По умолчанию", + "Delay for Snapshot": "Задержка для снижения", + "Delete": "Удалить", + "Delete Camera": "Удалить камеру", + "Delete Filter": "Удалить фильтр", + "Delete Logs": "Удалить журналы", + "Delete Matches": "Удалить совпадения", + "Delete Monitor": "Удалить монитор", + "Delete Monitor State?": "Удалить состояние монитора", + "Delete Monitor States Preset": "Удалить состояния монитора предустановку", + "Delete Monitors and Files": "Удалить мониторы и файлы", + "Delete Motionless Video": "Удалить видео без движения", + "Delete Motionless Videos (Record)": "Удалить видео без движения (запись)", + "Delete Region": "Удалить регион", + "Delete Schedule": "Удалить график", + "Delete Selected Videos": "Удалить выбранные видео", + "Delete Timelapse Frame": "Удалите рамку временного значения", + "Delete Video": "Удалить видео", + "Delete selected": "Удалить выбранные", + "DeleteMonitorText": "Вы действительно хотите удалить этот монитор? Это действие нельзя отменить. Файлы для этого ID останутся в файловой системе. Если позже вы создадите монитор с таким-же ID, события станут видны в личном кабинете.", + "DeleteMonitorsText": "Вы хотите удалить эти мониторы? Вы не можете их восстановить. Вы можете сохранить файлы для этих идентификаторов в файловой системе. Если вы решите воссоздать монитор с одним из идентификаторов, видео и события станут виден на приборной панели.", + "DeleteSelectedVideosMsg": "Вы действительно хотите удалить эти видео? Это действие нельзя отменить.", + "DeleteThisMsg": "Вы хотите удалить это? Вы не можете восстановить это.", + "DeleteVideoMsg": "Вы действительно хотите удалить это видео? Это действие нельзя отменить.", + "Deleted": "Удален", + "Deleted Schedule Configuration": "Удаленная конфигурация расписания", + "Deleted State Configuration": "Удаленная конфигурация состояния", + "Detect Objects": "Обнаруживать объекты ниже", + "Detection": "Обнаружение", + "Detection Engine": "Двигатель обнаружения", + "Detection Event": "Событие обнаружения", + "Detector": "Детектор", + "Detector Buffer": "Буфер детектора", + "Detector Filters": "Детекторные фильтры", + "Detector Flags": "Флаги детектора", + "Detector Grouping": "Группировка детектора Добавить группы в настройках ", + "Detector Rate": "Скорость детектора (кадры в секунду)", + "Detector Recording Complete": "Детекторная запись завершена", + "Detector Recording Process Exited Prematurely. Restarting.": "Процесс записи детектора выходит преждевременно. Перезапуск.", + "DetectorText": "

Когда показана ширина и высота области, установите её размер в 640х480 или меньше. Это позволит оптимизировать скорость чтения кадров.

", + "Died": "Умер", + "Digest Authentication": "Аутентификация переваривания", + "Disable": "Запрещать", + "Disable Night Vision": "Отключение ночного видения URL", + "Disable Nightvision": "Отключить ночное видение", + "Disabled": "Отключен", + "Discord": "Раздор", + "Discord Alert on Trigger": "Предупреждение о раздорах на триггер", + "Discord Bot": "Discord Bot", + "Discord on No Motion": "Раздор на \"Нет движения\"", + "DiscordErrorText": "Отправка на раздор вызвала ошибку", + "DiscordFailedText": "Отправка в Discord не удалась", + "DiscordLoggedIn": "Discord Bot завершил подлинность", + "DiscordNotEnabledText": "Discord Bot не включен, включите его в настройки учетной записи.", + "Documentation": "Документация", + "Does Not Contain": "Не содержит", + "Don't Show for 1 Week": "Не показывай 1 неделю", + "Don't show this anymore": "Больше не показывать это", + "DontAddToPreset": "Не добавляй к предустановке", + "Double Quote Directory": "Двойные кавычки в имени каталога у некоторых каталогов есть пробелы в имени. Из-за этого может произойти сбой некоторых камер.", + "Down": "Движение вниз URL", + "Down Stop": "Остановка движения вниз URL", + "Download": "Скачать", + "Download Bandwidth": "Скачать полосу пропускания", + "Downloading Videos": "Загрузка видео", + "Downloading...": "Загрузка ...", + "Duplicate": "Дублировать", + "EU": "Европа", + "Easy Remote Access (P2P)": "Легкий удаленный доступ (P2P)", + "Edit": "Редактировать", + "Edit Configuration": "Редактировать конфигурацию", + "Edit Selected": "Редактировать выбранный", + "Edited Schedule Configuration": "Отредактированная конфигурация расписания", + "Edited State Configuration": "Отредактированная конфигурация состояния", + "Email": "Электронная почта", + "Email Details": "Детали на электронную почту", + "Email address is in use.": "Адрес электронной почты используется.", + "Email and Password fields cannot be empty": "Поля по электронной почте и пароля не могут быть пустыми", + "Email on No Motion": "Письмо при отсутствии движения", + "Email on Trigger": "Письмо при срабатывании триггера письма будут приходить на почту, утсановленную как логин для аккаунта", + "Emotion": "Эмоция", + "Emotion Average": "Эмоции средний", + "Enable": "Давать возможность", + "Enable Night Vision": "Включение ночного видения URL", + "Enable Nightvision": "Включение ночного видения", + "Enabled": "Включен", + "Encoding": "Кодирование", + "EncodingInterval": "I-Frame", + "End": "Конец", + "End Time": "Время окончания", + "Ended": "Закончился", + "Endpoint": "Конечная точка", + "Endpoint Address": "Адрес конечной точки", + "Enlarge": "Увеличить", + "Enter at least one IP": "Введите хотя бы один IP", + "Enter this code to proceed": "Введите этот код, чтобы продолжить", + "Equal to": "Равно", + "Error Connecting": "Ошибка подключения", + "Error While Decoding": "Ошибка при расшифровании", + "ErrorWhileDecodingText": "Ваше оборудование может иметь нестабильное соединение с сетью. Проверьте свои сетевые подключения.", + "ErrorWhileDecodingTextAudio": "Ваша камера предоставляет разбитые данные. Попробуйте отключить звук во внутренних настройках камеры.", + "Event": "Событие", + "Event Counts": "Количество событий", + "Event Filter Error": "Ошибка фильтра событий", + "Event Filters": "Фильтры событий", + "Event Limit": "Ограничение событий", + "Event Occurred": "Событие произошло", + "Event Rules": "Правила события", + "Event Webhook Error": "СОБЫТАЯ Ошибка Webhook", + "EventText1": "Сработало событие движения в", + "EventText2": "Не удалось отправить изображение по электронной почте - файл не был доступен", + "Events": "События", + "Events Found": "События найдены", + "Example": "Пример", + "Execute Command": "Выполнить команду", + "Executed": "Выполнен", + "Export": "Экспорт", + "Export Selected Videos": "Экспорт выбранных видео", + "Export Video": "Экспортное видео", + "ExportSelectedVideosMsg": "Вы хотите экспортировать эти видео? Это может занять некоторое время, чтобы застегнуть и загрузить.", + "Exposure": "Экспозиция", + "FFmpegCantStart": "Не удалось запустить FFmpeg", + "FFmpegCantStartText": "Ядро записи для этой камеры не удалось запустить. Это может быть вызвано ошибочной настройкой камеры. Если, помимо этой, есть другие записи в журнале, пожалуйста, пришлите их в Issues на GitLab.", + "FFmpegTip": "FFprobe - это простой анализатор мултимедийных потоков. Вы можете использовать его для вывода всевозможной информации о потоке, включая продолжительность, частоту кадров, размер кадра и т. д.", + "FFprobe": "Зонд FFprobe", + "FLV": "FLV", + "FLV Stream Type": "FLV -поток тип", + "FactorAuthText1": "Код будет активен только в течение 15 минут. Если вы войдёте снова, таймер будет сброшен до 15 минут с тем же кодом.", + "Fatal": "Фатальная", + "Fatal Maximum Reached": "Фатальный максимум достигнут, камера останавливается.", + "FatalMaximumReachedText": "Ошибка JPEG была фатальной.", + "February": "Февраль", + "Feed-in Image Height": "Входная высота изображения", + "Feed-in Image Width": "Входная ширина изображения", + "Female": "женский", + "Field Missing Value": "Поле отсутствующее значение", + "Fields cannot be empty": "Поля не могут быть пустыми", + "File Delete Error": "Файл Удалить ошибку", + "File Not Exist": "Файл не существует", + "File Not Found": "Файл не найден", + "File Not Found in Database": "Файл не найден в базе данных", + "File Not Found in Filesystem": "Файл не найден в файловой системе", + "File Type": "Тип файла", + "FileBin Share": "Filebin Share", + "FileNotExistText": "Невозможно сохранить несуществущий файл. Что-то пошло не так.", + "Filename": "Имя файла", + "Filesize": "Размер файла", + "Filter ID": "ID фильтра", + "Filter Matches": "Совпадение фильтра", + "Filter Name": "Название фильтра", + "Filter for Objects only": "Фильтр только для объектов", + "FilterMatchesText1": "Этот фильтр удовлетворил условия.", + "FilterMatchesText2": "видео найдено.", + "Filters": "Фильтры", + "Filters Updated": "Фильтры обновлены", + "FiltersUpdatedText": "Ваши изменения были сохранены и применены.", + "Find Where": "Найти где", + "First stream in feed": "Первый поток в подаче", + "Fix": "Исправить", + "Fix Video": "Исправить видео", + "FixVideoMsg": "Вы действительно хотите исправить это видео? Это действие нельзя отменить.", + "Flush PM2 Logs": "Flush PM2 журналы", + "Font Path": "Путь к шрифту", + "Font Size": "Размер шрифта", + "For Group": "Для группы", + "Force Monitors Per Row": "Силовые мониторы в ряду", + "Force Port": "Порт (принудительно)", + "Form Data Not Found": "Форма данных не найдены", + "Found Devices": "Найдено устройств", + "Frame Rate": "Частота кадров (кадров / с)", + "FrameRateLimit": "Предел скорости кадров (FPS)", + "Frames": "Рамы", + "Friday": "Пятница", + "Frigate": "Фрегат", + "Full Frame Detection": "Обнаружение в полном Кадре", + "Full Stream URL": "Полный поток URL", + "Full URL Path": "Полный URL -пути", + "Fullscreen": "Полноэкранный", + "Gateway": "Ворота", + "Gender": "Пол", + "Generate Subtitles": "Генерировать субтитры", + "Get Code": "Получить код", + "Get Logs to Client": "Получить журналы к клиенту", + "Global Detector Settings": "Глобальные настройки детектора", + "Google Drive": "Гугл Диск", + "GovLength": "Губернатор", + "Greater Than": "Больше", + "Greater Than or Equal to": "Больше или равно", + "Grid": "Сетка", + "Group Key": "Ключ группы", + "Group Key is in use.": "Групповой ключ используется.", + "Group Name": "Название группы", + "Grouping": "Группировка ", + "H.264 / H.265 / H.265+": "Формат H.264 / H.265 / H.265+", + "H264Profile": "H264 Профиль", + "HEVC (H.265)": "HEVC (H.265)", + "HLS (.m3u8)": "Формат HLS (.m3u8)", + "HLS (includes Audio)": "Формат HLS со звуком", + "HLS Audio Encoder": "HLS Аудио кодировщик", + "HLS List Size": "Размер списка HLS", + "HLS Live Start Index": "HLS Live Start Index", + "HLS Preset": "Предустановка HLS", + "HLS Segment Length": "Длина сегмента HLS в секундах", + "HLS Start Number": "HLS Start Number", + "HLS Video Encoder": "HLS Видео кодировщик", + "HTTP": "Протокол http", + "HTTPS": "Протоколу https", + "Hardware Accelerated": "Аппаратное ускорение", + "Height": "Высота", + "Help": "Помощь", + "Hide List": "Скрыть список", + "Hide Notes": "Скрыть заметки", + "Home": "Дом", + "Host": "Хост", + "Host Type": "Тип хоста", + "Hostname": "Имя хоста", + "Hotswap Modes (Watch-Only)": "Режим горячей замены (только просмотр)", + "How to Record": "Как записать", + "IP Address": "IP-адрес", + "Identity": "Идентификация", + "IdentityText1": "Здесь определяется, как система будет идентифийироват данные этого потока. После сохранения вы не сможете изменить ID монитора. Если хотите, можете сделать ID монитора понятным для людей перед прожолжением.", + "IdentityText2": "Вы можете сделать дубликат монитора, изменив ID монитора и нажав \"Сохранить\". Запрещено использовать ID существующего монитора - в противном случае его информация в БД будет перезаписана.", + "Idle": "Простаивающий", + "Image Height": "Высота изображения", + "Image Location": "Путь к изображению абсолютный путь к файлу, или оставьте поле пустым для использования глобального", + "Image Position": "Положение изображения", + "Image Width": "Ширина изображения", + "Imaging": "Визуализация", + "Import": "Импорт", + "Import Monitor Configuration": "Импорт конфигурации монитора", + "ImportMonitorConfigurationText": "Это перезапишет любые не сохранённые изменения. Импортированные настройки будут применены только при нажатии Сохранить.", + "ImportMultiMonitorConfigurationText": "Это будет перебросить любые мониторы с идентификаторами, существующими в файле импорта.", + "In": "В", + "Incorrect Settings Chosen": "Выбраны неправильные установки", + "Indifference": "Безразличие", + "Info": "Информация", + "Information": "Информация", + "Input": "Ввод", + "Input Feed": "Входной податок", + "Input Feeds Selected": "Входной подачу выбран", + "Input Flags": "Флаги ввода", + "Input Map": "Входная карта", + "Input Selector": "Селектор ввода", + "Input Settings": "Настройки ввода", + "Input Type": "Тип ввода", + "InputText1": "Этот раздел описывает, как использовать поток. Для оптимальной производительности, измените внутренние настройки камеры. Найдите соответствующие настройки и установите их, как показано ниже. Чтобы обнаружить вашу камеру, вы можете использовать встроенный ONVIF-сканер. Некоторые ONVIF камеры требуют использования специального инструмента управления для изменения внутренних настроек. Если Вы не можете обнаружить ваши, попробуйте ONVIF Device Manager для Windows.", + "InputText2": "
  • Кадров в секунду (FPS) :10-15 FPS, минимум 2-5 FPS
  • Интервал I-кадра: 80
  • Тип битрейта: CBR (постоянный)
  • Битрейт: от 256Кбит/с до 500 Кбит/с
", + "InputText3": "Если вам нужна помощь в определении потока вашей камеры, обратитесь к странице Списка URL-адресов камер на сайте Shinobi.", + "Inserted Schedule Configuration": "Вставленная конфигурация графика", + "Inserted State Configuration": "Вставленная конфигурация состояния", + "Install": "Установить", + "Interface": "Интерфейс", + "Invalid Data": "Неверные данные", + "Invalid JSON": "Недопустимый формат JSON", + "Invalid Settings": "Неверные настройки", + "InvalidJSONText": "Пожалуйста, убедитесь, что для конфигурации монитора указана корректная JSON-строка.", + "Inverse Trigger": "Обратный триггер", + "Invert Y-Axis": "Инвертный Y-ось", + "IrCutFilter": "Ночное видение", + "JPEG": "В формате JPEG", + "JPEG (Auto Enables JPEG API)": "JPEG ('Авто'' включает JPEG API)", + "JPEG API": "JPEG API cgi-bin", + "JPEG Error": "Ошибка JPEG", + "JPEG Mode": "Режим JPEG", + "JPEGErrorText": "Ошибка получения JPEG данных с камеры.", + "January": "Январь", + "July": "Июль", + "June": "Июнь", + "LDAP": "LDAP", + "LDAP Success": "LDAP успех", + "LDAP User Authenticated": "Пользователь LDAP аутентифицирован", + "LDAP User is New": "Пользователь LDAP новый", + "Landing Page": "Целевая страница", + "Last": "Прошлой", + "Last Modified": "Последнее изменение", + "Launch in New Window": "Запустить в новом окне", + "Leave blank for random.": "Оставьте пустым для случайного значения.", + "Leave blank for unlimited": "Оставить пустым для неограниченного", + "Left": "Влево URL", + "Left Stop": "Остановка влево URL", + "Legacy Webhook": "Legacy Webhook", + "Less Than": "Меньше", + "Less Than or Equal to": "Меньше или равно", + "License Activated": "Лицензия активирована", + "License Activation": "Активация лицензии", + "License Activation Failed": "Активация лицензии не удалась", + "License Key": "Лицензионный ключ", + "License Plate Detector": "Детектор номерного знака", + "Like": "Наподобие", + "Limited": "Ограничено", + "Link Google Account": "Ссылка Google Account", + "Link LDAP Account": "Ссылка LDAP Account", + "Link Shinobi": "Ссылка Шиноби", + "Lisence Plate Detector": "Распознавание номерных знаков", + "List Toggle": "Переключение списка", + "List of Videos Delete Error": "Список видео удаления ошибки", + "Live Grid": "Живая сетка", + "Live Stream Toggle": "Переключение живого вида", + "Live View": "Живой вид", + "Local": "Локальный", + "Log Level": "Уровень журналирования", + "Log Signal Event": "Журналировать сигнальные события только на стороне клиента", + "Log Stream": "Поток журнала", + "Logging": "Журналирование", + "Login": "Логин", + "Logout": "Выход", + "Logs": "Журналы", + "Loop Stream": "Петля потока", + "MB": "Мб", + "MJPEG": "Формат MJPEG", + "MP4 (copy, libx264, libx265)": "Формат MP4 (копирование, libx264, libx265)", + "MPEG-4 (.mp4 / .ts)": "Формат MPEG-4 (.mp4 / .ts)", + "MPEG-DASH (includes Audio)": "Mpeg-Dash (включает в себя аудио)", + "MQTT Client": "MQTT Клиент", + "MQTT Error": "Ошибка MQTT", + "MQTT Inbound": "MQTT вход", + "MQTT Outbound": "MQTT Источник", + "MailError": "Ошибка электронной почты: не удалось отправить письмо, проверьте настройки в conf.json.", + "Main": "Главный", + "Male": "Мужской", + "Manual": "Руководство по эксплуатации", + "Map": "карта", + "March": "Маршировать", + "Matches": "Совпадения", + "Matrices": "Матрицы", + "Max Indifference": "Максимальное безразличие", + "Max Latency": "Максимальная задержка", + "Max Number of Cameras": "Максимальное количество камер", + "Max Storage Amount": "Максимально занимаемое место в мегабайтах", + "MaxExposureTime": "Максимальное время экспозиции", + "MaxGain": "Максимальное усиление", + "Maximum Change": "Максимальное изменение", + "Maximum dB": "Максимум дБ", + "May": "Мая", + "Merge Selected Videos": "Объединить выбранные видео", + "Merge Video": "Слияние видео", + "Merge and Download": "Слияние и скачать", + "MergeSelectedVideosMsg": "Вы хотите объединить эти видео? Это может занять некоторое время, чтобы слияние и загрузку. В тот момент, когда соединение закрыто, файл будет удален. Убедитесь, что вы держите браузер открытым, пока он не будет завершен.", + "Methods": "Методы", + "Migrator": "Мигратор", + "MinExposureTime": "Минимальное время экспозиции", + "MinGain": "Минимальный прирост", + "Minimum Change": "Минимальное изменение", + "Minimum dB": "Минимальный дБ", + "Minutes": "Минуты", + "Mode": "Режим", + "Monday": "Понедельник", + "Monitor": "Монитор", + "Monitor Added by user": "Монитор добавлен пользователем.", + "Monitor Capture Rate": "Скорость захвата монитора(FPS)", + "Monitor Died": "Монитор умер", + "Monitor Edit": "Монитор редактирования", + "Monitor Groups": "Группы мониторов", + "Monitor ID": "ID монитора", + "Monitor Idling": "Простаивание монитора", + "Monitor Name": "Имя монитора", + "Monitor Settings": "Настройки монитора", + "Monitor Start": "Следите за запуском", + "Monitor States": "Мониторинг состояний", + "Monitor States and Schedules": "Контролировать состояния и графики", + "Monitor Stop": "Следите за остановкой", + "Monitor Stopped": "Монитор остановился", + "Monitor Updated by user": "Монитор обновлён пользователем.", + "Monitor is now Disabled": "Монитор теперь отключен", + "Monitor is now Idle": "Монитор теперь простаивает", + "Monitor is now Recording": "Монитор сейчас записывает", + "Monitor is now Watching": "Монитор сейчас смотрит", + "Monitor mode changed": "Изменён режим монитора", + "Monitor mode is already": "Режим монитора уже установлен в", + "Monitor or Key does not exist.": "Монитор или ключ не существует.", + "MonitorIdlingText": "Сессия монитора изменена на простаивающую.", + "MonitorStatesText": "Вы можете узнать о том, как использовать это здесь, на Shinobihub .", + "MonitorStoppedText": "Сессия монитора остановлена.", + "Monitors": "Мониторы", + "Monitors per row": "Мониторов на строку для монтажа", + "Monitors to Copy to": "Мониторы, чтобы скопировать в", + "Montage": "Монтаж", + "Motion": "Движение", + "Motion Detection": "Обнаружение движения", + "Motion GUI": "Интерфейс движения", + "Motion Meter": "Шкала движения", + "Motion Threshold": "Порог движения", + "Mp4Frag": "Mp4frag", + "Must be atleast one row": "Должен быть как минимум один ряд", + "Mute Audio": "Мореат звук", + "NTP": "НТП", + "NTP Servers": "NTP -серверы", + "NVIDIA": "Нвидия", + "Name": "Название", + "Name cannot be empty.": "Имя не может быть пустым.", + "Nameservers": "Серверы имен", + "Network": "Сеть", + "Network Manager": "Сетевой менеджер", + "Never": "Никогда", + "New Authentication Token": "Новый токен аутентификации", + "New Monitor": "Новый монитор", + "Newest": "Новейший", + "Next Video": "Следующее видео", + "No": "Нет", + "No API Key": "Нет ключа API", + "No Audio": "Без звука", + "No Data": "Нет данных", + "No Events found for this video": "Для этого видео событий не найдено", + "No Group with this key exists": "Групп с таким ключом не существует", + "No Monitor Exists with this ID.": "С этим идентификатором не существует монитора.", + "No Monitor Found, Ignoring Request": "Не найден монитор Нашли, запрос проигнорирован", + "No Monitor ID Present in Form": "Нет идентификатора монитора в форме", + "No Monitors Selected": "Никаких мониторов не выбрано", + "No Region": "Нет региона", + "No Rotation": "Без вращения", + "No Sound": "Без звука", + "No Trigger": "Нет триггера", + "No Videos Found": "Видео не найдено", + "No such file": "Нет такого файла", + "NoLogsFoundForDateRange": "Журналы не найдены в этом диапазоне дат. Попробуйте расширить диапазон дат.", + "NoMotionEmailText1": "Нет движения", + "NoMotionEmailText2": "Не было никакого движения в камере", + "NoVideosFoundForDateRange": "Не найдено видео в указанном промежутке. Попробуйте расширить диапазон дат.", + "Noise Filter": "Фильтр шума", + "Noise Filter Range": "Диапазон шумовых фильтров", + "Non-Standard ONVIF": "Нестандартный OnVif", + "Not Activated": "Не активирован", + "Not Authorized": "Не разрешено", + "Not Connected": "Не подключен", + "Not Equal to": "Не равно", + "Not Found": "не обнаружена", + "Not In": "Не входит", + "Not Matches": "Не соответствует", + "Not Permitted": "Не допускается", + "Not Saved": "Не спасен", + "Not an Administrator Account": "Не является аккаунтом администратора", + "NotAuthorizedText1": "Не разрешено, подайте команду init с \"auth\",\"ke\", and \"uid\"", + "Notes": "Примечания", + "NotesPlacholder": "Комментарии для этих настроек камеры.", + "Nothing exists": "Ничего не существует", + "Notice": "Уведомление", + "Notification Sound": "Звук уведомления", + "Notification Video Length": "Уведомление о длине видео", + "Notifications": "Уведомления", + "NotifyErrorText": "Отправка уведомления вызвала ошибку", + "November": "Ноябрь", + "Number of Days to keep": "Количество дней хранения", + "Numeric criteria unsupported for Region tests, Ignoring Conditional": "Числовые критерии, не поддерживаемые для региональных тестов, игнорируя условные", + "OAuth Code": "OAUTH CODE", + "OAuth Credentials": "Оаутские полномочия", + "ONVIF": "Стандарт ONVIF", + "ONVIF Device Manager": "OnVif Device Manager", + "ONVIF Port": "Onvif Port", + "ONVIF Scanner": "Сканер ONVIF", + "ONVIFErr400": "Нашел порт ONVIF, но разрешение не удалось при получении URL -адреса потока. Проверьте имя пользователя и пароль, используемые для сканирования. Убедитесь, что время вашей камеры и время сервера синхронизированы.", + "ONVIFErr404": "Не обнаружена. Это может быть просто веб -панель для сетевого устройства.", + "ONVIFErr405": "Метод не разрешен. Проверьте имя пользователя и пароль, используемые для сканирования.", + "ONVIFEventsNotAvailable": "События ONVIF недоступны", + "ONVIFEventsNotAvailableText1": "Эта услуга может быть недоступна для этой камеры или Onvif еще не инициализирована.", + "ONVIFnotCompliantProfileT": "Камера не является соответствием профиля Onvif T", + "ONVIFnote": "Обнаружте ONVIF-совместимые в сетях за пределами вашей, или оставьте поле пустым для сканирования вашей текущей сети.
Логин и пароль можно оставить пустым.", + "OR": "ИЛИ", + "Object": "Объект", + "Object Count": "Объект объектов", + "Object Detection": "Обнаружение объекта", + "Object Detector Flags": "Флаги детектора объектов", + "Object Tag": "Тег объекта", + "Objects to look for": "Объекты, чтобы искать", + "October": "Октябрь", + "Off": "Выключенный", + "Oldest": "Старейший", + "On": "На", + "On Unexpected Exit": "На неожиданном выходе", + "Open": "Открытым", + "Open All Monitors": "Откройте все мониторы", + "Open Remote Dashboard": "Открыть удаленную панель панели", + "OpenCV Cascades": "Каскады OpenCV", + "Operating Hours": "Часы работы", + "Optional": "Необязательный", + "Options": "Опции", + "Order Streams": "Порядок потоков", + "Original Choice": "Оригинальный выбор", + "Other Devices": "Другие устройства", + "Output": "Выход", + "Output Method": "Способ вывода", + "P2P API Key": "P2P API -ключ", + "P2P Host": "P2P хост", + "P2P Server Not Selected": "Сервер P2P не выбран", + "P2P Settings Applied": "Применяются настройки P2P", + "PTZ Tracking": "PTZ отслеживание", + "PTZ Tracking Target": "Цель отслеживания PTZ", + "Password": "Пароль", + "Password Again": "Пароль ещё раз", + "Passwords don't match": "Пароли не совпадают", + "Paste JSON here.": "Вставьте JSNON здесь.", + "Path": "Путь", + "Pause": "Пауза", + "Per Monitor": "За монитор", + "Performance Optimization Possible": "Оптимизация производительности возможна", + "Permissions": "Разрешения", + "Ping Failed": "Пинг не удался", + "Plain": "Простой", + "Play": "Играть", + "Playback": "Воспроизведение", + "Please Check Your Settings": "Пожалуйста, проверьте свои настройки", + "Please Wait for Completion": "Пожалуйста, дождитесь завершения, в зависимости от количества выбранных файлов это может занять некоторое время.", + "Please Wait or Click to Stop Checking": "Пожалуйста, подождите или нажмите, чтобы прекратить проверять", + "Please Wait...": "Пожалуйста, подождите...", + "Plugin": "Плагин", + "Plugin Manager": "Менеджер плагинов", + "Points": "Точки для добавления точки, щелкните на границе фигуры.", + "Pop": "Поп", + "Popout Monitor on Event": "Монитор всплыва", + "Port": "Порт", + "Pose": "Поза", + "Poseidon": "Посейдон", + "Position X": "Положение по X", + "Position Y": "Положение по Y", + "Power Video Viewer": "Продвинутый просмотр видео", + "Power Viewer": "Продвинутый просмотр", + "Preferences": "Предпочтения", + "Prefix": "Префикс", + "Preset": "Предустановка", + "Preset Name": "Предустановленное название", + "Presets": "Предустановка", + "Preview": "Предварительный просмотр", + "Previous Video": "Предыдущее видео", + "Primary Engine": "Первичный двигатель", + "Primary Input": "Первичный вход", + "Privileges": "Привилегии", + "Probe Size": "Размер зонда", + "Process Already Running": "Процесс уже работает", + "Process Crashed for Monitor": "Процесс завершился аварийно для монитора", + "Process Not Running": "Процесс не работает", + "Process Started": "Процесс начался", + "Process Unexpected Exit": "Неожиданное завершение процесса", + "Processor": "Процессор", + "Profile": "Профиль", + "Protocol": "Протокол", + "Public on ShinobiHub": "Общественность на Shinobihub", + "Quality": "Качество 1 - высокий, 23 - низкий", + "Query": "Запрос", + "Quick Settings": "Быстрые настройки", + "Quick Sync Video": "Quick Sync Video", + "RAM": "ОЗУ", + "RTMP": "RTMP", + "RTMP Stream": "Rtmp -поток", + "RTMP Stream Flags": "RTMP -потоковые флаги", + "RTMPS": "RTMPS", + "RTSP": "Протокол RTSP", + "RTSP Transport": "Транспорт RTSP", + "Range or Single": "Диапазон или одиночный", + "Raspberry Pi": "Raspberry Pi", + "Rate": "Скорость (кадров в секунду)", + "Raw": "Сырой", + "Raw H.264 Stream": "Raw H.264 Stream", + "Reason": "Причина", + "Reboot": "Перезагрузить", + "Reboot Camera": "Перезагрузить камеру", + "Recent Events": "Недавние события", + "Recent Videos": "Недавние видео", + "Recipient ID": "Идентификатор получателя", + "Recommended": "рекомендуемые", + "Reconnect Stream": "Восстановите поток", + "Record": "Запись", + "Record File Type": "Тип файла записи", + "Record Height": "Высота записи", + "Record Video Filter": "Видео-фильтр записи", + "Record Width": "Ширина записи", + "Recorded Buffer": "Записанный буфер", + "Recording": "Запись", + "Recording FPS": "Запись FPS", + "Recording FPS Change on Start": "Запись FPS -изменений в начале", + "Recording Flags": "Флаги записи", + "Recording Segment Interval": "Интервал сегмента записи в минутах", + "Recording Timeout": "Тайм-аут записи в минутах", + "Recording Timestamp": "Временная метка записи", + "Recording Watermark": "Водяной знак на записи", + "RecordingText": "Рекоменжуется установить Тип файла записи в WebMMP4 и Видео-кодек в libvpxcopy or libx264, потому что Тип ввода установлен в .", + "Refresh List of Cascades": "Обновить список каскадов", + "Region": "Область, край", + "Region Editor": "Редактор областей", + "Region Name": "Название области", + "RegionNote": "Точки сохраняются только при нажатии Сохранить в окне настройки монитора.", + "Regions": "Области", + "Registered": "Зарегистрированный", + "Registered Servers": "Зарегистрированные серверы", + "Remember Me": "Запомнить меня", + "Request": "Запрос", + "Require Object to be in Region": "Требовать объекта, чтобы быть в регионе", + "Reset": "Перезагрузить", + "Reset Form": "Сброс форма", + "Reset Timer": "Сброс таймера", + "Resolution": "Разрешение", + "Restart": "Начать сначала", + "Restart CRON": "Перезапустить Крона", + "Restart Core": "Перезапустить ядро", + "Restarting": "Перезапуск", + "Restarting Process": "Процесс перезагрузки", + "Retry Connection": "Количество попыток подключения допустимое количество неудачных попыток", + "Retrying...": "Повтор...", + "Right": "Вправо URL", + "Right Stop": "Остановить вправо URL", + "Rotate": "Повернуть", + "Rule": "Правило", + "Run Installer": "Запустить установщик", + "S3-Based Network Storage": "S3 Network Storage", + "SFTP": "SFTP", + "SFTP (SSH File Transfer)": "SFTP (передача файла SSH)", + "SFTP Error": "Ошибка SFTP", + "Saturday": "Суббота", + "Save": "Сохранить", + "Save Changes": "Сохранить изменения", + "Save Directory": "Каталог для сохранения", + "Save Events": "Сохраните события", + "Save Events to SQL": "Сохранять события в БД", + "Save Frames to Events": "Сохранить кадры на событиях", + "Save Links to Database": "Сохранить ссылки на базу данных", + "Save Log in SQL": "Сохранить журнал в БД Следите щза размером БД, т.к. она может быстро заполниться", + "Save New": "Сохранить новый", + "Save as": "Сохранить как", + "Saved": "Сохранил", + "Saved Filters": "Сохраненные фильтры", + "Saved Logs": "Сохраненные журналы", + "Saved Presets": "Сохраненные пресеты", + "Saved Schedules": "Сохраненные графики", + "Scan Settings": "Параметры сканирования", + "Schedule": "Расписание", + "Schedule Configuration Not Found": "Запланировать конфигурацию не найдена", + "Schedules": "Расписание", + "Search": "Поиск", + "Search Base": "Поиск базы", + "Search Filter": "Поиск фильтра", + "Search Images": "Поиск изображений", + "Search Settings": "Настройки поиска", + "Second stream in feed": "Второй поток в подаче", + "Secure": "Безопасный", + "Select a Monitor": "Выберите монитор", + "Select atleast one monitor to delete": "Выберите «По крайней мере один монитор», чтобы удалить.", + "Selected": "Выбранный", + "Send Frames": "Отправить кадры для анализа", + "Send Notification": "Отправить уведомление", + "Send to": "Отправить", + "Separate with commas, no spaces": "Отделить запятыми, без пробелов", + "September": "Сентябрь", + "Server URL": "Сервер URL", + "Session Key": "Сеанс ключ", + "Set Home": "Установите домой", + "Set Home Position (ONVIF-only)": "Установить домашнее положение (только OnVif)", + "Set Mode": "Установить режим", + "Set to Watch Only": "Установить только для просмотра", + "Settings": "Настройки", + "Settings Changed": "Настройки изменены", + "SettingsChangedText": "Ваши настройки были сохранены и применены.", + "Sharpness": "Четкость", + "Shinobi": "Шиноби", + "Shinobi Ordered to Update": "Обновление Shinobi завершено", + "Shinobi Streamer": "Трансляция Shinobi Streamer", + "ShinobiHub": "Shinobihub", + "Show Logs": "Показать журнал", + "Show Matrices": "Показать матрицы", + "Show Matrix": "Показать матрицу", + "Show Regions of Interest": "Показать интересующие регионы", + "Show Stream HUD": "Показать Stream HUD", + "Show Thumbnails in Video List": "Показать миниатюры в списке видео", + "Silent": "Молча", + "Simple": "Простой", + "Size (mb)": "Размер (Мб)", + "Skip Ping": "Пропуская", + "Snapshot": "Снимок", + "Snapshot Flags": "Флаги снимка", + "Snapshots": "Снимки", + "Sort By": "Сортировать по", + "Space Used": "Пространство используется", + "Start": "Начало", + "Start Recording": "Начало записи", + "Start Time": "Время начала", + "Start Time cannot be empty.": "Время начала не может быть пустым.", + "Started": "Начато", + "Started Building": "Начал строить", + "Starting": "Начинающий", + "State Configuration Not Found": "Конфигурация состояния не найдена", + "State Configuration has no monitors associated": "Конфигурация состояния не связана с мониторами", + "Status Changed": "Статус изменился", + "Status Indicator": "Индикатор состояния", + "Stop": "Останавливаться", + "Stop Command": "Остановить команду", + "Stop URL": "URL остановки", + "Stopped": "Остановился", + "Stopping": "Остановка", + "Storage Location": "Место хранения", + "Storage Use": "Использование хранилища", + "Stream": "Трансляция", + "Stream Channel": "Канал потока", + "Stream Flags": "Флаги трансляции", + "Stream Key": "Ключ потока", + "Stream Timestamp": "Временная метка трансляции", + "Stream Type": "Тип потока", + "Stream Watermark": "Водяной знак трансляции", + "Stream in Background": "Поток на фоне", + "Stream to YouTube": "Транслировать на YouTube", + "Stream to YouTube Flags": "Флаги трансляции на YouTube", + "StreamText": "

Этот раздел описывает основной метод вещания и его настройки. Трансляция будет отображена на приборной доске. Если вы выберете HLS, JPEG, или MJPEG, то вы сможете просматривать поток в других программах.

Использование JPEG-потока по сути выключит основную трансляцию и приведёт к использованию корзины снимков для получения кадров.

", + "Streamed Logs": "Потоковые журналы", + "Streamer": "Вещатель", + "Streams": "Потоки", + "Sub-Accounts": "Суб-аккаунт", + "Subdivision": "Подразделение", + "Substream": "Подстег", + "Substream Process": "Процесс подстежки", + "SubstreamNotConfigured": "Подстег не настроен. Откройте настройки монитора и настройте его.", + "Subtitle": "Подзаголовок", + "Success": "Успех", + "Sunday": "Воскресенье", + "Superuser": "Суперпользователь", + "Superuser Logs": "Журналы суперпользователя", + "Switch on for Still Image": "Переключиться на неподвижное изображение", + "System": "Система", + "System Level": "Системный уровень", + "TCP": "Протокол TCP", + "TV Channel": "Телевизионный канал", + "TV Channel Group": "Группа телеканалов", + "TV Channel ID": "Телеканал идентификатор канала", + "Telegram": "Телеграмма", + "Text Box Color": "Цвет текстового поля", + "Text Color": "Цвет текста", + "Text criteria unsupported for Object Count tests, Ignoring Conditional": "Текстовые критерии, не поддерживаемые для тестов подсчета объектов, игнорируя условные", + "Themes": "Темы", + "There are no monitors that you can view with this account.": "Нет мониторов, которые вы можете просмотреть с помощью этой учетной записи.", + "Threads": "Потоки", + "Thumbnail": "Миниатюра", + "Thursday": "Четверг", + "Time": "Время", + "Time Created": "Время создано", + "Time Left": "Время вышло", + "Time Occurred": "Время произошло", + "Time-lapse": "Интервальная съёмка", + "Time-lapse Tool": "Инструменты интервальной съёмки", + "TimeZone": "Часовой пояс", + "Timelapse": "Промежуток времени", + "Timelapse Frames Share": "Рамовые рамки с временем делится", + "Timelapse Watermark": "Timelapse Watermark", + "Timeout": "Таймаут", + "Timeout Reset on Next Event": "Тайм -аут сброс на следующем мероприятии", + "Timeout Reset on Next Motion": "Сброс таймаута при следующем движении", + "Timezone": "Часовой пояс", + "Timezone Offset": "Смещение часового пояса", + "Title": "Заголовок", + "Today": "Сегодня", + "Toggle Sidebar": "Переключение боковой панели", + "Toggle Substream": "Переключатель подстежки", + "Token": "Токен", + "Top Left": "Верхний Левый", + "Top Right": "Верхний Правый", + "Traditional (Watch-Only, Includes Buffer)": "Традиционный (только для часа, включает буфер)", + "Traditional Recording": "Традиционная запись", + "Traditional Recording Flags": "Традиционные флаги записи", + "Train": "Тренироваться", + "TrainConfirm": "Вы уверены, что хотите начать тренировку? Это может занять более 12 часов с более чем 500 изображениями. Это будет потреблять большое количество ресурсов, таких как RAM и/или CPU.", + "TrainConfirmStop": "Вы уверены, что хотите перестать тренироваться?", + "Trainer Engine": "Двигатель тренера", + "Trigger Blocked": "Триггер заблокирован", + "Trigger Camera Groups": "Запустить группы камер", + "Trigger Event": "Триггер событие", + "Trigger Group to Record": "Запустить группу для записи", + "Trigger Record": "Триггер записи", + "Trigger Successful": "Триггер успешен", + "Trigger Threshold": "Триггерный порог", + "Tuesday": "Вторник", + "Turn Speed": "Поверните скорость", + "Type": "Тип", + "UDP": "Протокол UDP", + "URL": "URL-адрес", + "URL Stop Timeout": "Таймаут URL остановки вызвать URL после X миллисекунд", + "US": "США", + "UTCDateTime": "Дата", + "Unable to Launch": "Не удалось запустить", + "UnabletoLaunchText": "Пожалуйста, сначала сохраните монитор а затем используйте редактор областей.", + "Uncommon Objects": "Необычные объекты", + "Uniform": "Униформа", + "Unlink": "Неопределенность", + "Unlink Login": "Unlink Login?", + "Unlinked": "Не связанный", + "Up": "Вверх URL", + "Up Stop": "Остановить вверх URL", + "Update": "Обновлять", + "Update to Development": "Обновление до разработки", + "Update to Master": "Обновление для мастера", + "Upload Bandwidth": "Загрузить полосу пропускания", + "Upload File": "Загрузить файл", + "Uploaded Only": "Загружен только", + "Uploaders": "Загрузчики", + "Use Built-In": "Используйте встроенный", + "Use Camera Timestamps": "Используйте временные метки камеры", + "Use Global Amazon S3 Video Storage": "Используйте Global Amazon S3 Video Storage", + "Use Global Backblaze B2 Video Storage": "Используйте Global Backblaze B2 Video", + "Use Global Wasabi Hot Cloud Storage Video Storage": "Используйте глобальное хранение видео с хранением горячих облаков Wasabi", + "Use Global WebDAV Video Storage": "Используйте Global Webdav Video Storage", + "Use HTML5 Play Method": "Используйте метод игры HTML5", + "Use Max Storage Amount": "Используйте максимальную сумму хранения", + "Use Raw Snapshot": "Используйте необработанный снимок", + "Use Substream": "Используйте подстрим", + "Use coProcessor": "Используйте копроцессор", + "UseCount": "Использование", + "User Log": "Пользовательский журнал", + "User Not Found": "Пользователь не найден", + "Username": "Логин", + "VA-API": "Va-api", + "Value": "Значение", + "Video": "Видео", + "Video Bit Rate": "Видеобиточная скорость", + "Video Codec": "Видео-кодек", + "Video Configuration": "Видео конфигурация", + "Video Filter": "Видео-фильтр", + "Video Finished": "Видео окончено", + "Video Length (minutes) and Motion Count per video": "Продолжительность видео (минут) и кол-во движений на видео", + "Video Limit": "Видеоминку", + "Video Record Rate": "Скорость видео (кадров в секунду)", + "Video Set": "Видео набор", + "Video Share": "Видео обмена", + "Video Status": "Статус видео", + "Video and Time Span (Minutes)": "Видео и отрезок времени (минут)", + "Video stream only from first feed": "Видеопоток только от первого канала", + "Video streams only": "Только видеопотоки", + "Videos": "Видео", + "Videos List": "Список видео", + "Videos Merge": "Видео слияние", + "Viewing Server Stats": "Просмотр статистики сервера", + "Warning": "Предупреждение", + "Wasabi Hot Cloud Storage": "Wasabi Hot Cloud Storage", + "Wasabi Hot Cloud Storage Upload Error": "Ошибка загрузки хранения Hot Cloud Wasabi", + "Watch": "Смотреть", + "Watch Only": "Только просмотр", + "Watch-Only": "Только для часа", + "Watching": "Наблюдая", + "Web Page": "Страница интернета", + "WebDAV": "Протокол WebDAV", + "WebM (libvpx)": "Webm (libvpx)", + "Webdav Error": "Ошибка WebDAV", + "WebdavErrorText": "Не удается сохранить. Попробуйте сначала создать каталог.", + "WebdavErrorTextCreatingDir": "Не может создать каталог.", + "WebdavErrorTextTryCreatingDir": "Не может спасти. Попытка создать каталог.", + "Webhook": "Вебхук", + "Webhook URL": "URL вебхука", + "Websocket": "WebSocket", + "Websocket Connected": "WebSocket подключен", + "Websocket Disconnected": "WebSocket отключен", + "Wednesday": "Среда", + "Welcome": "Добро пожаловать!", + "When Detector is Off": "Когда детектор отключен", + "When Detector is On": "Когда детектор включен", + "WhiteBalance": "Баланс белого", + "WideDynamicRange": "Широкий динамический диапазон", + "Width": "Ширина", + "X Point": "X точка", + "Y Point": "Y точка", + "Yes": "Да", + "Zip and Download": "Zip и скачать", + "Zipping Videos": "Застегивание видео", + "Zones": "Зоны", + "Zoom In": "Увеличение URL", + "Zoom In Stop": "Остановка увеличениеURL", + "Zoom Out": "Уменьшить URL", + "Zoom Out Stop": "Остановка уменьшения URL-Адрес", + "a day": "день", + "a few seconds": "несколько секунд", + "a minute": "минуту", + "a month": "месяц", + "a year": "год", + "aac": "ААС", + "aac (Default)": "ААС (по умолчанию)", + "ac3": "АС3", + "accountActionFailed": "Действие аккаунта не удалось", + "accountAdded": "Учетная запись добавлена", + "accountAddedText": "Учетная запись была добавлена.", + "accountDeleted": "Удалена", + "accountDeletedText": "Учетная запись была удалена.", + "accountId": "Идентификатор учетной записи", + "accountSettingsDescription": "Управляйте своим профилем и установите такие параметры, как Max Hore Summ и Max -количество дней для хранения видео.", + "accountSettingsError": "Ошибка настроек учетной записи", + "activatedText": "Ваша установка была активирована.", + "ago": "назад", + "airplane": "самолет", + "alreadyLinked": "Уже связан с учетной записью", + "an hour": "час", + "apple": "яблоко", + "applicationKey": "Ключ приложения", + "aws_accessKeyId": "Доступ к ключе", + "aws_secretAccessKey": "Секретный ключ доступа", + "backpack": "рюкзак", + "banana": "банан", + "baseball bat": "бейсбольная бита", + "baseball glove": "бейсбольная перчатка", + "bear": "нести", + "bed": "кровать", + "bench": "скамья", + "bicycle": "велосипед", + "bindDN": "binddn", + "bird": "птица", + "blankPassword": "Оставьте поле пустым, чтобы сохранить текущий пароль", + "boat": "лодка", + "book": "книга", + "bottle": "бутылка", + "bowl": "чаша", + "broccoli": "брокколи", + "bus": "автобус", + "cake": "торт", + "calendar": "календарь", + "car": "автомобиль", + "carrot": "морковь", + "cat": "Кот", + "cell phone": "сотовый телефон", + "chair": "стул", + "clientStreamFailedattemptingReconnect": "Проверка потока на стороне клиента провалилась, попытка переподключения", + "clock": "Часы", + "coProcess Crashed for Monitor": "Coprocess разбился для монитора", + "coProcess Unexpected Exit": "Coprocess неожиданный выход", + "coProcessor": "Коконсор", + "coProcessor Started": "Коконсор начал", + "coProcessor Stopped": "Коконсор остановился", + "coProcessorTextStarted": "Копроцессор начал только для результатов процессора.", + "coProcessorTextStopped": "Коконсор закончился.", + "codecMismatchText1": "Ваша камера предоставляет потоковые данные H.265 (HEVC), и вы используете копию в качестве видеокодека для раздела потока. Ваш поток от Shinobi может не отображаться на устройствах, которые не могут использовать этот кодек. Мобильное приложение Shinobi может просмотреть эти потоки.", + "codecMismatchText2": "Вы выбранный видеокодек не применим. Ваша камера предоставляет данные потока MJPEG, и вы используете копию в качестве видеокодека для раздела потока. Изменил тип потока на MJPEG.", + "codecMismatchText3": "Вы выбранный видеокодек не применим. Ваша камера предоставляет данные потока MJPEG, и вы используете копию в качестве видеокодека для раздела записи. Изменил видеокодек на Libx264.", + "confirmDeleteFilter": "Вы действительно хотите удалить этот фильтр? Это действие нельзя отменить.", + "contactAdmin": "Свяжитесь с сопровождающей установкой Shinobi.", + "copy": "копия", + "couch": "диван", + "cow": "корова", + "cuda": "cuda (nvidia nvenc)", + "cup": "кружка", + "cuvid": "cuvid (nvidia nvenc)", + "days": "дней", + "deleteApiKey": "Удалить ключ API", + "deleteApiKeyText": "Вы хотите удалить этот ключ API? Вы не можете восстановить это.", + "deleteMonitorStateText1": "Вы хотите удалить этот монитор состояний? Связанные конфигурации монитора не могут быть восстановлены.", + "deleteMonitorStateText2": "Вы хотите удалить предустановку этого монитора?", + "deleteScheduleText": "Вы хотите удалить это расписание? Связанные предустановки не будут изменены.", + "deleteSubAccount": "Удалить суб-аккунт", + "deleteSubAccountText": "Вы хотите удалить этот суб-аккунт? Вы не можете восстановить это.", + "dining table": "обеденный стол", + "dog": "собака", + "donut": "пончик", + "drm": "Обмен объектом DRM", + "dropBoxSuccess": "Успех! Файлы сохранены в Dropbox.", + "dxva2": "dxva2 (видео DirectX, Windows)", + "elephant": "слон", + "eventFilterActionText": "Это действия, которые происходят из условий фильтра, которые преуспели. «Оригинальный выбор» относится к опции, которую вы выбрали в настройках вашего монитора.", + "eventFilterErrorBrackets": "У вас есть не более чем количество кронштейнов. Их игнорируют.", + "eventFiltersDescription": "Настройка фильтров для того, когда происходят события.", + "failedLoginText1": "Вы не смогли войти слишком много раз. Вы должны подождать 15 минут, прежде чем попробовать снова.", + "failedLoginText2": "Пожалуйста, проверьте свои учетные данные для входа.", + "fieldMissingValueText1": "Ваша камера предоставляет данные потока MJPEG. Вам нужно установить скорость захвата монитора. Шиноби попытается обнаружить его и заполнить его автоматически.", + "fieldTextAccelerator": "Аппаратное ускорение (HWACCEL) для декодирования потоков.", + "fieldTextAcodec": "Аудиокодек для записи.", + "fieldTextActionsCommand": "Вы можете использовать это, чтобы запустить сценарий по команде.", + "fieldTextActionsHalt": "Заставьте мероприятие ничего не делать, как будто этого никогда не было.", + "fieldTextActionsIndifference": "Измените минимальное безразличие, необходимое для события.", + "fieldTextActionsRecord": "Используйте традиционную запись, HotSwap или удалить неподвижно с помощью их в настоящее время установленных параметров в разделе «Настройки глобальных обнаружений».", + "fieldTextAduration": "Укажите, сколько микросекунд анализируется, чтобы исследовать вход. Установите 100000, если вы используете RTSP и имеете проблемы с потоком.", + "fieldTextAudioAlert": "Звук, когда происходит событие.", + "fieldTextAudioDelay": "Задержка до следующего раза, когда событие может начать предупреждение. Измеряется в секундах.", + "fieldTextAudioNote": "Звук, когда появляется информационный пузырь.", + "fieldTextAutoHost": "Полный URL.", + "fieldTextAutoHostEnable": "Поправьте отдельные части, необходимые для создания URL -адреса потока, или обеспечить полный URL и позволить Shinobi проанализировать его для вас.", + "fieldTextChannelHlsListSize": "Количество максимальных сегментов перед автоматическим удалением старых сегментов.", + "fieldTextChannelHlsTime": "Как долго должен быть каждый видео -сегмент, за считанные минуты. Каждый сегмент будет нарисован клиентом через файл M3U8. Более короткие сегменты занимают меньше места.", + "fieldTextChannelPresetStream": "Предустающий флаг для определенных видеокодеров. Если вы обнаружите, что ваша камера сбивается каждые несколько секунд: попробуйте оставить ее пустой.", + "fieldTextChannelStreamAcodec": "Аудиокодек для потоковой передачи.", + "fieldTextChannelStreamAcodecAac": "Используется для видео MP4.", + "fieldTextChannelStreamAcodecAc3": "Используется для видео MP4.", + "fieldTextChannelStreamAcodecAuto": "Пусть ffmpeg выберет.", + "fieldTextChannelStreamAcodecCopy": "Используется для видео MP4. Имеет очень низкое использование процессора, но некоторые аудиокодеки нуждаются в пользовательских флагах, таких как -strict 2 для AAC.", + "fieldTextChannelStreamAcodecLibmp3lame": "Используется для видео MP4.", + "fieldTextChannelStreamAcodecLibopus": "Используется для видео WebM.", + "fieldTextChannelStreamAcodecLibvorbis": "Используется для видео WebM.", + "fieldTextChannelStreamAcodecNoAudio": "Нет звука, это вариант, который должен быть установлен в некоторых частях света по юридическим причинам.", + "fieldTextChannelStreamFps": "Скорость, с которой кадры отображаются для клиентов, в кадрах в секунду. Имейте в виду, что нет дефолта. Это может привести к использованию высокой пропускной способности.", + "fieldTextChannelStreamQuality": "Низкое число означает более высокое качество. Более высокое число означает меньшее качество.", + "fieldTextChannelStreamRotate": "Измените угол просмотра видеопотока.", + "fieldTextChannelStreamScaleX": "Ширина изображения потока, которое выводит после обработки.", + "fieldTextChannelStreamScaleY": "Высота изображения потока, которое выводит после обработки.", + "fieldTextChannelStreamType": "Метод, который будет использовать для потребления видеопотока.", + "fieldTextChannelStreamTypeFLV": "Отправка FLV -кодированных кадров за WebSocket.", + "fieldTextChannelStreamTypeHLS(includesAudio)": "Аналогичный метод Facebook Live Streams. включает в себя Audio , если вход предоставляет его. Существует задержка около 4-6 секунд, потому что этот метод записывает сегменты, а затем подталкивает их к клиенту, а не толкает, пока он их создает.", + "fieldTextChannelStreamTypeMJPEG": "Стандартное движение JPEG Изображение. Нет аудио.", + "fieldTextChannelStreamTypePoseidon": "Посейдон построен на коде обработки MP4 Кевина Годелла. Он имитирует потоковой файл MP4, но используя данные живого потока. Включает аудио. Некоторые браузеры могут воспроизводить его как обычный файл MP4. Поток через HTTP или WebSocket.", + "fieldTextChannelStreamVcodec": "Видеодек для потоковой передачи.", + "fieldTextChannelStreamVcodecAuto": "Пусть ffmpeg выберет.", + "fieldTextChannelStreamVcodecCopy": "Используется для видео MP4. Имеет очень низкое использование процессора, но не может использовать видеофильтры, и файлы, может быть гигантским. Лучше всего настроить настройки MP4 на стороне камеры при использовании этой опции.", + "fieldTextChannelStreamVcodecLibx264": "Используется для видео MP4.", + "fieldTextChannelStreamVcodecLibx265": "Используется для видео MP4.", + "fieldTextChannelSvf": "Поместите видеофильтры FFMPEG в этом поле, чтобы повлиять на потоковую часть. Нет мест.", + "fieldTextControlInvertY": "Когда ваша камера установлена вверх ногами или использует инвертированные вертикальные элементы управления.", + "fieldTextCrf": "Низкое число означает более высокое качество. Более высокое число означает меньшее качество.", + "fieldTextCustDetect": "Пользовательские флаги, которые связываются с потоковым детектором, используют для анализа.", + "fieldTextCustDetectObject": "Пользовательские флаги, которые связываются с потоковым детектором, используют для анализа.", + "fieldTextCustInput": "Пользовательские флаги, которые связываются с вводом процесса FFMPEG.", + "fieldTextCustRecord": "Пользовательские флаги, которые связываются с записи процесса FFMPEG.", + "fieldTextCustSipRecord": "Пользовательские флаги, которые связываются с выводом, от которых Siphon на основе событий.", + "fieldTextCustSnap": "Пользовательские флаги, которые связываются с снимками.", + "fieldTextCustStream": "Пользовательские флаги, которые связываются с потоком (вид клиента) процесса FFMPEG.", + "fieldTextCustomOutput": "Добавьте пользовательский выход, такой как кадры JPEG, или отправьте данные прямо на другой сервер.", + "fieldTextCutoff": "В считанные минуты. Когда отрезать и запустить новый видеофайл.", + "fieldTextDays": "Количество дней для хранения видео перед очисткой.", + "fieldTextDetailSubstreamInputRtspTransportAuto": "Пусть FFMPEG решит. Обычно он сначала попробует UDP.", + "fieldTextDetailSubstreamInputRtspTransportTCP": "Установите это, если UDP начнет давать нежелательные результаты.", + "fieldTextDetailSubstreamInputRtspTransportUDP": "FFMPEG пытается это сначала.", + "fieldTextDetailSubstreamOutputHlsListSize": "Количество максимальных сегментов перед автоматическим удалением старых сегментов.", + "fieldTextDetailSubstreamOutputHlsTime": "Как долго должен быть каждый видео -сегмент, за считанные минуты. Каждый сегмент будет нарисован клиентом через файл M3U8. Более короткие сегменты занимают меньше места.", + "fieldTextDetailSubstreamOutputPresetStream": "Предустающий флаг для определенных видеокодеров. Если вы обнаружите, что ваша камера сбивается каждые несколько секунд: попробуйте оставить ее пустой.", + "fieldTextDetailSubstreamOutputStreamAcodec": "Аудиокодек для потоковой передачи.", + "fieldTextDetailSubstreamOutputStreamAcodecAac": "Используется для видео MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecAc3": "Используется для видео MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecAuto": "Пусть ffmpeg выберет.", + "fieldTextDetailSubstreamOutputStreamAcodecCopy": "Используется для видео MP4. Имеет очень низкое использование процессора, но некоторые аудиокодеки нуждаются в пользовательских флагах, таких как -strict 2 для AAC.", + "fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame": "Используется для видео MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecLibopus": "Используется для видео WebM.", + "fieldTextDetailSubstreamOutputStreamAcodecLibvorbis": "Используется для видео WebM.", + "fieldTextDetailSubstreamOutputStreamAcodecNoAudio": "Нет звука, это вариант, который должен быть установлен в некоторых частях света по юридическим причинам.", + "fieldTextDetailSubstreamOutputStreamFps": "Скорость, с которой кадры отображаются для клиентов, в кадрах в секунду. Имейте в виду, что нет дефолта. Это может привести к использованию высокой пропускной способности.", + "fieldTextDetailSubstreamOutputStreamQuality": "Низкое число означает более высокое качество. Более высокое число означает меньшее качество.", + "fieldTextDetailSubstreamOutputStreamRotate": "Измените угол просмотра видеопотока.", + "fieldTextDetailSubstreamOutputStreamScaleX": "Ширина изображения потока, которое выводит после обработки.", + "fieldTextDetailSubstreamOutputStreamScaleY": "Высота изображения потока, которое выводит после обработки.", + "fieldTextDetailSubstreamOutputStreamType": "Метод, который будет использовать для потребления видеопотока.", + "fieldTextDetailSubstreamOutputStreamVcodec": "Видеодек для потоковой передачи.", + "fieldTextDetailSubstreamOutputStreamVcodecAuto": "Пусть ffmpeg выберет.", + "fieldTextDetailSubstreamOutputStreamVcodecCopy": "Используется для видео MP4. Имеет очень низкое использование процессора, но не может использовать видеофильтры, и файлы, может быть гигантским. Лучше всего настроить настройки MP4 на стороне камеры при использовании этой опции.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx264": "Используется для видео MP4.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx265": "Используется для видео MP4.", + "fieldTextDetailSubstreamOutputSvf": "Поместите видеофильтры FFMPEG в этом поле, чтобы повлиять на потоковую часть. Нет мест.", + "fieldTextDetector": "Это добавит еще один вывод в команде FFMPEG для детектора движения.", + "fieldTextDetectorAudio": "Проверьте, произошло ли звук в Decible Certiain. Декабкое чтение может быть не точным для реального измерения.", + "fieldTextDetectorBufferHlsListSize": "Количество максимальных сегментов перед автоматическим удалением старых сегментов.", + "fieldTextDetectorBufferHlsTime": "Как долго должен быть каждый видео -сегмент, в считанные секунды. Каждый сегмент будет нарисован клиентом через файл M3U8. Более короткие сегменты занимают меньше места.", + "fieldTextDetectorColorThreshold": "Количество различий, допускаемое в пикселе до того, как оно будет считаться движением.", + "fieldTextDetectorCommand": "Команда, которая будет работать. Это эквивалент запуска команды оболочки из терминала.", + "fieldTextDetectorCommandTimeout": "Это значение является таймером, позволяющим следующему запуску вашего сценария. Это значение в минутах.", + "fieldTextDetectorFps": "Сколько кадров в секунду отправит детектору движения; 2 - дефолт.", + "fieldTextDetectorFrame": "Это будет читать весь кадр для различий в пикселях. Это то же самое, что создание области, которая охватывает весь экран.", + "fieldTextDetectorHttpApi": "Вы хотите разрешить HTTP -триггеры на эту камеру?", + "fieldTextDetectorLisencePlate": "Включить распознавание номерного знака. Плагин OpenALPR имеет это всегда включено.", + "fieldTextDetectorLisencePlateCountry": "Выберите тип пластин для распознавания. Только мы и ЕС поддерживаются в это время.", + "fieldTextDetectorLockTimeout": "Блокировка, когда разрешен следующий триггер, чтобы избежать перегрузки базы данных и приема клиентов. Измеряется в миллисекундах.", + "fieldTextDetectorMaxSensitivity": "Рейтинг достоверности движения должен быть ниже, чем это значение, которое следует рассматривать как триггер. Оставьте пустым без максимума. Этот вариант был ранее назван «максимальным безразличием».", + "fieldTextDetectorNoiseFilter": "Попытка отфильтровать зерно или повторное движение при определенном безразличие.", + "fieldTextDetectorNoiseFilterRange": "Количество различий, допускаемое в пикселе до того, как оно будет считаться движением.", + "fieldTextDetectorNotrigger": "Проверьте, произошло ли движение в интервале. Если произошло движение, чек будет сброшен.", + "fieldTextDetectorNotriggerCommand": "Команда, которая будет работать. Это эквивалент запуска команды оболочки из терминала.", + "fieldTextDetectorNotriggerCommandTimeout": "Это значение является таймером, позволяющим следующему запуску вашего сценария. Это значение в минутах.", + "fieldTextDetectorNotriggerDiscord": "Если движение не было обнаружено после периода времени ожидания, вы получите уведомление о Discord.", + "fieldTextDetectorNotriggerTimeout": "Тайм -аут рассчитывается за считанные минуты.", + "fieldTextDetectorNotriggerWebhook": "Отправьте запрос на получение URL -адреса с некоторыми значениями с мероприятия.", + "fieldTextDetectorObjCount": "Граф обнаруженных объектов.", + "fieldTextDetectorObjCountInRegion": "Считайте объекты только внутри регионов.", + "fieldTextDetectorPam": "Используйте детектор движения Кевина Годелла. Это встроено в Shinobi и не требует другой конфигурации для активации.", + "fieldTextDetectorPtzFollow": "Следуйте по самым большим обнаруженным объектам с PTZ? Требуется детектор объекта или матрицы, обеспечиваемые событиями.", + "fieldTextDetectorRecordMethod": "Есть несколько способов начать запись, когда происходит событие, например, движение. Традиционная запись является самой удобной.", + "fieldTextDetectorSave": "Сохранить события движения в SQL. Это позволит отображать движение над видео во время событий движения времени, произошедших в зрителе Power.", + "fieldTextDetectorScaleX": "Ширина обнаруженного изображения. Меньшие размеры принимают меньше процессора.", + "fieldTextDetectorScaleY": "Высота обнаруженного изображения. Меньшие размеры принимают меньше процессора.", + "fieldTextDetectorSendFrames": "Протолкните рамки к подключенному плагину, который будет проанализирован.", + "fieldTextDetectorSendFramesObject": "Протолкните рамки к подключенному плагину, который будет проанализирован.", + "fieldTextDetectorSendVideoLength": "В секундах. Продолжительность видео, которое отправляется в вашу службу уведомлений, например, по электронной почте или раздор.", + "fieldTextDetectorSensitivity": "Рейтинг достоверности движения должен превышать это значение, чтобы рассматриваться как триггер. Это число коррелирует непосредственно с рейтингом доверия, возвращенным детектором движения. Этот вариант был ранее назван «безразличием».", + "fieldTextDetectorThreshold": "Минимальное количество обнаружений, чтобы запустить событие движения. Обнаружения должны быть внутри детектора. Порог, разделенный на детектор FPS секунд. Например, если FPS детектора составляет 2, а порог триггера - 3, то в течение 1,5 секунды должны произойти три обнаружения, чтобы запустить событие движения. Этот порог - это область обнаружения.", + "fieldTextDetectorTimeout": "Продолжительность «Запись запуска» будет работать. Это читается за считанные минуты.", + "fieldTextDetectorTrigger": "Это закажет камеру для записи, если она установлена на «только для наблюдения», когда обнаруживается событие.", + "fieldTextDetectorUseDetectObject": "Создайте кадры для отправки в любой подключенный плагин.", + "fieldTextDetectorWebhook": "Отправьте запрос на получение URL -адреса с некоторыми значениями с мероприятия.", + "fieldTextDetectorWebhookTimeout": "Это значение является таймером, позволяющим следующему запуску вашего веб -крючка. Это значение в минутах.", + "fieldTextDir": "Расположение там, где будут сохранены записанные файлы. Вы можете настроить больше местоположений с переменной addStorage .", + "fieldTextEventDays": "Количество дней, чтобы сохранить события перед очисткой.", + "fieldTextEventMonPop": "Когда событие происходит всплывающе, поток монитора.", + "fieldTextEventRecordScaleX": "Ширина изображения записи на основе событий, которое выводит после обработки.", + "fieldTextEventRecordScaleY": "Высота изображения записи на основе событий, которое выводится после обработки.", + "fieldTextExt": "Тип файла для вашего записанного видеофайла.", + "fieldTextExtMP4": "Этот тип файла воспроизводится почти все современные веб -браузеры, которые включают мобильные. Размер файлов, как правило, больше, если вы не снижаете качество.", + "fieldTextExtWebM": "Маленький размер файлов, низкая совместимость с клиентами. Хорошо для загрузки на сайты, такие как YouTube.", + "fieldTextFactorAuth": "Включите вторичное требование для входа через один из включенных методов.", + "fieldTextFatalMax": "Количество раз повторно повторно для сетевого соединения между сервером и камерой перед установкой монитора для отключения. Нет десятичных десятиков. Установите на 0, чтобы повторно повторить навсегда.", + "fieldTextFps": "Скорость, в которой кадры записываются в файлы, кадры в секунду. Имейте в виду, что нет дефолта. Это может привести к большим файлам. Лучше всего установить эту камеру.", + "fieldTextHeight": "Высота изображения потока.", + "fieldTextHlsListSize": "Количество максимальных сегментов перед автоматическим удалением старых сегментов.", + "fieldTextHlsTime": "Как долго должен быть каждый видео -сегмент, за считанные минуты. Каждый сегмент будет нарисован клиентом через файл M3U8. Более короткие сегменты занимают меньше места.", + "fieldTextHost": "Адрес соединения", + "fieldTextHwaccel": "Декодирование двигателя", + "fieldTextHwaccelVcodec": "Декодирование двигателя", + "fieldTextInverseTrigger": "Запустить внешние указанные регионы. Не будет запускать с включенным полным кадром.", + "fieldTextIp": "Диапазон или холост", + "fieldTextIrCutFilterAuto": "IR Cut Filter автоматически активируется устройством.", + "fieldTextIrCutFilterOff": "Отключить IR Cut Fiter. Обычно ночной режим.", + "fieldTextIrCutFilterOn": "Включить IR Cut Fiter. Обычно дневной режим.", + "fieldTextIsOnvif": "Это камера, соответствующая Onvif?", + "fieldTextLang": "Основной язык текстовых элементов. Для получения полного перевода добавить свой язык в conf.json, например: \"Язык\": \"en_ca\", ", + "fieldTextLogDays": "Количество дней, чтобы сохранить журналы перед очисткой.", + "fieldTextLoglevel": "Объем данных для предоставления при выполнении работы.", + "fieldTextLoglevelAllWarnings": "Показать все предупреждения. Используйте это, если вы не можете узнать, что не так с вашей камерой.", + "fieldTextLoglevelFatal": "Отображать только смертельные ошибки.", + "fieldTextLoglevelOnError": "Отображать все важные ошибки. Примечание: это не всегда показывает важную информацию.", + "fieldTextLoglevelSilent": "Никто. Это заставит замолчать все регистрации.", + "fieldTextMail": "Вход для учетных записей. Адрес электронной почты основного владельца учетной записи получит уведомления.", + "fieldTextMapRtspTransportAuto": "Пусть FFMPEG решит. Обычно он сначала попробует UDP.", + "fieldTextMapRtspTransportTCP": "Установите это, если UDP начнет давать нежелательные результаты.", + "fieldTextMapRtspTransportUDP": "FFMPEG пытается это сначала.", + "fieldTextMaxKeepDays": "Количество дней для хранения видео до очистки этого монитора.", + "fieldTextMid": "Это непасный идентификатор для монитора. Вы можете дублировать монитор, дважды щелкнув идентификатор монитора и изменив его.", + "fieldTextMode": "Это основная задача монитора.", + "fieldTextModeDisabled": "Неактивный монитор, в этом режиме не будет создан процесс.", + "fieldTextModeRecord": "Непрерывная запись. Сегменты производятся каждые 15 минут по умолчанию.", + "fieldTextModeWatchOnly": "Монитор будет транслироваться только, никакой записи не будет происходить, если не будет упорядочено API или детектор.", + "fieldTextMpass": "Пароль для вашей камеры", + "fieldTextMuser": "Вход пользователя для вашей камеры", + "fieldTextName": "Это имя, читаемое на человеке для монитора.", + "fieldTextNotes": "Комментарии, которые вы хотите оставить для этой камеры.", + "fieldTextOnvifNonStandard": "Это нестандартная камера Onvif?", + "fieldTextOnvifPort": "ONVIF обычно запускается на порте 8000 . Это может быть 80 , а также в зависимости от модели вашей камеры.", + "fieldTextPass": "Оставьте пустой, чтобы сохранить тот же пароль во время изменения настроек.", + "fieldTextPasswordAgain": "Необходимо сопоставить поле пароля, если вы хотите изменить его.", + "fieldTextPath": "Путь к вашей камере", + "fieldTextPort": "Разделять запятыми или диапазоном", + "fieldTextPortForce": "Использование веб -порта по умолчанию может разрешить автоматический переключатель на другие порты для потоков, таких как RTSP.", + "fieldTextPresetRecord": "Предустающий флаг для определенных видеокодеров. Если вы обнаружите, что ваша камера сбивается каждые несколько секунд: попробуйте оставить ее пустой.", + "fieldTextPresetStream": "Предустающий флаг для определенных видеокодеров. Если вы обнаружите, что ваша камера сбивается каждые несколько секунд: попробуйте оставить ее пустой.", + "fieldTextProbesize": "Укажите, насколько велик, чтобы сделать анализ датчика для ввода. Установите 100000, если вы используете RTSP и имеете проблемы с потоком.", + "fieldTextProtocol": "Протокол, который будет использовать для потребления видеопотока.", + "fieldTextRecordScaleX": "Ширина изображения потока.", + "fieldTextRecordScaleY": "Высота изображения потока.", + "fieldTextRecordTimelapse": "Создайте временную шлюпу на основе JPEG.", + "fieldTextRecordTimelapseMp4": "Создайте файл MP4 в конце каждого дня для TimeLapse.", + "fieldTextRecordTimelapseWatermark": "Изображение, которое сжигается на кадрах записанного видео.", + "fieldTextRecordTimelapseWatermarkLocation": "Расположение изображения, которое будет использоваться в качестве водяного знака.", + "fieldTextRecordTimelapseWatermarkPosition": "Изображение, которое сжигается на кадрах записанного видео.", + "fieldTextRotate": "Измените угол записи видеопотока.", + "fieldTextRtmpKey": "Клавиша потока для входящих потоков на порту RTMP.", + "fieldTextRtspTransport": "Транспортный протокол, которую будет использовать ваша камера. TCP, как правило, лучший выбор.", + "fieldTextRtspTransportAuto": "Пусть FFMPEG решит. Обычно он сначала попробует UDP.", + "fieldTextRtspTransportHTTP": "Стандартный метод соединения.", + "fieldTextRtspTransportTCP": "Установите это, если UDP начнет давать нежелательные результаты.", + "fieldTextRtspTransportUDP": "FFMPEG пытается это сначала.", + "fieldTextSfps": "Укажите частоту кадров (FPS), в которой камера предоставляет свой поток.", + "fieldTextSignalCheck": "Как часто ваш клиент проверяет поток, чтобы увидеть, жив ли он. Это рассчитывается за считанные минуты.", + "fieldTextSignalCheckLog": "Это только для клиента. Он будет отображаться в потоке журнала, когда будут проходить проверки сигнала клиента.", + "fieldTextSize": "Количество дискового пространства, шиноби, позволит потреблять до очистки. Это значение читается в мегабайтах.", + "fieldTextSizeFilebinPercent": "Процент максимальной суммы хранилища, которую архив файлов может использовать.", + "fieldTextSizeTimelapsePercent": "Процент максимальной суммы хранения. Крамы с временем могут записаться.", + "fieldTextSizeVideoPercent": "Процент максимальной суммы хранения, к которым видео могут записать.", + "fieldTextSkipPing": "Выберите, требуется ли успешный пинг до начала процесса монитора.", + "fieldTextSnap": "Получите последний кадр в JPEG.", + "fieldTextSnapSecondsInward": "в секундах", + "fieldTextSqllog": "Используйте это с осторожностью, так как FFMPEG любит собирать излишние данные по временам, что может привести к множеству строк баз данных.", + "fieldTextSqllogNo": "Нет, это дефолт.", + "fieldTextSqllogYes": "Сделайте это, если у вас есть только повторяющиеся проблемы.", + "fieldTextStreamAcodec": "Аудиокодек для потоковой передачи.", + "fieldTextStreamAcodecAac": "Используется для видео MP4.", + "fieldTextStreamAcodecAc3": "Используется для видео MP4.", + "fieldTextStreamAcodecAuto": "Пусть ffmpeg выберет.", + "fieldTextStreamAcodecCopy": "Используется для видео MP4. Имеет очень низкое использование процессора, но некоторые аудиокодеки нуждаются в пользовательских флагах, таких как -strict 2 для AAC.", + "fieldTextStreamAcodecLibmp3lame": "Используется для видео MP4.", + "fieldTextStreamAcodecLibopus": "Используется для видео WebM.", + "fieldTextStreamAcodecLibvorbis": "Используется для видео WebM.", + "fieldTextStreamAcodecNoAudio": "Нет звука, это вариант, который должен быть установлен в некоторых частях света по юридическим причинам.", + "fieldTextStreamFlvType": "Это только для приборной панели Shinobi. Оба потоковых метода все еще активны и готовы к использованию.", + "fieldTextStreamFps": "Скорость, с которой кадры отображаются для клиентов, в кадрах в секунду. Имейте в виду, что нет дефолта. Это может привести к использованию высокой пропускной способности.", + "fieldTextStreamLoop": "Целью статического файла, чтобы поток файлов ведет себя как прямой поток.", + "fieldTextStreamQuality": "Низкое число означает более высокое качество. Более высокое число означает меньшее качество.", + "fieldTextStreamRotate": "Измените угол просмотра видеопотока.", + "fieldTextStreamScaleX": "Ширина изображения потока, которое выводит после обработки.", + "fieldTextStreamScaleY": "Высота изображения потока, которое выводит после обработки.", + "fieldTextStreamTimestamp": "Часы, которые сжигаются на кадрах видеопотока.", + "fieldTextStreamTimestampBoxColor": "Timstamp Facdrop Color.", + "fieldTextStreamTimestampColor": "Timstamp Text Color.", + "fieldTextStreamTimestampFont": "Файл шрифта, чтобы укрепить свою временную метку.", + "fieldTextStreamTimestampFontSize": "Размер шрифта в Pt.", + "fieldTextStreamTimestampX": "Горизтональное положение временной метки", + "fieldTextStreamTimestampY": "Вертикальное положение временной метки", + "fieldTextStreamType": "Метод, который будет использовать для потребления видеопотока.", + "fieldTextStreamTypeBase64OverWebsocket": "Отправка кодированных рам BASE64 через WebSocket. Это избегает кэширования, но нет звука.", + "fieldTextStreamTypeFLV": "Отправка FLV -кодированных кадров за WebSocket.", + "fieldTextStreamTypeHLS(includesAudio)": "Аналогичный метод Facebook Live Streams. включает в себя Audio , если вход предоставляет его. Существует задержка около 4-6 секунд, потому что этот метод записывает сегменты, а затем подталкивает их к клиенту, а не толкает, пока он их создает.", + "fieldTextStreamTypeMJPEG": "Стандартное движение JPEG Изображение. Нет аудио.", + "fieldTextStreamTypePoseidon": "Посейдон построен на коде обработки MP4 Кевина Годелла. Он имитирует потоковой файл MP4, но используя данные живого потока. Включает аудио. Некоторые браузеры могут воспроизводить его как обычный файл MP4. Поток через HTTP или WebSocket.", + "fieldTextStreamVcodec": "Видеодек для потоковой передачи.", + "fieldTextStreamVcodecAuto": "Пусть ffmpeg выберет.", + "fieldTextStreamVcodecCopy": "Используется для видео MP4. Имеет очень низкое использование процессора, но не может использовать видеофильтры, и файлы, может быть гигантским. Лучше всего настроить настройки MP4 на стороне камеры при использовании этой опции.", + "fieldTextStreamVcodecLibx264": "Используется для видео MP4.", + "fieldTextStreamVcodecLibx265": "Используется для видео MP4.", + "fieldTextStreamVf": "Поместите видеофильтры FFMPEG в этом поле, чтобы повлиять на потоковую часть. Нет мест.", + "fieldTextStreamWatermark": "Изображение, которое сжигается на кадрах видеопотока.", + "fieldTextStreamWatermarkLocation": "Расположение изображения, которое будет использоваться в качестве водяного знака.", + "fieldTextStreamWatermarkPosition": "Изображение, которое сжигается на кадрах видеопотока.", + "fieldTextTimestamp": "Часы, которые сжигаются на кадрах записанного видео.", + "fieldTextTimestampBoxColor": "Timstamp Facdrop Color.", + "fieldTextTimestampColor": "Timstamp Text Color.", + "fieldTextTimestampFont": "Файл шрифта, чтобы укрепить свою временную метку.", + "fieldTextTimestampFontSize": "Размер шрифта в Pt.", + "fieldTextTimestampX": "Горизтональное положение временной метки", + "fieldTextTimestampY": "Вертикальное положение временной метки", + "fieldTextTvChannel": "Этот монитор будет иметь включенные функции телевизионного канала. Вы сможете просмотреть его в списке телеканалов.", + "fieldTextTvChannelGroupTitle": "Пользовательская группа для канала.", + "fieldTextTvChannelId": "Пользовательский идентификатор для канала.", + "fieldTextType": "Метод, который будет использовать для потребления видеопотока.", + "fieldTextTypeDashcam(StreamerV2)": "WebSocket Webm на основе P2P Stream.", + "fieldTextTypeH.264/H.265/H.265+": "Чтение высококачественной видео -стрелки, которая иногда включает в себя аудио.", + "fieldTextTypeHLS(.m3u8)": "Чтение высококачественной видео -стрелки, которая иногда включает в себя аудио.", + "fieldTextTypeJPEG": "Чтение снимков из URL и создание потока и/или видео от них.", + "fieldTextTypeLocal": "Чтение карт захвата, веб -камеры или интегрированные камеры.", + "fieldTextTypeMJPEG": "Подобно JPEG, за исключением того, что обработка кадров выполняется FFMPEG, а не Shinobi.", + "fieldTextTypeMPEG4(.mp4/.ts)": "Статический файл. Читать с более низкой скоростью и не следует использовать для реального живого потока.", + "fieldTextTypeMxPEG": "MOBOTIX MJPEG Stream", + "fieldTextTypeRTMP": "Научитесь подключаться здесь: статья: Как толкать потоки через RTMP к Shinobi ", + "fieldTextTypeShinobiStreamer": "WebSocket JPEG на основе P2P Stream.", + "fieldTextVcodec": "Видеодек для записи.", + "fieldTextVf": "Поместите видеофильтры FFMPEG в этом поле, чтобы повлиять на часть записи. Нет мест.", + "fieldTextWallClockTimestampIgnore": "Основыте все входящие данные камеры во время камеры вместо времени на сервере.", + "fieldTextWatchdogReset": "Если в записи триггера есть перекрытие.", + "fieldTextWatermark": "Изображение, которое сжигается на кадрах записанного видео.", + "fieldTextWatermarkLocation": "Расположение изображения, которое будет использоваться в качестве водяного знака.", + "fieldTextWatermarkPosition": "Изображение, которое сжигается на кадрах записанного видео.", + "fieldTextWidth": "Ширина изображения потока.", + "fire hydrant": "пожарный гидрант", + "flv": "FLV", + "for Global Access": "для глобального доступа", + "fork": "вилка", + "frisbee": "Фрисби", + "getAMonitor": "Получите монитор", + "getATvChannel": "Получить телевизионные каналы для монитора", + "getATvChannelText": "Получите доступные потоки H.264 в одиночном мониторе в списке воспроизведения .m3u8.", + "getAllMonitors": "Получите все мониторы", + "getAllTvChannels": "Получите все телеканалы", + "getAllTvChannelsText": "Получите все доступные потоки H.264 в плейлисте .m3u8. Включите опцию TV Channel в настройках вашего монитора, чтобы увидеть их потоки в этом списке.", + "getUserInfo": "Получите информацию пользователя", + "getVideos": "Получите видео", + "getVideosForMonitor": "Получите видео для монитора", + "giraffe": "жирафа", + "h264_cuvid": "H.264 Cuvid", + "h264_mmal": "H.264 (Raspberry Pi)", + "h264_nvenc": "H.264 NVENC (NVIDIA HW ACCEL)", + "h264_omx": "H.264 OpenMax (Raspberry Pi)", + "h264_qsv": "H.264 (Quick Sync Video)", + "h264_vaapi": "H.264 VA-API (Intel HW Accel)", + "h265BrowserText1": "Если вы пытаетесь воспроизводить файл H.265, вам может потребоваться загрузить его и открыть его в другом приложении, таком как VLC.", + "hair drier": "фен", + "handbag": "сумочка", + "hevc_cuvid": "H.265 Cuvid", + "hevc_nvenc": "H.265 NVENC (NVIDIA HW ACCEL)", + "hevc_qsv": "H.265 (Quick Sync Video)", + "hevc_vaapi": "H.265 VA-API (Intel HW Accel)", + "hlsOptions": "Варианты HLS", + "hlsOptionsInvalid": "Варианты HLS недействительны", + "horse": "лошадь", + "hot dog": "хотдог", + "hour": "час", + "hours": "часов", + "hwaccel": "Двигатель ускорения", + "hwaccel_device": "Устройство Hwaccel", + "hwaccel_vcodec": "Видео декодер", + "in": "в", + "in Days": "в дни", + "in seconds": "в секундах", + "keyId": "Идентификатор ключа", + "keyboard": "клавиатура", + "kite": "воздушный змей", + "knife": "нож", + "laptop": "ноутбук", + "lastLogin": "Последний Войти", + "libmp3lame": "libmp3lame", + "libopus": "Либоп", + "libvorbis (Default)": "libvorbis (по умолчанию)", + "libvpx (Default)": "libvpx (по умолчанию)", + "libvpx-vp9": "libvpx-vp9", + "libx264": "libx264", + "libx264 (Default)": "libx264 (по умолчанию)", + "libx265": "libx265", + "liveGridDescription": "Live Grid - это дисплей с несколькими потоками для Shinobi. Этот метод просмотра в основном предназначен для настольных компьютеров.", + "loginHandleUnbound": "Логин был не связан с этой учетной записи.", + "microwave": "Микроволновая печь", + "migrateText1": " входной тип не может быть проанализирован. Пожалуйста, установите вручную.", + "minute": "минута", + "minutes": "минут", + "mjpeg_cuvid": "MJPEG CUVID", + "modifyVideoText1": "Метод не существует. Убкдитесь, что последнее значение URL не пустое.", + "monSavedButNotCopied": "Ваш монитор был сохранен, но не скопирован на любой другой монитор.", + "monitorConfigFinderDescription": "Этот инструмент поможет вам найти конфигурации для камер, опубликованных сообществом. Все размещены на shinobihub . Вы тоже можете опубликовать свой, это действительно поможет сообществу :)", + "monitorEditFailedMaxReached": "Достигнт предел создаваемых камер для вашего аккаунта. Свяжитесь с вашим администратором для изменения предела.", + "monitorEditText1": "Неверные данные - убедитесь, что введена корректная строка для импорта", + "monitorEditText2": "Неверная строка деталей. Убедитесь, что введа корректная JSON-строка вместо обычного объекта.", + "monitorGetText1": "неполный запрос, уберите слэш в конце URL или введите приемлимое значение.", + "monitorStateNotEnoughChanges": "Вам необходимо внести изменения в конфигурацию вашего монитора, прежде чем попытаться добавить ее в предустановку.", + "monitorStatesError": "Мониторинг ошибки пресетов", + "months": "месяцев", + "motorcycle": "мотоцикл", + "mouse": "мыши", + "mpeg2_mmal": "MPEG-2 (Raspberry Pi)", + "mpeg2_qsv": "MPEG2 (Quick Sync Video)", + "mpeg4_cuvid": "MPEG4 CUVID", + "mpeg4_mmal": "MPEG-4 (Raspberry Pi)", + "noLoginTokensAdded": "Там нет альтернативных логин, связанных с этой учетной записью.", + "noSpecialCharacters": "Без пробелов или специальных символов.", + "noTriggerText": "Если движение не было обнаружено после периода времени ожидания, вы получите уведомление о Discord.", + "noUndoForAction": "Вы не можете отменить это действие.", + "notActivatedText": "Ваша установка не выполнила активацию.", + "notEnoughFramesText1": "Недостаточно кадров для компиляции.", + "notPermitted1": "Это действие не разрешено администратором вашей учетной записи.", + "on": "на", + "on Error": "в случае ошибки", + "on Event": "на мероприятии", + "onvifdeviceManagerGlobalTip": "ONVIF позволяет изменять внутренние настройки камеры. Onvif - это нечто, к сожалению, это может означать много вещей. При этом вы можете увидеть вариант в этом инструменте, но он не может быть редактируемым. Обычно это связано с тем, что поставщик камеры не добавил этот метод или отклонился от своего предполагаемого использования. В этих случаях вам нужно будет ввести конфигурацию камеры с помощью предписанного метода поставщика камеры, это обычно открывает IP -адрес камеры в вашем веб -браузере.", + "onvifdeviceSavedFoundErrorText": "Некоторые настройки могли вернуться к предыдущему значению. Возможно, что модифицированный вариант недоступен с этой камерой через ONVIF.", + "onvifdeviceSavedText": "Внутренние настройки камеры были сохранены. Вам может потребоваться перезагрузить камеру, чтобы эти изменения вступили в силу.", + "openImagesDownloadConfirm": "Вы уверены, что хотите начать загружать изображения и ограничивающие коробки (предустановленные матрицы) с OpenImages?", + "openImagesDownloadConfirmStop": "Вы уверены, что хотите перестать тренироваться?", + "opencl": "Opencl", + "opencvCascadesText": "Если вы ничего не видите здесь, просто загрузите этот пакет Каскады . Оставьте их в plugins/opencv/cascades , затем нажмите обновить .", + "orange": "апельсин", + "oven": "духовой шкаф", + "p2pServerNotSelectedText": "Выберите сервер из списка и нажмите «Сохранить». Подождите 10 секунд, а затем попытайтесь открыть приборную панель удаленно.", + "p2pSettingsText1": "Вам нужно будет обновить эту страницу для применения изменений.", + "parking meter": "счетчик на стоянке", + "performanceOptimizeText1": "Ваша камера предоставляет данные потока H.264. Вы можете установить тип потока на HLS, Poseidon и видеокодек для копирования.", + "person": "человек", + "pizza": "пицца", + "possibleInternalError": "Возможная внутренняя ошибка", + "postDataBroken": "Проверьте формат JSON. Убедитесь, что он строка и определен в разделе «Данные»", + "potted plant": "растение в горшке", + "powerVideoEventLimit": "Вы установили высокий предел события. Вы уверены, что хотите сделать этот запрос?", + "privateKey": "Частный ключ", + "qsv": "QSV", + "rebootingCamera": "Перезагрузить камеру", + "refrigerator": "холодильник", + "remote": "дистанционный пульт", + "restartRequired": "Перезапуск ядра Shinobi требуется для вступления в силу.", + "sandwich": "сэндвич", + "scissors": "ножницы", + "separateByCommasOrRange": "Разделять запятыми или диапазоном", + "setMaxStorageAmountText": "Вы должны установить максимальную сумму хранения в настройках учетной записи, расположенные слева. Найдите опцию в разделе профиля. По умолчанию 10 ГБ.", + "sheep": "овца", + "sink": "раковина", + "sizePurgeLockedText": "Похоже, что блокировка чистки размера (deleteoverMax) не удалось разблокировать. Разблокируется сейчас ...", + "skateboard": "скейтборд", + "skipPingText1": "Попробуйте установить «пропустить пинг» на да.", + "skis": "лыжи", + "snowboard": "сноуборд", + "sorryNo": "Извините но нет", + "sorryNothingWasFound": "Извините, ничего не было найдено.", + "spoon": "ложка", + "sports ball": "Спортивный мяч", + "startUpText0": "Проверка используемого диска...", + "startUpText1": "Проверка используемого диска окончена.", + "startUpText2": "все пользователи проверены, ожидаю закрытия открытых файлов и удаления файлов, превышающих ограничения пользователей", + "startUpText3": "даю немного времени на завершение проверки видео. 3 секунды.", + "startUpText4": "Запуск мониторов... Пожалуйста, ожидайте...", + "startUpText5": "Shinobi готов.", + "startUpText6": "Несвязанные видео найдены и добавлены", + "stop sign": "знак СТОП", + "subAccountManager": "Менеджер суб-аккунтирования", + "substreamConnectionText": "Вы можете оставить детали подключения как есть, если вы хотите, чтобы оно использовало основную информацию о подключении, установленную выше.", + "substreamOutputText": "Здесь вы можете установить конфигурацию потока по требованию. Узнайте о задержка типов потоков здесь. ", + "substreamText": "Это метод просмотра живого потока по требованию. Вы можете сделать так, чтобы процесс просмотра был доступен только тогда, когда кто -то смотрит или будет использоваться для переключения между низким и высоким разрешением.", + "suitcase": "чемодан", + "superAdminText": "Файл \"super.json\" не существует. Переименуйте \"super.sample.json\" в \"super.json\".", + "superAdminTitle": "Shinobi: Супер Админ", + "surfboard": "доска для серфинга", + "teddy bear": "плюшевый медведь", + "tennis racket": "теннисная ракетка", + "tie": "галстук", + "toaster": "тостер", + "toilet": "туалет", + "tokenNotUserBound": "Эта ручка для входа не связана с пользователем на этом сервере!", + "tokenNotUserBoundPt2": "Введите свои учетные данные, затем используйте кнопку входа в Google, чтобы быстро ссылаться.", + "toothbrush": "зубная щетка", + "total": "итого", + "traffic light": "светофор", + "train": "тренироваться", + "truck": "грузовая машина", + "tv": "телевидение", + "umbrella": "зонтик", + "undoAllUnsaveChanges": "Вы уверены, что хотите это сделать? Это отменит все неспасенные изменения.", + "unexpectedExitText": "Информация об этом выходе будет найдена до этого журнала. Кроме того, вот команда FFMPEG, которая использовалась, когда процесс разбился.", + "updateCamerasInternalSettings": "Обновить внутренние настройки камеры?", + "updateKeyText1": "\"updateKey\" отсутствует в \"conf.json\", невозможно провести обновление, пока он не будет добавлен.", + "updateKeyText2": "\"updateKey\" неверный.", + "updateNotice1": "Обновление Shinobi означает перезапись файлов. Если вы самостоятельно изменили какие -либо файлы, вы должны обновить Shinobi вручную. Ваши конфигурации и видеофайлы не будут изменены.", + "useSubStreamOnlyWhenWatching": "Только при просмотре используйте подстрим", + "vaapi": "Ваапи (VA-API)", + "vase": "ваза", + "vda": "VDA (аппаратное ускорение Apple VDA)", + "vdpau": "Vdpau", + "videoBuildingText1": "Видео в настоящее время строит. Проверьте еще раз позже.", + "videotoolbox": "VideoToolbox", + "vp8_cuvid": "VP8 NVENC (NVIDIA HW ACCEL)", + "vp8_qsv": "VP8 (Quick Sync Video)", + "vp9_cuvid": "VP9 NVENC (NVIDIA HW ACCEL)", + "wannaReset": "Вы хотите сбросить?", + "willTriggerAnEvent": "запустит событие", + "wine glass": "бокал для вина", + "years": "лет", + "zebra": "зебра" +} \ No newline at end of file diff --git a/languages/tr.json b/languages/tr.json new file mode 100644 index 00000000..0350c1be --- /dev/null +++ b/languages/tr.json @@ -0,0 +1,1649 @@ +{ + "\"No Motion\" Detector": "\"Hareket yok\" dedektör", + "# of Allow MJPEG Clients": "Sonsuz için MJPEG istemcilerinin için", + "'Already Installing...'": "'Zaten yükleme ...'", + "180 Degrees": "180 derece", + "2-Factor Authentication": "2 faktörlü kimlik doğrulama", + "90 Clockwise": "90 saat yönünde", + "90 Clockwise and Vertical Flip": "90 saat yönünde ve dikey flip", + "90 Counter Clockwise and Vertical Flip (default)": "90 Saat yönünde ve dikey flip (varsayılan)", + "AND": "VE", + "API": "API", + "API Key": "API Anahtarı", + "API Key Action Failed": "API anahtar eylemi başarısız oldu", + "API Key Added": "API Anahtarı Eklendi", + "API Key Deleted": "API anahtarı silindi", + "API Keys": "API anahtarları", + "APIKeyAddedText": "Bu anahtarı şimdi kullanabilirsiniz.", + "APIKeyDeletedText": "Anahtar silindi. Artık işe yaramayacak.", + "ASC": "ASC", + "Accelerator": "Gaz pedalı", + "Account Info": "Hesap bilgisi", + "Account Information": "Hesap Bilgileri", + "Account Privileges": "Hesap ayrıcalıkları", + "Account Save": "Hesap Kaydet", + "Account Settings": "Hesap ayarları", + "AccountEditText1": "Düzenleyemedi. Sorun devam ederse sayfasını yenileyin.", + "Accounts": "Hesaplar", + "Action for Selected": "Seçilen için Eylem", + "Activated": "Aktif", + "Active Monitors": "Aktif monitörler", + "Add": "Ekle", + "Add All": "Hepsini ekle", + "Add Camera": "Kamera ekle", + "Add Cameras": "Kamera ekle", + "Add Channel": "Kanal ekle", + "Add Input Feed": "Giriş beslemesi ekle", + "Add Map": "Harita ekle", + "Add Monitor": "Monitör Ekle", + "Add New": "Yeni ekle", + "AddToPreset": "Ön ayara ekle", + "Additional Inputs": "Ek Girişler", + "Admin": "Yönetici", + "Advanced": "ileri", + "After": "Sonrasında", + "Again": "Tekrar", + "Age": "Yaş", + "Alert Sound": "Uyarı sesi", + "Alert Sound Delay": "Uyarı Ses Gecikmesi", + "All Logs": "Tüm günlükler", + "All Monitors": "Tüm monitörler", + "All Monitors and Privileges": "Tüm monitörler ve ayrıcalıklar", + "All Privileges": "Tüm ayrıcalıklar", + "All Warnings": "Tüm uyarılar", + "All streams in first feed": "İlk yemdeki tüm akışlar", + "Allow API Trigger": "API tetikleyicisine izin ver", + "Allow Next Alert": "Sonraki Uyarıya İzin Verin", + "Allow Next Command": "Sonraki Komuta İzin Ver", + "Allow Next Discord Alert": "Sonraki Discord uyarısına izin verin ", + "Allow Next Email": "Sonraki e -postaya izin verin ", + "Allow Next Trigger": "Sonraki tetikleyiciye izin ver", + "Allow Next Webhook": "Sonraki webhook'a izin ver", + "Allowed IPs": "İzin verilen IPS", + "Already Processing": "Zaten işleme", + "Already exists": "Zaten var", + "Alternate Logins": "Alternatif girişler", + "Always": "Her zaman", + "Amazon S3": "Amazon S3", + "Amazon S3 Upload Error": "Amazon S3 Yükleme Hatası", + "Analyzation Duration": "Analiz süresi", + "AppNotEnabledText": "Uygulama etkin değil, hesap ayarlarınızda etkinleştirin.", + "April": "Nisan", + "Archive": "Arşiv", + "Are you sure?": "Emin misin?", + "Attach Snapshot": "Anlık görüntü ekleyin", + "Attach Video Clip": "Video klip ekleyin", + "Audio": "Ses", + "Audio Bit Rate": "Ses bit hızı", + "Audio Codec": "Ses Kodek", + "Audio Detection": "Ses tespiti", + "Audio Detector": "Ses dedektörü", + "Audio stream only from first feed": "Yalnızca ilk yemden ses akışı", + "Audio streams only": "Yalnızca ses akışları", + "August": "Ağustos", + "Authenticate": "Doğrulamak", + "Authenticated": "Doğrulanmış", + "Authentication Failed": "Kimlik doğrulama başarısız oldu", + "Auto": "Oto", + "Automatic": "Otomatik", + "Automatic Checking Cancelled": "Otomatik çek iptal edildi", + "Automatic Codec Repair": "Otomatik Codec onarımı", + "Automatic Field Fill": "Otomatik alan dolgusu", + "Autosave": "Otomobil", + "Back": "Geri", + "Backblaze B2": "Backblaze B2", + "Backblaze Error": "Backblaze Hatası", + "BacklightCompensation": "Arka ışık telafisi", + "Backup": "Destek olmak", + "Bandwidth": "Bant genişliği", + "Base64 over Websocket": "Base64 WebSocket üzerinden", + "Basic Authentication": "Temel kimlik doğrulama", + "Batch": "Grup", + "Before": "Önceki", + "Bind Credentials": "Bağlantı Kimlik Bilgileri (Parola)", + "BitrateLimit": "Bit hızı sınırı", + "Blank for No Change": "Değişiklik için boş", + "Bottom Left": "Sol alt", + "Bottom Right": "Sağ alt", + "Brightness": "Parlaklık", + "Browser Console Log": "Tarayıcı Konsolu Günlüğü", + "Bucket": "Kova", + "Buffer Preview": "Arabellek önizlemesi", + "Build": "İnşa etmek", + "Build Video": "Video Oluştur", + "Building": "Bina", + "CPU": "İşlemci", + "CPU indicator will not work. Continuing...": "CPU göstergesi çalışmaz. Devam ediyor...", + "CPU used by this stream": "Bu akış tarafından kullanılan CPU", + "CSS": "CSS Gösterge Tablonuzu Stil. ", + "Calendar": "Takvim", + "Call Method": "Çağrı yöntemi", + "Camera Password": "Kamera şifresi", + "Camera Username": "Kamera Kullanıcı Adı", + "Camera is not recording": "Kamera kayıt değil", + "Camera is not running": "Kamera çalışmıyor", + "Camera is not streaming": "Kamera akış değil", + "CameraNotRecordingText": "Ayarlar uyumsuz olabilir. Kodlayıcıları kontrol edin. Yeniden başlatma...", + "Can Authenticate Websocket": "WebSocket'i doğrulayabilir", + "Can Change User Settings": "Kullanıcı ayarlarını değiştirebilir", + "Can Control Monitors": "Monitörleri kontrol edebilir", + "Can Create and Delete Monitors": "Monitörler oluşturabilir ve silebilir", + "Can Delete Videos": "Videoları silebilir", + "Can Delete Videos and Events": "Videoları ve etkinlikleri silebilir", + "Can Edit Monitor": "Monitörü Düzenleyebilir", + "Can Get Logs": "Günlük alabilir", + "Can Get Monitors": "Monitör alabilir", + "Can View Logs": "Günlükleri görüntüleyebilir", + "Can View Monitor": "Monitörü görüntüleyebilir", + "Can View Snapshots": "Anlık görüntüleri görüntüleyebilir", + "Can View Streams": "Akışları görüntüleyebilir", + "Can View Videos": "Videoları görüntüleyebilir", + "Can View Videos and Events": "Videoları ve etkinlikleri görüntüleyebilir", + "Can edit Max Days": "Maksimum günleri düzenleyebilir", + "Can edit Max Storage": "Max depolamayı düzenleyebilir", + "Can edit how long to keep Events": "Etkinlikleri ne kadar tutacağınızı düzenleyebilir", + "Can edit how long to keep Logs": "Günlükleri ne kadar tutacağınızı düzenleyebilir", + "Can use Admin Panel": "Yönetici panelini kullanabilir", + "Can use Amazon S3": "Amazon S3 kullanabilir", + "Can use Discord Bot": "Discord bot kullanabilir", + "Can use LDAP": "LDAP kullanabilir", + "Can use SFTP": "SFTP kullanabilir", + "Can use Wasabi Hot Cloud Storage": "Wasabi Sıcak Bulut Depolama Kullanabilir", + "Can use WebDAV": "Webdav kullanabilir", + "Can't Connect": "Bağlanamıyorum", + "Cannot watch a monitor that isn't running.": "Koşmayan bir monitör izleyemiyorum.", + "Cards": "Kart", + "Carousel in Background": "Arka planda atlıkarınca", + "Center": "Center URL adresi ", + "Channel": "Kanal", + "Channel ID": "Kanal kimliği", + "Chat on Discord": "Discord hakkında sohbet edin", + "Check": "Kontrol etmek", + "Check Signal Interval": "Sinyal aralığını kontrol edin ", + "Check for Motion First": "Önce Hareketi Kontrol Edin", + "Check the Channel ID": "Kanal Kimliğini Kontrol Edin", + "Check the Recipient ID": "Alıcı Kimliğini Kontrol Edin", + "Clear": "Açık", + "Clear Recorder Process": "Kayıt cihazı işlemi", + "Close": "Kapat", + "Close All Monitors": "Tüm monitörleri kapatın", + "Closed": "Kapalı", + "Cloud": "Bulut", + "Codec Mismatch": "Kodek uyuşmazlığı", + "Color Threshold": "Renk eşiği", + "ColorSaturation": "Renk doygunluğu", + "Command": "Emretmek", + "Command on Trigger": "Tetikte komut", + "Common Objects": "Ortak nesneler", + "Complete Stream URL": "Tam Akış URL'si", + "Conditions": "Koşullar", + "Confidence": "Kendinden emin", + "Confidence of Detection": "Tespitin güveni", + "Configuration": "Konfigürasyon", + "Confirm": "Onaylamak", + "Connected": "Bağlı", + "Connected Users": "Bağlı Kullanıcılar", + "Connection": "Bağ", + "Connection Type": "Bağlantı türü", + "Connection timed out": "Bağlantı zaman aşımı", + "Contains": "İçermek", + "Contrast": "Zıtlık", + "Control": "Kontrol", + "Control Error": "Kontrol hatası", + "Control Trigger Started": "Kontrol Tetikleyicisi başladı", + "Control Triggered": "Kontrol Tetiklendi", + "ControlErrorText1": "Kontrol etkin değil", + "ControlErrorText2": "Bağlantı ayrıntılarınızı kontrol edin. Temel URL'yi 8000 veya 80 bağlantı noktasına yönlendirmeniz gerekebilir. Kimlik doğrulama bilgilerinizi kontrol edin.", + "Controllable": "Kontrol edilebilir", + "Controls and Logs": "Kontroller ve günlükler", + "Copied": "Kopyalanmış", + "Copied to Clipboard": "Panoya kopyalandı", + "Copy": "Kopya", + "Copy Connection Settings": "Bağlantı Ayarlarını Kopyala", + "Copy Custom Settings": "Özel Ayarları Kopyala", + "Copy Detector Settings": "Dedektör Ayarlarını Kopyala", + "Copy Group Settings": "Grup Ayarlarını Kopyala", + "Copy Input Settings": "Giriş Ayarlarını Kopyala", + "Copy JPEG API Settings": "JPEG API ayarlarını kopyala", + "Copy Logging Settings": "Günlük ayarlarını kopyala", + "Copy Mode": "Kopya Modu", + "Copy Recording Settings": "Kayıt Ayarlarını Kopyala", + "Copy Remote Link": "Uzaktan bağlantıyı kopyala", + "Copy Settings": "Ayarları Kopyala", + "Copy Stream Channel Settings": "Akış Kanalı Ayarlarını Kopyala", + "Copy Stream Channels": "Akış kanallarını kopyala", + "Copy Stream Settings": "Akış Ayarlarını Kopyala", + "Copy Stream URL": "Akış URL'sini Kopyala", + "Copy Timelapse Settings": "Timelapse ayarlarını kopyala", + "Copy to Settings": "Ayarlara Kopyala", + "Cores": "Çekirdek", + "Could not create Bucket.": "Kova oluşturamadı.", + "Count Objects": "Nesneleri Say", + "Count Objects only inside Regions": "Nesneleri yalnızca bölgelerde say", + "Country of Plates": "Tabak ülkesi", + "Counts of Motion": "Hareket sayıları", + "Create Sub-Accounts at /admin": "/Admin atında alt hesap oluştur", + "Creating New Account": "Yeni Hesap Oluşturma", + "Creation Interval": "Yaratılış aralığı", + "Current": "Akım", + "Currently Active": "Şu anda aktif", + "Currently viewing": "Şu anda görüntülüyor", + "Custom": "Gelenek", + "Custom Auto Load": "Özel Otomatik Yük", + "Custom Base URL": "Özel Taban URL Ana bilgisayar url'ini kullanmak için boş bırakın ", + "Custom Endpoint": "Özel uç nokta", + "DB Lost.. Retrying..": "Veritabanı kayıp .. yeniden deneme ..", + "DESC": "Desc", + "DHCP": "DHCP", + "DNS": "DNS", + "Daily Events": "Günlük Etkinlikler", + "Dashboard": "Gösterge Paneli", + "Dashboard Language": "Gösterge paneli dili", + "Dashcam": "Şık", + "Dashcam (Streamer v2)": "Dashcam (Salam V2)", + "Database": "Veri tabanı", + "Database Not Found": "Veritabanı bulunamadı", + "Database row does not exist": "Veritabanı satırı yok", + "Date": "Tarih", + "Date Added": "Ekleme Tarihi", + "Date Range": "Tarih aralığı", + "Date Updated": "Tarih Güncellendi", + "Date and Time": "Tarih ve saat", + "DateTimeType": "Tarih yönetimi", + "DaylightSavings": "Gün ışığından yararlanma", + "Days": "Günler", + "Debug": "Ayıklamak", + "December": "Aralık", + "Default": "Varsayılan", + "Delay for Snapshot": "Anlık görüntü için gecikme", + "Delete": "Silmek", + "Delete Camera": "Kamerayı sil", + "Delete Filter": "Filtreyi sil", + "Delete Logs": "Günlükleri Sil", + "Delete Matches": "Maçları Sil", + "Delete Monitor": "Monitörü Sil", + "Delete Monitor State?": "Monitör Durumunu Sil", + "Delete Monitor States Preset": "Monitör Durumlarını Sil Ön ayar", + "Delete Monitors and Files": "Monitörleri ve dosyaları sil", + "Delete Motionless Video": "Hareketsiz videoyu sil", + "Delete Motionless Videos (Record)": "Hareketsiz videoları sil (kayıt)", + "Delete Region": "Bölge Sil", + "Delete Schedule": "Programı Sil", + "Delete Selected Videos": "Seçilen videoları sil", + "Delete Timelapse Frame": "Timelapse çerçevesini sil", + "Delete Video": "Videoyu sil", + "Delete selected": "Silme seçildi", + "DeleteMonitorText": "Bu monitörü silmek istiyor musunuz? Onu kurtaramazsın. Dosyaların dosya sisteminde kalmasını seçebilirsiniz. Aynı kimliğe sahip bir monitörü yeniden oluşturmayı seçerseniz, videolar ve etkinlikler gösterge tablosunda görünür olacaktır.", + "DeleteMonitorsText": "Bu monitörleri silmek ister misiniz? Onları kurtaramazsın. Bu kimliklerin dosyalarını dosya sisteminde tutmayı seçebilirsiniz. Kimliklerden biriyle bir monitörü yeniden oluşturmayı seçerseniz, videolar ve etkinlikler gösterge tablosunda görünür olacaktır.", + "DeleteSelectedVideosMsg": "Bu videoları silmek ister misiniz? Onları kurtaramazsın.", + "DeleteThisMsg": "Bunu silmek ister misin? Onu kurtaramazsın.", + "DeleteVideoMsg": "Bu videoyu silmek ister misiniz? Onu kurtaramazsın.", + "Deleted": "silindi", + "Deleted Schedule Configuration": "Silinmiş Program Yapılandırması", + "Deleted State Configuration": "Silinmiş durum yapılandırması", + "Detect Objects": "Nesneleri algılayın Aşağıya bakın ", + "Detection": "Tespit etme", + "Detection Engine": "Algılama motoru", + "Detection Event": "Tespit olayı", + "Detector": "Dedektör", + "Detector Buffer": "Dedektör tamponu", + "Detector Filters": "Dedektör filtreleri", + "Detector Flags": "Dedektör bayrakları", + "Detector Grouping": "Dedektör Gruplama Ayarları 'de grup ekleyin", + "Detector Rate": "Dedektör oranı (fps) ", + "Detector Recording Complete": "Dedektör kaydı tamamlandı", + "Detector Recording Process Exited Prematurely. Restarting.": "Dedektör kayıt işlemi erken çıktı. Yeniden başlatma.", + "DetectorText": "

Genişlik ve yükseklik kutuları gösterildiğinde, bunları 640x480 veya daha altına ayarlamanız gerekir. Bu, çerçevelerin okuma hızını optimize edecektir.

", + "Died": "Öldü", + "Digest Authentication": "Sindirim Kimlik Doğrulaması", + "Disable": "Devre dışı bırakmak", + "Disable Night Vision": "Gece Görüşünü Devre Dışı Bırak URL Adresi ", + "Disable Nightvision": "NightVision'u devre dışı bırakın", + "Disabled": "Engelli", + "Discord": "Anlaşmazlık", + "Discord Alert on Trigger": "Tetikte uyumsuzluk uyarısı", + "Discord Bot": "Anlaşmazlık botu", + "Discord on No Motion": "\"Hareket yok\"", + "DiscordErrorText": "Anlaşmazlığa göndermek bir hataya neden oldu", + "DiscordFailedText": "Anlaşmazlığa göndermek başarısız oldu", + "DiscordLoggedIn": "Discord bot kimlik doğrulandı", + "DiscordNotEnabledText": "Discord bot etkinleştirilmez, hesap ayarlarınızda etkinleştirin.", + "Documentation": "Belgeler", + "Does Not Contain": "İçermiyor", + "Don't Show for 1 Week": "1 Hafta Gösterme", + "Don't show this anymore": "Bunu artık gösterme", + "DontAddToPreset": "Ön ayara eklemeyin", + "Double Quote Directory": "Çift Alıntı Dizini Bazı dizinlerin boşlukları vardır. Bunu kullanmak bazı kameraları çökertebilir. ", + "Down": "Aşağı url adresi ", + "Down Stop": "Down Stop url adresi ", + "Download": "İndirmek", + "Download Bandwidth": "Bant genişliğini indir", + "Downloading Videos": "Video indirme", + "Downloading...": "İndirme ...", + "Duplicate": "Kopyalamak", + "EU": "AB", + "Easy Remote Access (P2P)": "Kolay Uzaktan Erişim (P2P)", + "Edit": "Düzenlemek", + "Edit Configuration": "Yapılandırmayı Düzenle", + "Edit Selected": "Seçili Düzenle", + "Edited Schedule Configuration": "Düzenlenmiş program yapılandırması", + "Edited State Configuration": "Düzenlenen durum yapılandırması", + "Email": "E -posta", + "Email Details": "E -posta Ayrıntıları", + "Email address is in use.": "E -posta adresi kullanılıyor.", + "Email and Password fields cannot be empty": "E -posta ve şifre alanları boş olamaz", + "Email on No Motion": "\"Hareket yok\" e e -posta", + "Email on Trigger": "Tetikleyici e -posta", + "Emotion": "Duygu", + "Emotion Average": "Duygu ortalaması", + "Enable": "Olanak vermek", + "Enable Night Vision": "Gece Görüşünü Etkinleştir URL Adresi ", + "Enable Nightvision": "NightVision'u etkinleştirin", + "Enabled": "Etkinleştirilmiş", + "Encoding": "Kodlama", + "EncodingInterval": "I-Frame", + "End": "Son", + "End Time": "Bitiş zamanı", + "Ended": "Bitti", + "Endpoint": "Son nokta", + "Endpoint Address": "Uç nokta adresi", + "Enlarge": "Büyütmek", + "Enter at least one IP": "En az bir IP girin", + "Enter this code to proceed": "Devam etmek için bu kodu girin", + "Equal to": "Eşittir", + "Error Connecting": "Hata bağlanma", + "Error While Decoding": "Kod çözerken hata", + "ErrorWhileDecodingText": "Donanımınızın ağ ile kararsız bir bağlantısı olabilir. Ağ bağlantılarınızı kontrol edin.", + "ErrorWhileDecodingTextAudio": "Kameranız bozuk veriler sağlıyor. Kameranın dahili ayarlarındaki sesi devre dışı bırakmayı deneyin.", + "Event": "Etkinlik", + "Event Counts": "Olay sayıları", + "Event Filter Error": "Olay filtresi hatası", + "Event Filters": "Olay filtreleri", + "Event Limit": "Olay sınırı", + "Event Occurred": "Olay meydana geldi", + "Event Rules": "Etkinlik Kuralları", + "Event Webhook Error": "Olay Webhook Hatası", + "EventText1": "Bir etkinliği tetikledi", + "EventText2": "Resim e -posta gönderemedi, dosyaya erişilemez", + "Events": "Olaylar", + "Events Found": "Olaylar Bulundu", + "Example": "Misal", + "Execute Command": "Komutu çalıştır", + "Executed": "Uygulanmış", + "Export": "İhracat", + "Export Selected Videos": "Seçilen videoları dışa aktarın", + "Export Video": "İhracat Videosu", + "ExportSelectedVideosMsg": "Bu videoları dışa aktarmak ister misiniz? Zip ve indirmek biraz zaman alabilir.", + "Exposure": "Poz", + "FFmpegCantStart": "FFMPEG başlayamadı", + "FFmpegCantStartText": "Bu kamera için kayıt motoru başlayamadı. Kamera yapılandırmanızda bir sorun olabilir. Bunun dışında herhangi bir günlük varsa, lütfen GitHub'daki sorunlarında yayınlayın.", + "FFmpegTip": "FFProbe basit bir multimedya akışı analizörüdür. Süre, kare hızı, kare boyutu vb.", + "FFprobe": "İncelemek, bulmak", + "FLV": "Flv", + "FLV Stream Type": "FLV akış tipi", + "FactorAuthText1": "Kod yalnızca 15 dakika etkin olacaktır. Tekrar giriş yaparsanız, zamanlayıcı aynı kodla 15 dakikaya sıfırlanır.", + "Fatal": "Ölümcül", + "Fatal Maximum Reached": "Ölümcül maksimuma ulaşıldı, kamerayı durdurdu.", + "FatalMaximumReachedText": "JPEG hatası ölümcüldü.", + "February": "Şubat", + "Feed-in Image Height": "Besleme görüntü yüksekliği", + "Feed-in Image Width": "Besleme görüntü genişliği", + "Female": "Dişi", + "Field Missing Value": "Alan Eksik Değer", + "Fields cannot be empty": "Tarlalar boş olamaz", + "File Delete Error": "Dosya Sil hatası", + "File Not Exist": "Dosya mevcut değil", + "File Not Found": "Dosya bulunamadı", + "File Not Found in Database": "Dosya Veritabanında bulunamadı", + "File Not Found in Filesystem": "Dosya sisteminde bulunamadı", + "File Type": "Dosya tipi", + "FileBin Share": "FileBin Paylaşımı", + "FileNotExistText": "Var olmayan dosyayı kaydedilemez. Bir şeyler yanlış gitti.", + "Filename": "Dosya adı", + "Filesize": "Dosya boyutu", + "Filter ID": "Filtre Kimliği", + "Filter Matches": "Filtre eşleşmeleri", + "Filter Name": "Filtre adı", + "Filter for Objects only": "Yalnızca nesneler için filtre", + "FilterMatchesText1": "Bu filtre koşulları karşıladı.", + "FilterMatchesText2": "videolar bulundu.", + "Filters": "Filtreler", + "Filters Updated": "Filtreler Güncellendi", + "FiltersUpdatedText": "Değişiklikleriniz kaydedildi ve uygulandı.", + "Find Where": "Nerede bulun", + "First stream in feed": "Feed'teki ilk akış", + "Fix": "Düzeltmek", + "Fix Video": "Düzeltme Video", + "FixVideoMsg": "Bu videoyu düzeltmek ister misiniz? Bu eylemi geri alamazsınız.", + "Flush PM2 Logs": "PM2 günlüklerini yıkamak", + "Font Path": "Yazı tipi yolu", + "Font Size": "Yazı Boyutu", + "For Group": "Grup için", + "Force Monitors Per Row": "Sıra başına kuvvet monitörleri", + "Force Port": "Kuvvet portu", + "Form Data Not Found": "Form verileri bulunamadı", + "Found Devices": "Bulunan Cihazlar", + "Frame Rate": "Kare hızı", + "FrameRateLimit": "Çerçeve Hızı Sınırı (FPS)", + "Frames": "Çerçeveler", + "Friday": "Cuma", + "Frigate": "Firkateyn", + "Full Frame Detection": "Tam Çerçeve Tespiti", + "Full Stream URL": "Tam Akış URL'si", + "Full URL Path": "Tam url yolu", + "Fullscreen": "Tam ekran", + "Gateway": "Geçit", + "Gender": "Cinsiyet", + "Generate Subtitles": "Altyazılar Oluşturun", + "Get Code": "Kod alın", + "Get Logs to Client": "Müşteriye Günlük Alın", + "Global Detector Settings": "Global Dedektör Ayarları", + "Google Drive": "Google sürücü", + "GovLength": "Gov", + "Greater Than": "Daha büyük", + "Greater Than or Equal to": "Daha büyük veya eşit", + "Grid": "Kafes", + "Group Key": "Grup anahtarı", + "Group Key is in use.": "Grup anahtarı kullanımda.", + "Group Name": "Grup ismi", + "Grouping": "Gruplandırma", + "H.264 / H.265 / H.265+": "H.264 / h.265 / h.265+", + "H264Profile": "H264 Profili", + "HEVC (H.265)": "HEVC (H.265)", + "HLS (.m3u8)": "HLS (.m3u8)", + "HLS (includes Audio)": "HLS (ses içerir)", + "HLS Audio Encoder": "Sesli kodlayıcı", + "HLS List Size": "Liste boyutu", + "HLS Live Start Index": "HLS Canlı Başlangıç Dizin", + "HLS Preset": "Önceden ayarlanmış şablon", + "HLS Segment Length": "Segment uzunluğu saniye cinsinden ", + "HLS Start Number": "HLS Başlangıç Numarası", + "HLS Video Encoder": "Video kodlayıcı", + "HTTP": "HTTP", + "HTTPS": "HTTPS", + "Hardware Accelerated": "Donanım hızlandı", + "Height": "Yükseklik", + "Help": "Yardım", + "Hide List": "Gizle Listesi", + "Hide Notes": "Gizle notları", + "Home": "Ev", + "Host": "Ev sahibi", + "Host Type": "Ana bilgisayar türü", + "Hostname": "Ana bilgisayar adı", + "Hotswap Modes (Watch-Only)": "Hotswap modları (sadece izleme)", + "How to Record": "Nasıl kaydedilir", + "IP Address": "IP adresi", + "Identity": "Kimlik", + "IdentityText1": "Sistem bu akış için verileri bu şekilde tanımlayacaktır. Kaydetmeye bastıktan sonra Monitör Kimliğini değiştiremezsiniz. İsterseniz, devam etmeden önce Monitör Kimliği daha insanı okunabilir hale getirebilirsiniz.", + "IdentityText2": " Monitör Kimliğini değiştirerek bir monitörü çoğaltabilir ve ardından Kaydet'e basabilirsiniz. zaten var olan bir monitör kimliğini kullanamazsınız veya bu monitörün veritabanı bilgilerinden kaydedilir.", + "Idle": "Boşta", + "Image Height": "Görüntü Yüksekliği", + "Image Location": "Resim Konumu", + "Image Position": "Görüntü konumu", + "Image Width": "Resim Genişliği", + "Imaging": "Görüntüleme", + "Import": "İçe aktarmak", + "Import Monitor Configuration": "Monitör Yapılandırması İçe Aktar", + "ImportMonitorConfigurationText": "Bunu yapmak, şu anda kaydedilmeyen değişiklikleri aşılacaktır. İçe aktarılan değişiklikler yalnızca Kaydet tuşuna bastığınızda uygulanır.", + "ImportMultiMonitorConfigurationText": "Bunu yapmak, herhangi bir monitörü içe aktarma dosyasında bulunan kimliklerle aşırılacaktır.", + "In": "İçinde", + "Incorrect Settings Chosen": "Seçilen yanlış ayarlar", + "Indifference": "Kayıtsızlık", + "Info": "Bilgi", + "Information": "Bilgi", + "Input": "Giriş", + "Input Feed": "Giriş besleme", + "Input Feeds Selected": "Giriş beslemesi seçildi", + "Input Flags": "Giriş Bayrakları", + "Input Map": "Giriş Haritası", + "Input Selector": "Giriş Seçicisi", + "Input Settings": "Giriş ayarları", + "Input Type": "Giriş tipi", + "InputText1": "Bu bölüm Shinobi'ye bir akarsu nasıl tüketileceğini anlatıyor. Optimum performans için kameranızın dahili ayarlarını ayarlamayı deneyin. Kameranızı bulmak için Shinobi'nin OnVIF tarayıcısında yerleşik kullanabilirsiniz. ONVIF tarayıcısını açmak için Sol üst ve sonra OnVIF'de kullanıcı adınızı tıklayın.", + "InputText2": "Kameralarınızı yapılandırma ve ayarlama hakkında bilgi edinin burada .", + "InputText3": "Kameranızın hangi giriş türünü bulmak için yardıma ihtiyacınız varsa, kamera urls listesine bir göz atabilirsiniz Shinobi web sitesi.", + "Inserted Schedule Configuration": "Eklenen Program Yapılandırması", + "Inserted State Configuration": "Eklenen durum yapılandırması", + "Install": "Düzenlemek", + "Interface": "Arayüz", + "Invalid Data": "Geçersiz veri", + "Invalid JSON": "Geçersiz JSON", + "Invalid Settings": "Geçersiz ayarlar", + "InvalidJSONText": "Lütfen bunun Shinobi Monitör Yapılandırması için geçerli bir JSON Dizesi olduğundan emin olun.", + "Inverse Trigger": "Ters tetik", + "Invert Y-Axis": "Y eksenini tersine çevir", + "IrCutFilter": "Gece görüşü", + "JPEG": "JPEG", + "JPEG (Auto Enables JPEG API)": "JPEG (Otomatik JPEG API'sını Etkinleştirir)", + "JPEG API": "JPEG API", + "JPEG Error": "Jpeg hatası", + "JPEG Mode": "JPEG modu", + "JPEGErrorText": "Kameranızdan veri alma sorunları vardı.", + "January": "Ocak", + "July": "Temmuz", + "June": "Haziran", + "LDAP": "Ldap", + "LDAP Success": "LDAP Başarısı", + "LDAP User Authenticated": "LDAP kullanıcı kimliği doğrulandı", + "LDAP User is New": "LDAP kullanıcısı yeni", + "Landing Page": "Açılış sayfası", + "Last": "Geçen", + "Last Modified": "Son düzenleme", + "Launch in New Window": "Yeni Pencerede Başlat", + "Leave blank for random.": "Rastgele için boş bırakın.", + "Leave blank for unlimited": "Sınırsız için boş bırakın", + "Left": "Sol url adresi ", + "Left Stop": "Sol durma url adresi ", + "Legacy Webhook": "Eski Webhook", + "Less Than": "Daha az", + "Less Than or Equal to": "Daha az veya eşit", + "License Activated": "Lisans etkinleştirildi", + "License Activation": "Lisans etkinleştirme", + "License Activation Failed": "Lisans etkinliği başarısız oldu", + "License Key": "Lisans anahtarı", + "License Plate Detector": "Plaka Dedektörü", + "Like": "Beğenmek", + "Limited": "Sınırlı", + "Link Google Account": "Bağlantı Google Hesabı", + "Link LDAP Account": "Link LDAP hesabı", + "Link Shinobi": "Link Shinobi", + "List Toggle": "Liste geçiş", + "List of Videos Delete Error": "Videoların Listesi Sil hatası", + "Live Grid": "Canlı ızgara", + "Live Stream Toggle": "Canlı akış geçişi", + "Live View": "Canlı görüntü", + "Local": "Yerel", + "Log Level": "Kütük seviyesi", + "Log Signal Event": "Günlük sinyali olayı yalnızca istemci tarafı ", + "Log Stream": "Kütük akışı", + "Logging": "Kerestecilik", + "Login": "Giriş yapmak", + "Logout": "Çıkış Yap", + "Logs": "Kütükler", + "Loop Stream": "Döngü akışı", + "MB": "MB", + "MJPEG": "Mjpeg", + "MP4 (copy, libx264, libx265)": "MP4 (kopya, libx264, libx265)", + "MPEG-4 (.mp4 / .ts)": "MPEG-4 (.mp4 / .ts)", + "MPEG-DASH (includes Audio)": "MPEG-DASH (ses içerir)", + "MQTT Client": "MQTT istemcisi", + "MQTT Error": "MQTT HATA", + "MQTT Inbound": "MQTT Gelen", + "MQTT Outbound": "MQTT giden", + "MailError": "Posta hatası: E -posta gönderilemedi, conf.json'u kontrol edin. Postaya dayanan özellikleri atlamak.", + "Main": "Ana", + "Male": "Erkek", + "Manual": "Manuel", + "Map": "Harita", + "March": "Mart", + "Matches": "Maçlar", + "Matrices": "Matris", + "Max Indifference": "Maksimum kayıtsızlık", + "Max Latency": "Maksimum gecikme", + "Max Number of Cameras": "Maksimum kamera sayısı", + "Max Storage Amount": "Maksimum depolama miktarı", + "MaxExposureTime": "Maksimum pozlama süresi", + "MaxGain": "Maksimum kazanç", + "Maximum Change": "Maksimum değişiklik", + "Maximum dB": "Maksimum DB", + "May": "Mayıs", + "Merge Selected Videos": "Seçilen videoları birleştirin", + "Merge Video": "Birleştirme Videosu", + "Merge and Download": "Birleştir ve indir", + "MergeSelectedVideosMsg": "Bu videoları birleştirmek ister misiniz? Birleştirmek ve indirmek biraz zaman alabilir. Bağlantı kapatıldığında dosya silinir. Tarayıcıyı tamamlanana kadar açık tuttuğunuzdan emin olun.", + "Methods": "Yöntem", + "Migrator": "Göçmen", + "MinExposureTime": "Minimum pozlama süresi", + "MinGain": "Minimum Kazanç", + "Minimum Change": "Minimum değişiklik", + "Minimum dB": "Minimum DB", + "Minutes": "Dakikalar", + "Mode": "Moda", + "Monday": "Pazartesi", + "Monitor": "İzlemek", + "Monitor Added by user": "Monitör kullanıcı tarafından eklendi.", + "Monitor Capture Rate": "Monitör yakalama oranı (fps) ", + "Monitor Died": "Monitör öldü", + "Monitor Edit": "Monitör Düzenle", + "Monitor Groups": "Monitör grupları", + "Monitor ID": "Monitör Kimliği", + "Monitor Idling": "Rölanti izlemek", + "Monitor Name": "Monitör Adı", + "Monitor Settings": "Monitör Ayarları", + "Monitor Start": "Monitör Başlangıç", + "Monitor States": "İzleme durumları", + "Monitor States and Schedules": "Durumları ve programları izleyin", + "Monitor Stop": "Monitör Durağı", + "Monitor Stopped": "Monitör durdu", + "Monitor Updated by user": "Kullanıcı tarafından güncellenen izleme.", + "Monitor is now Disabled": "Monitör artık devre dışı bırakıldı", + "Monitor is now Idle": "Monitör artık boşta", + "Monitor is now Recording": "Monitör şimdi kayıt yapıyor", + "Monitor is now Watching": "Monitör şimdi izliyor", + "Monitor mode changed": "Monitör modu değişti", + "Monitor mode is already": "Monitör modu zaten", + "Monitor or Key does not exist.": "Monitör veya anahtar mevcut değildir.", + "MonitorIdlingText": "Monitör oturumunun boşta olması emredildi.", + "MonitorStatesText": "Bunu nasıl kullanacağınızı öğrenebilirsiniz.", + "MonitorStoppedText": "Monitör oturumunun durması emredildi.", + "Monitors": "Monitörler", + "Monitors per row": "Montaj için satır başına monitörler ", + "Monitors to Copy to": "Kopyalayacak monitörler", + "Montage": "Montaj", + "Motion": "Hareket", + "Motion Detection": "Hareket algılama", + "Motion GUI": "Hareket gui", + "Motion Meter": "Hareketli metre", + "Motion Threshold": "Hareket eşiği", + "Mp4Frag": "Mp4frag", + "Must be atleast one row": "En az tek bir sıra olmalı", + "Mute Audio": "Sessiz Ses", + "NTP": "NTP", + "NTP Servers": "NTP sunucuları", + "NVIDIA": "Nvidia", + "Name": "İsim", + "Name cannot be empty.": "İsim boş olamaz.", + "Nameservers": "İsim verenler", + "Network": "Ağ", + "Network Manager": "Ağ yöneticisi", + "Never": "Asla", + "New Authentication Token": "Yeni kimlik doğrulama jetonu", + "New Monitor": "Yeni Monitör", + "Newest": "En yeni", + "Next Video": "Sonraki video", + "No": "Numara", + "No API Key": "API anahtarı yok", + "No Audio": "Ses yok", + "No Data": "Veri yok", + "No Events found for this video": "Bu video için etkinlik bulunamadı", + "No Group with this key exists": "Bu anahtarla hiçbir grup yok", + "No Monitor Exists with this ID.": "Bu kimlikle hiçbir monitör yoktur.", + "No Monitor Found, Ignoring Request": "Monitör bulunamadı, isteği görmezden geliyor", + "No Monitor ID Present in Form": "Formda Monitör Kimliği Yok", + "No Monitors Selected": "Monitör seçilmedi", + "No Region": "Bölge yok", + "No Rotation": "Rotasyon yok", + "No Sound": "Ses yok", + "No Trigger": "Tetik yok", + "No Videos Found": "Video bulunamadı", + "No such file": "Böyle bir dosya yok", + "NoLogsFoundForDateRange": "Bu tarih aralığında kütük bulunamadı. Tarih aralığını genişletmeyi deneyin.", + "NoMotionEmailText1": "İçin hareket yok", + "NoMotionEmailText2": "Kamerada herhangi bir hareket tespit edilmedi", + "NoVideosFoundForDateRange": "Bu tarih aralığında video bulunamadı. Başlangıç tarihini daha fazla ayarlamayı deneyin.", + "Noise Filter": "Gürültü filtresi", + "Noise Filter Range": "Gürültü filtresi aralığı", + "Non-Standard ONVIF": "Standart olmayan onvif", + "Not Activated": "Etkinleştirilmedi", + "Not Authorized": "Yetkili değil", + "Not Connected": "Bağlı değil", + "Not Equal to": "Eşit değil", + "Not Found": "Bulunamadı", + "Not In": "Değil", + "Not Matches": "Eşleşmez", + "Not Permitted": "İzin verilmedi", + "Not Saved": "Kaydedilmedi", + "Not an Administrator Account": "Yönetici hesabı değil", + "NotAuthorizedText1": "Yetkili değil, init komutunu \"auth\", \"ke\" ve \"uid\" ile gönderin", + "Notes": "Notalar", + "NotesPlacholder": "Bu kamera ayarları için ayrılmak istediğiniz yorumlar.", + "Nothing exists": "Hiçbir şey yok", + "Notice": "Fark etme", + "Notification Sound": "Bilgilendirme sesi", + "Notification Video Length": "Bildirim video uzunluğu", + "Notifications": "Bildirimler", + "NotifyErrorText": "Bildirim göndermek bir hataya neden oldu", + "November": "Kasım", + "Number of Days to keep": "Tutulması gereken gün sayısı", + "Numeric criteria unsupported for Region tests, Ignoring Conditional": "Bölge testleri için desteklenmeyen sayısal kriterler, koşullu göz ardı ederek", + "OAuth Code": "OAuth kodu", + "OAuth Credentials": "OAuth kimlik bilgileri", + "ONVIF": "Atış", + "ONVIF Device Manager": "ONVIF Cihaz Yöneticisi", + "ONVIF Port": "Onvif bağlantı noktası", + "ONVIF Scanner": "OnVIF tarayıcı", + "ONVIFErr400": "ONVIF bağlantı noktası bulundu ancak akış URL'sini alırken yetkilendirme başarısız oldu. Tarama için kullanılan kullanıcı adını ve şifreyi kontrol edin. Kamera zamanınızın ve sunucu sürenizin senkronize olduğundan emin olun.", + "ONVIFErr404": "Bulunamadı. Bu sadece bir ağ cihazı için web paneli olabilir.", + "ONVIFErr405": "İzinsiz metod. Tarama için kullanılan kullanıcı adını ve şifreyi kontrol edin.", + "ONVIFEventsNotAvailable": "OnVIF etkinlikleri mevcut değil", + "ONVIFEventsNotAvailableText1": "Bu hizmet bu kamera için mevcut olmayabilir veya ONVIF henüz başlatılmamıştır.", + "ONVIFnotCompliantProfileT": "Kamera onvif profil t uyumlu değil", + "ONVIFnote": "OnVIF cihazlarını kendi dışındaki ağlardaki keşfedin veya geçerli ağınızı taramak için boş bırakın.
Kullanıcı adı ve şifre boş bırakılabilir.", + "OR": "VEYA", + "Object": "Nesne", + "Object Count": "Nesne sayımı", + "Object Detection": "Nesne algılama", + "Object Detector Flags": "Nesne Dedektörü Bayrakları", + "Object Tag": "Nesne etiketi", + "Objects to look for": "Aranacak nesneler", + "October": "Ekim", + "Off": "Kapalı", + "Oldest": "En eski", + "On": "Açık", + "On Unexpected Exit": "Beklenmedik çıkışta", + "Open": "Açık", + "Open All Monitors": "Tüm monitörleri aç", + "Open Remote Dashboard": "Uzaktan Gösterge Tablosunu Aç", + "OpenCV Cascades": "Opencv Cascades", + "Operating Hours": "Çalışma saatleri", + "Optional": "İsteğe bağlı", + "Options": "Seçenekler", + "Order Streams": "Sipariş Akışı", + "Original Choice": "Orijinal Seçim", + "Other Devices": "Diğer cihazlar", + "Output": "Çıktı", + "Output Method": "Çıktı yöntemi", + "P2P API Key": "P2P API Anahtarı", + "P2P Host": "P2P ana bilgisayar", + "P2P Server Not Selected": "P2P sunucusu seçilmedi", + "P2P Settings Applied": "P2P Ayarları Uygulanan", + "PTZ Tracking": "PTZ izleme", + "PTZ Tracking Target": "PTZ izleme hedefi", + "Password": "Parola", + "Password Again": "Şifre Tekrar", + "Passwords don't match": "Şifreler eşleşmiyor", + "Paste JSON here.": "JSON'u buraya yapıştırın veya dosyayı yükleyin.", + "Path": "Yol", + "Pause": "Duraklat", + "Per Monitor": "Monitör başına", + "Performance Optimization Possible": "Performans Optimizasyonu Mümkün", + "Permissions": "İzin", + "Ping Failed": "Ping başarısız oldu", + "Plain": "Sade", + "Play": "Oyna", + "Playback": "Geri çalma", + "Please Check Your Settings": "Lütfen ayarlarınızı kontrol edin", + "Please Wait for Completion": "Seçilen dosya sayısına bağlı olarak lütfen tamamlanmayı bekleyin.", + "Please Wait or Click to Stop Checking": "Lütfen kontrol etmeyi bırakmak için bekleyin veya tıklayın", + "Please Wait...": "Lütfen bekleyin...", + "Plugin": "Eklenti", + "Plugin Manager": "eklenti Yöneticisi", + "Points": "Puan", + "Pop": "Pop", + "Popout Monitor on Event": "Etkinlikte Popout Monitor", + "Port": "Liman", + "Pose": "Poz", + "Poseidon": "Poseidon", + "Position X": "X Pozisyonu", + "Position Y": "Y konumu", + "Power Video Viewer": "Güç Video Görüntüleyicisi", + "Power Viewer": "Güç izleyicisi", + "Preferences": "Tercihler", + "Prefix": "Önek", + "Preset": "Önceden belirlemek", + "Preset Name": "Önceden ayarlanmış isim", + "Presets": "Ön planlar", + "Preview": "Ön izleme", + "Previous Video": "Önceki video", + "Primary Engine": "Birincil motor", + "Primary Input": "Birincil giriş", + "Privileges": "Ayrıcalıklar", + "Probe Size": "Prob boyutu", + "Process Already Running": "İşlem zaten çalışıyor", + "Process Crashed for Monitor": "Monitör için çöktü", + "Process Not Running": "İşlem Çalışmıyor", + "Process Started": "Süreç başladı", + "Process Unexpected Exit": "Beklenmedik çıkış işlemini işleyin", + "Processor": "İşlemci", + "Profile": "Profil", + "Protocol": "Protokol", + "Public on ShinobiHub": "Shinobihub'da halka açık", + "Quality": "Kalite", + "Query": "Sorgu", + "Quick Settings": "Hızlı Ayarlar", + "Quick Sync Video": "Hızlı Senkronizasyon Videosu", + "RAM": "Veri deposu", + "RTMP": "RTMP", + "RTMP Stream": "RTMP akışı", + "RTMP Stream Flags": "RTMP akış bayrakları", + "RTMPS": "RTMPS", + "RTSP": "RTSP", + "RTSP Transport": "RTSP Taşımacılığı", + "Range or Single": "Menzil veya tek", + "Raspberry Pi": "Ahududu pi", + "Rate": "Oran (fps)
", + "Raw": "Çiğ", + "Raw H.264 Stream": "Ham H.264 akışı", + "Reason": "Sebep", + "Reboot": "Yeniden başlatmak", + "Reboot Camera": "Kamerayı yeniden başlat", + "Recent Events": "Son olaylar", + "Recent Videos": "Son Videolar", + "Recipient ID": "Alıcı kimliği", + "Recommended": "Tavsiye edilen", + "Reconnect Stream": "Akışı yeniden bağlayın", + "Record": "Kayıt", + "Record File Type": "Kayıt Dosya Türü", + "Record Height": "Kayıt yüksekliği", + "Record Video Filter": "Video filtresini kaydedin", + "Record Width": "Kayıt genişliği", + "Recorded Buffer": "Kaydedilmiş tampon", + "Recording": "Kayıt", + "Recording FPS": "Kayıt FPS", + "Recording FPS Change on Start": "Başlangıçta FPS Değişikliği Kayıt", + "Recording Flags": "Kayıt bayrakları", + "Recording Segment Interval": "Kayıt Segment Aralığı Dakikalarda ", + "Recording Timeout": "Dakikalarda
kayıt saati
", + "Recording Timestamp": "Kayıt Zaman damgası", + "Recording Watermark": "Kayıt Filigran", + "RecordingText": " kayıt dosyası türünü için webm b> ve Video Codec to libvpx kopya veya LIBX264 giriş türünüz olarak ayarlanmıştır.", + "Refresh List of Cascades": "Kaskadların Yenileme Listesi", + "Region": "Bölge", + "Region Editor": "Bölge editörü", + "Region Name": "Bölge adı", + "RegionNote": "Noktalar eklerken çokgenin kenarına tıklayın. Kaldırılacak bir noktayı sağ tıklayın.", + "Regions": "Bölgeler", + "Registered": "Kayıtlı", + "Registered Servers": "Kayıtlı sunucular", + "Remember Me": "Beni Hatırla", + "Request": "Rica etmek", + "Require Object to be in Region": "Nesnenin bölgede olmasını gerektirin", + "Reset": "Sıfırla", + "Reset Form": "Formu Sıfırla", + "Reset Timer": "Zamanlayıcı Sıfırla", + "Resolution": "Çözünürlük", + "Restart": "Tekrar başlat", + "Restart CRON": "Cron'u yeniden başlatın", + "Restart Core": "Çekirdeği yeniden başlatın", + "Restarting": "Yeniden başlatma", + "Restarting Process": "Yeniden başlatma işlemi", + "Retry Connection": "Bağlantıyı yeniden deneyin başarısız olmasına izin verildi ", + "Retrying...": "Yeniden deneme ...", + "Right": "Doğru URL adresi ", + "Right Stop": "Doğru durdurma url adresi ", + "Rotate": "Döndürmek", + "Rule": "Kural", + "Run Installer": "Yükleyiciyi çalıştır", + "S3-Based Network Storage": "S3 tabanlı ağ depolama", + "SFTP": "SFTP", + "SFTP (SSH File Transfer)": "SFTP (SSH dosya aktarımı)", + "SFTP Error": "SFTP hatası", + "Saturday": "Cumartesi", + "Save": "Kaydetmek", + "Save Changes": "Değişiklikleri Kaydet", + "Save Directory": "Dizini Kaydet", + "Save Events": "Etkinlikleri Kaydet", + "Save Events to SQL": "Etkinlikleri SQL'e kaydet", + "Save Frames to Events": "Çerçeveleri etkinliklere kaydet", + "Save Links to Database": "Veritabanına bağlantıları kaydet", + "Save Log in SQL": "Günlük kaydet sql Bu hızlı bir şekilde doldurulabilir. ", + "Save New": "Yeni Kaydet", + "Save as": "Farklı kaydet", + "Saved": "Kurtarılmış", + "Saved Filters": "Kaydedilen filtreler", + "Saved Logs": "Kaydedilen Günlükler", + "Saved Presets": "Kaydedilmiş Ön Ayarlar", + "Saved Schedules": "Kaydedilen Programlar", + "Scan Settings": "Ayarlar Tarama", + "Schedule": "Takvim", + "Schedule Configuration Not Found": "Çizelge yapılandırması bulunamadı", + "Schedules": "Programları", + "Search": "Aramak", + "Search Base": "Arama tabanı", + "Search Filter": "Arama filtresi", + "Search Images": "Resimler Arama", + "Search Settings": "Arama Ayarları", + "Second stream in feed": "Yemde ikinci akış", + "Secure": "Güvenli", + "Select a Monitor": "Bir Monitör Seçin", + "Select atleast one monitor to delete": "Silmek için en az bir monitör seçin.", + "Selected": "Seçilmiş", + "Send Frames": "Analiz edilecek çerçeveler push çerçeveleri gönderin", + "Send Notification": "Bildirim Gönder", + "Send to": "Gönderildi", + "Separate with commas, no spaces": "Virgülle ayrı, boşluk yok", + "September": "Eylül", + "Server URL": "Sunucu URL'si", + "Session Key": "Oturum Anahtarı", + "Set Home": "Eve koymak", + "Set Home Position (ONVIF-only)": "Ev pozisyonunu ayarlayın (sadece onvif)", + "Set Mode": "Set Modu", + "Set to Watch Only": "Sadece izlemeye hazır", + "Settings": "Ayarlar", + "Settings Changed": "Ayarlar değişti", + "SettingsChangedText": "Ayarlarınız kaydedildi ve uygulandı. Bazı ayarlar bu sayfanın yenilenmesini gerektirebilir.", + "Sharpness": "Keskinlik", + "Shinobi": "Shinobi", + "Shinobi Ordered to Update": "Shinobi güncellemesi tamamlandı", + "Shinobi Streamer": "Shinobi flama", + "ShinobiHub": "Shinobihub", + "Show Logs": "Günlükleri Göster", + "Show Matrices": "Matrisleri Göster", + "Show Matrix": "Matrisi Göster", + "Show Regions of Interest": "İlgilenilen bölgeleri göster", + "Show Stream HUD": "STREAM Stream HUD", + "Show Thumbnails in Video List": "Video listesinde küçük resimleri göster", + "Silent": "Sessiz", + "Simple": "Basit", + "Size (mb)": "Boyut (MB)", + "Skip Ping": "Ping atlamak", + "Snapshot": "Enstantane fotoğraf", + "Snapshot Flags": "Anlık görüntü bayrakları", + "Snapshots": "Anlık görüntüler", + "Sort By": "Göre sırala", + "Space Used": "Kullanılan Alan", + "Start": "Başlangıç", + "Start Recording": "Kayda başla", + "Start Time": "Başlangıç ​​saati", + "Start Time cannot be empty.": "Başlangıç zamanı boş olamaz.", + "Started": "Başlayan", + "Started Building": "İnşa etmeye başladı", + "Starting": "Başlangıç", + "State Configuration Not Found": "Durum yapılandırması bulunamadı", + "State Configuration has no monitors associated": "Durum yapılandırmasının ilişkili monitör yok", + "Status Changed": "Durum Değiştirildi", + "Status Indicator": "Durum Göstergesi", + "Stop": "Durmak", + "Stop Command": "Durdur komut", + "Stop URL": "URL'yi durdur", + "Stopped": "Durdu", + "Stopping": "Durduruluyor", + "Storage Location": "Depolama yeri", + "Storage Use": "Depolama kullanımı", + "Stream": "Aktarım", + "Stream Channel": "Akış kanalı", + "Stream Flags": "Akış bayrakları", + "Stream Key": "Akış", + "Stream Timestamp": "Akış zaman damgası", + "Stream Type": "Akış türü", + "Stream Watermark": "Stream Fuarmeri", + "Stream in Background": "Arka planda akış", + "Stream to YouTube": "YouTube'a akış", + "Stream to YouTube Flags": "YouTube bayraklarına akış", + "StreamText": "

Bu bölüm, akış ve ayarlarının birincil yöntemini belirleyecektir. Bu akış gösterge tablosunda görüntülenecektir. HLS, JPEG veya MJPEG kullanmayı seçerseniz, akışı diğer programlar aracılığıyla tüketebilirsiniz. çerçeveler.

", + "Streamed Logs": "Akışlı günlükler", + "Streamer": "Flama", + "Streams": "Canlı Yayınlar", + "Sub-Accounts": "Alt Hesaplar", + "Subdivision": "Alt bölüm", + "Substream": "Alt akım", + "Substream Process": "Alt akım işlemi", + "SubstreamNotConfigured": "Alt akım yapılandırılmadı. Monitör ayarlarınızı açın ve yapılandırın.", + "Subtitle": "Alt yazı", + "Success": "Başarı", + "Sunday": "Pazar", + "Superuser": "Süper", + "Superuser Logs": "Süper kullanıcı günlükleri", + "Switch on for Still Image": "Hareketsiz görüntü için aç", + "System": "Sistem", + "System Level": "Sistem seviyesi", + "TCP": "TCP", + "TV Channel": "TV kanalı", + "TV Channel Group": "TV kanalı grubu", + "TV Channel ID": "TV kanalı kimliği", + "Telegram": "Telgraf", + "Text Box Color": "Metin kutusu rengi", + "Text Color": "Metin rengi", + "Text criteria unsupported for Object Count tests, Ignoring Conditional": "Nesne sayımı testleri için desteklenmeyen metin kriterleri, koşullu göz ardı ederek", + "Themes": "Temalar", + "There are no monitors that you can view with this account.": "Bu hesapla görüntüleyebileceğiniz monitör yoktur.", + "Threads": "İş Parçacığı", + "Thumbnail": "Küçük resim", + "Thursday": "Perşembe", + "Time": "Zaman", + "Time Created": "Zaman yaratıldı", + "Time Left": "Kalan zaman", + "Time Occurred": "Zaman meydana geldi", + "Time-lapse": "Hızlandırılmış", + "Time-lapse Tool": "Hızlandırılmış aracı", + "TimeZone": "Saat dilimi", + "Timelapse": "Timelapse", + "Timelapse Frames Share": "Timelapse Frames Paylaşım", + "Timelapse Watermark": "Timelapse filigran", + "Timeout": "Zaman aşımı", + "Timeout Reset on Next Event": "Bir sonraki etkinlikte zaman aşımı sıfırlaması", + "Timeout Reset on Next Motion": "Sonraki Harekette Zaman Aşımı Sıfırlama", + "Timezone": "Saat dilimi", + "Timezone Offset": "Timezone ofseti", + "Title": "Başlık", + "Today": "Bugün", + "Toggle Sidebar": "Kenar çubuğu", + "Toggle Substream": "Alt akışı değiştirin", + "Token": "Jeton", + "Top Left": "Sol üst", + "Top Right": "Sağ üst", + "Traditional (Watch-Only, Includes Buffer)": "Geleneksel (yalnızca izleme, tampon içerir)", + "Traditional Recording": "Geleneksel kayıt", + "Traditional Recording Flags": "Geleneksel kayıt bayrakları", + "Train": "Tren", + "TrainConfirm": "Eğitime başlamak istediğinden emin misiniz? Bu, 500'den fazla görüntü ile 12 saatten fazla sürebilir. Bu, RAM ve/veya CPU gibi büyük miktarda kaynak tüketecektir.", + "TrainConfirmStop": "Eğitimi durdurmak istediğinden emin misiniz?", + "Trainer Engine": "Eğitmen motoru", + "Trigger Blocked": "Tetikle engellendi", + "Trigger Camera Groups": "Tetik Kamera Grupları", + "Trigger Event": "Tetikleyici olay", + "Trigger Group to Record": "Kayıt için tetikleme grubu", + "Trigger Record": "Tetik kaydı", + "Trigger Successful": "Tetikleyici Başarılı", + "Trigger Threshold": "Tetik eşiği", + "Tuesday": "Salı", + "Turn Speed": "Dönüş hızı", + "Type": "Tip", + "UDP": "UDP", + "URL": "Url", + "URL Stop Timeout": "URL Stop Timeout X milisaniye sonrası URL'yi çalıştırın ", + "US": "BİZ", + "UTCDateTime": "Tarih", + "Unable to Launch": "Başlatılamıyor", + "UnabletoLaunchText": "Lütfen önce yeni monitör kaydedin. Ardından bölge editörünü başlatmaya çalışın.", + "Uncommon Objects": "Nadir Nesneler", + "Uniform": "Üniforma", + "Unlink": "Bağırmak", + "Unlink Login": "Bağlantı girişi?", + "Unlinked": "Bağlantısız", + "Up": "Yukarı url adresi ", + "Up Stop": "UP DURDUR URL Adresi ", + "Update": "Güncelleme", + "Update to Development": "Geliştirme Güncellemesi", + "Update to Master": "Master'a Güncelleme", + "Upload Bandwidth": "Bant genişliği yükleyin", + "Upload File": "Dosya yükleme", + "Uploaded Only": "Yalnızca yüklendi", + "Uploaders": "Yükleyiciler", + "Use Built-In": "Yerleşik kullanın", + "Use Camera Timestamps": "Kamera zaman damgalarını kullanın", + "Use Global Amazon S3 Video Storage": "Global Amazon S3 video depolama alanı kullanın", + "Use Global Backblaze B2 Video Storage": "Global Backblaze B2 Video Depolama", + "Use Global Wasabi Hot Cloud Storage Video Storage": "Global Wasabi Sıcak Bulut Depolama Video Depolama", + "Use Global WebDAV Video Storage": "Global WebDAV video depolama alanı kullanın", + "Use HTML5 Play Method": "HTML5 Play Method kullanın", + "Use Max Storage Amount": "Maksimum depolama miktarını kullanın", + "Use Raw Snapshot": "Ham Anlık Görüntü kullanın", + "Use Substream": "Alt akışı kullanın", + "Use coProcessor": "Kopya kullanıcı kullanın", + "UseCount": "Usan sayısı", + "User Log": "Kullanıcı günlüğü", + "User Not Found": "Kullanıcı bulunamadı", + "Username": "Kullanıcı adı", + "VA-API": "VA-API", + "Value": "Değer", + "Video": "Video", + "Video Bit Rate": "Video bit hızı", + "Video Codec": "Video kodek", + "Video Configuration": "Video yapılandırması", + "Video Filter": "Video filtresi", + "Video Finished": "Video Bitti", + "Video Length (minutes) and Motion Count per video": "Video başına video uzunluğu (dakika) ve hareket sayısı", + "Video Limit": "Video sınırı", + "Video Record Rate": "Video kayıt oranı", + "Video Set": "Video seti", + "Video Share": "Video payı", + "Video Status": "Video durumu", + "Video and Time Span (Minutes)": "Video ve Zaman Süresi (Dakika)", + "Video stream only from first feed": "Video akışı yalnızca First Feed'den", + "Video streams only": "Yalnızca video akışları", + "Videos": "Videolar", + "Videos List": "Videolar listesi", + "Videos Merge": "Videolar birleştiriyor", + "Viewing Server Stats": "Sunucu İstatistiklerini Görüntüleme", + "Warning": "Uyarı", + "Wasabi Hot Cloud Storage": "Wasabi Sıcak Bulut Depolama", + "Wasabi Hot Cloud Storage Upload Error": "Wasabi sıcak bulut depolama yükleme hatası", + "Watch": "İzlemek", + "Watch Only": "Sadece izle", + "Watch-Only": "SADECE SATIRLI", + "Watching": "Seyretme", + "Web Page": "İnternet sayfası", + "WebDAV": "Webdav", + "WebM (libvpx)": "WebM (libvpx)", + "Webdav Error": "Webdav hatası", + "WebdavErrorTextCreatingDir": "Dizin oluşturulamıyor.", + "WebdavErrorTextTryCreatingDir": "Kaydedilemez. Dizin oluşturmaya çalışıyorum.", + "Webhook": "Webhook", + "Webhook URL": "Webhook URL'si", + "Websocket": "WebSocket", + "Websocket Connected": "WebSocket Bağlı", + "Websocket Disconnected": "WebSocket bağlantısı kesildi", + "Wednesday": "Çarşamba", + "Welcome": "Hoş geldin!", + "When Detector is Off": "Dedektör kapalıyken", + "When Detector is On": "Dedektör açıkken", + "WhiteBalance": "Beyaz dengesi", + "WideDynamicRange": "Geniş dinamik alan", + "Width": "Genişlik", + "X Point": "X Noktası", + "Y Point": "Y nokta", + "Yes": "Evet", + "Zip and Download": "Zip ve İndir", + "Zipping Videos": "Zil videoları", + "Zones": "Bölgeler", + "Zoom In": "Yakınlaştır", + "Zoom In Stop": "DURDURMA URL Adresi ", + "Zoom Out": "Uzaklaştır", + "Zoom Out Stop": "Yakınlaştırma Durdur url adresi ", + "a day": "bir gün", + "a few seconds": "birkaç saniye", + "a minute": "Bir dakika", + "a month": "bir ay", + "a year": "bir yıl", + "aac": "AAC", + "aac (Default)": "AAC (varsayılan)", + "ac3": "AC3", + "accountActionFailed": "Hesap eylemi başarısız oldu", + "accountAdded": "Hesap eklendi", + "accountAddedText": "Hesap eklendi.", + "accountDeleted": "Hesap silindi", + "accountDeletedText": "Hesap silindi.", + "accountId": "Hesap kimliği", + "accountSettingsDescription": "Profilinizi yönetin ve videoları tutmak için maksimum depolama miktarı ve maksimum gün sayısı gibi seçenekleri ayarlayın.", + "accountSettingsError": "Hesap Ayarları Hatası", + "activatedText": "Kurulumunuz etkinleştirildi.", + "ago": "evvel", + "airplane": "uçak", + "alreadyLinked": "Zaten bir hesaba bağlı", + "an hour": "bir saat", + "apple": "elma", + "applicationKey": "Uygulama anahtarı", + "aws_accessKeyId": "Anahtar Kimliği Erişim", + "aws_secretAccessKey": "Gizli erişim anahtarı", + "backpack": "sırt çantası", + "banana": "muz", + "baseball bat": "beysbol sopası", + "baseball glove": "beyzbol eldiveni", + "bear": "dayanmak", + "bed": "yatak", + "bench": "Bank", + "bicycle": "bisiklet", + "bindDN": "Binddn", + "bird": "kuş", + "blankPassword": "Aynı şifreyi tutmak için boş bırakın", + "boat": "bot", + "book": "kitap", + "bottle": "şişe", + "bowl": "tas", + "broccoli": "Brokoli", + "bus": "otobüs", + "cake": "Kek", + "car": "araba", + "carrot": "havuç", + "cat": "kedi", + "cell phone": "cep telefonu", + "chair": "sandalye", + "clientStreamFailedattemptingReconnect": "İstemci tarafı akış kontrolü başarısız oldu, yeniden bağlandı.", + "clock": "saat", + "coProcess Crashed for Monitor": "Monitör için kopya çöktü", + "coProcess Unexpected Exit": "Takipsiz çıkış", + "coProcessor": "kopyacı", + "coProcessor Started": "kopukluk başladı", + "coProcessor Stopped": "kopukluk durdu", + "coProcessorTextStarted": "Koproçör sadece CPU çıktıları için başladı.", + "coProcessorTextStopped": "kopukluk sona erdi.", + "codecMismatchText1": "Kameranız H.265 (HEVC) akış verileri sağlıyor ve akış bölümü için Video Codec olarak kopyayı kullanıyorsunuz. Shinobi'den akışınız bu kodek kullanamayan cihazlarda görünmeyebilir. Shinobi mobil uygulaması bu akışları görüntüleyebilir.", + "codecMismatchText2": "Seçtiğiniz Video Codec uygulanamaz. Kameranız MJPEG akış verileri sağlıyor ve akış bölümü için Video Codec olarak kopyalama kullanıyorsunuz. Akış tipini MJPEG olarak değiştirdi.", + "codecMismatchText3": "Seçtiğiniz Video Codec uygulanamaz. Kameranız MJPEG akış verileri sağlıyor ve kayıt bölümü için Video Codec olarak kopyalama kullanıyorsunuz. Video kodekini libx264 olarak değiştirdi.", + "confirmDeleteFilter": "Bu filtreyi silmek ister misiniz? Onu kurtaramazsın.", + "contactAdmin": "Shinobi kurulumunuzun bakıcısıyla iletişime geçin.", + "copy": "kopya", + "couch": "kanepe", + "cow": "inek", + "cuda": "CUDA (Nvidia NVENC)", + "cup": "Fincan", + "cuvid": "Cuvid (Nvidia nvenc)", + "days": "günler", + "deleteApiKey": "API anahtarını sil", + "deleteApiKeyText": "Bu API anahtarını silmek istiyor musunuz? Onu kurtaramazsın.", + "deleteMonitorStateText1": "Bu monitör durumlarının ön ayarını silmek istiyor musunuz? İlişkili monitör yapılandırmaları geri kazanılamaz.", + "deleteMonitorStateText2": "Bu monitörün ön ayarını silmek istiyor musunuz?", + "deleteScheduleText": "Bu programı silmek ister misiniz? İlişkili Monitör Ön Setleri değiştirilmeyecek ..", + "deleteSubAccount": "Alt Hesabını Sil", + "deleteSubAccountText": "Bu alt hesap silmek ister misiniz? Onu kurtaramazsın.", + "dining table": "yemek masası", + "dog": "köpek", + "donut": "tatlı çörek", + "drm": "DRM Nesne Paylaşımı", + "dropBoxSuccess": "Başarı! Dropbox'ınıza kaydedilen dosyalar.", + "dxva2": "DXVA2 (DirectX Video, Windows)", + "elephant": "fil", + "eventFilterActionText": "Bunlar, başarılı olan filtre koşullarından meydana gelen eylemlerdir. \"Orijinal Seçim\", monitörünüzün ayarlarında seçtiğiniz seçeneği ifade eder.", + "eventFilterErrorBrackets": "Ever sayıda paranteziniz var. Göz ardı ediliyorlar.", + "eventFiltersDescription": "Olaylar meydana geldiğinde kurulum filtreleri.", + "failedLoginText1": "Çok kez giriş yapamadınız. Tekrar denemeden önce 15 dakika beklemelisiniz.", + "failedLoginText2": "Lütfen giriş bilgilerinizi kontrol edin.", + "fieldMissingValueText1": "Kameranız MJPEG akış verileri sağlıyor. Monitör yakalama oranını ayarlamanız gerekir. Shinobi onu tespit etmeye ve otomatik olarak doldurmaya çalışacaktır.", + "fieldTextAccelerator": "Akışların çözülmesi için donanım ivmesi (HWACCEL).", + "fieldTextAcodec": "Kayıt için ses kodek.", + "fieldTextActionsCommand": "Bunu komutta bir komut dosyasını tetiklemek için kullanabilirsiniz.", + "fieldTextActionsHalt": "Etkinliğin hiç olmaması gibi hiçbir şey yapmamasını sağlayın.", + "fieldTextActionsIndifference": "Olay için gerekli minimum kayıtsızlığı değiştirin.", + "fieldTextActionsRecord": "Global Algılama Ayarları bölümünde şu anda ayarlanan seçenekleriyle geleneksel kayıt, hotswap veya Siliness'i kullanın.", + "fieldTextAduration": "Girişi problamak için kaç mikrosaniye analiz edildiğini belirtin. RTSP kullanıyorsanız ve akış sorunları varsa 100000'e ayarlayın.", + "fieldTextAudioAlert": "Olay meydana geldiğinde ses.", + "fieldTextAudioDelay": "Bir daha bir olay bir uyarı başlatana kadar gecikme. Saniyeler içinde ölçülür.", + "fieldTextAudioNote": "Bilgi balonu göründüğünde ses.", + "fieldTextAutoHost": "Tam akış URL'si.", + "fieldTextAutoHostEnable": "Bir akış URL'si oluşturmak veya tam URL sağlamak için gereken ayrı parçaları besleyin ve Shinobi'nin sizin için ayrıştırmasına izin verin.", + "fieldTextChannelHlsListSize": "Eski segmentleri otomatik olarak silmeden önce maksimum segment sayısı.", + "fieldTextChannelHlsTime": "Her video segmentinin ne kadar süre olması gerektiği, dakikalar içinde. Her segment istemci tarafından bir M3U8 dosyası aracılığıyla çizilecektir. Daha kısa segmentler daha az yer alır.", + "fieldTextChannelPresetStream": "Belirli video kodlayıcıları için önceden ayarlanmış bayrak. Kameranızın birkaç saniyede bir çöktüğünü görürseniz: boş bırakmayı deneyin.", + "fieldTextChannelStreamAcodec": "Akış için ses kodek.", + "fieldTextChannelStreamAcodecAac": "MP4 video için kullanılır.", + "fieldTextChannelStreamAcodecAc3": "MP4 video için kullanılır.", + "fieldTextChannelStreamAcodecAuto": "FFMPEG'nin seçmesine izin verin.", + "fieldTextChannelStreamAcodecCopy": "MP4 video için kullanılır. Çok düşük CPU kullanımı vardır, ancak bazı ses kodekleri AAC için -Strict 2 gibi özel bayraklara ihtiyaç duyar.", + "fieldTextChannelStreamAcodecLibmp3lame": "MP4 video için kullanılır.", + "fieldTextChannelStreamAcodecLibopus": "WebM videosu için kullanılır.", + "fieldTextChannelStreamAcodecLibvorbis": "WebM videosu için kullanılır.", + "fieldTextChannelStreamAcodecNoAudio": "Ses yok, bu yasal nedenlerden dolayı dünyanın bazı bölgelerinde ayarlanması gereken bir seçenektir.", + "fieldTextChannelStreamFps": "Çerçevelerin saniyede kareler halinde müşterilere görüntülenme hızı. Varsayılan olmadığını unutmayın. Bu, yüksek bant genişliği kullanımına yol açabilir.", + "fieldTextChannelStreamQuality": "Düşük sayı daha yüksek kalite anlamına gelir. Daha yüksek sayı daha az kalite anlamına gelir.", + "fieldTextChannelStreamRotate": "Video akışının görüntüleme açısını değiştirin.", + "fieldTextChannelStreamScaleX": "İşlemden sonra çıktı olan akış görüntüsünün genişliği.", + "fieldTextChannelStreamScaleY": "İşlemden sonra çıktı olan akış görüntüsünün yüksekliği.", + "fieldTextChannelStreamType": "Video akışını tüketmek için kullanılacak yöntem.", + "fieldTextChannelStreamTypeFLV": "WebSocket üzerinden FLV kodlu çerçeveler gönderme.", + "fieldTextChannelStreamTypeHLS(includesAudio)": "Facebook canlı akışlarına benzer bir yöntem. Ses içerir Giriş sağlarsa. Yaklaşık 4-6 saniyelik bir gecikme vardır, çünkü bu yöntem segmentleri kaydeder, daha sonra onları oluştururken itmek yerine müşteriye iter.", + "fieldTextChannelStreamTypeMJPEG": "Standart hareket JPEG görüntüsü. Ses yok.", + "fieldTextChannelStreamTypePoseidon": "Poseidon, Kevin Godell'ın MP4 işleme kodu üzerine kurulmuştur. Bir akış MP4 dosyasını simüle eder, ancak canlı akışın verilerini kullanarak. Ses içerir. Bazı tarayıcılar normal bir MP4 dosyası gibi oynayabilir. HTTP veya WebSocket üzerinden akışlar.", + "fieldTextChannelStreamVcodec": "Akış için video kodek.", + "fieldTextChannelStreamVcodecAuto": "FFMPEG'nin seçmesine izin verin.", + "fieldTextChannelStreamVcodecCopy": "MP4 video için kullanılır. Çok düşük CPU kullanımı vardır, ancak video filtreleri kullanamaz ve dosya boyutları devasa olabilir. Bu seçeneği kullanırken MP4 ayarları kamera tarafınızı ayarlamak için en iyisi.", + "fieldTextChannelStreamVcodecLibx264": "MP4 video için kullanılır.", + "fieldTextChannelStreamVcodecLibx265": "MP4 video için kullanılır.", + "fieldTextChannelSvf": "Akış bölümünü etkilemek için FFMPEG video filtrelerini bu kutuya yerleştirin. Boşluksuz.", + "fieldTextControlInvertY": "Çünkü kameranız baş aşağı monte edildiğinde veya ters dikey kontroller kullandığında.", + "fieldTextCrf": "Düşük sayı daha yüksek kalite anlamına gelir. Daha yüksek sayı daha az kalite anlamına gelir.", + "fieldTextCustDetect": "Analizasyon için akarsu dedektörüne bağlanan özel bayraklar.", + "fieldTextCustDetectObject": "Analizasyon için akarsu dedektörüne bağlanan özel bayraklar.", + "fieldTextCustInput": "FFMPEG işleminin girişine bağlanan özel bayraklar.", + "fieldTextCustRecord": "FFMPEG işleminin kaydına bağlanan özel bayraklar.", + "fieldTextCustSipRecord": "Olay tabanlı kayıtların sifonundan kaynaklanan çıktıya bağlanan özel bayraklar.", + "fieldTextCustSnap": "Anlık görüntülere bağlanan özel bayraklar.", + "fieldTextCustStream": "FFMPEG işleminin akışına (istemci taraf görünümü) bağlanan özel bayraklar.", + "fieldTextCustomOutput": "JPEG çerçeveleri gibi özel bir çıktı ekleyin veya doğrudan başka bir sunucuya veri gönderin.", + "fieldTextCutoff": "Dakikalar içinde. Ne zaman dilimlenir ve yeni bir video dosyası başlatılır.", + "fieldTextDays": "Temizlemeden önce videoları saklamak için gün sayısı.", + "fieldTextDetailSubstreamInputRtspTransportAuto": "FFMPEG'nin karar vermesine izin verin. Normalde önce UDP'yi deneyecektir.", + "fieldTextDetailSubstreamInputRtspTransportTCP": "UDP istenmeyen sonuçlar vermeye başlarsa buna ayarlayın.", + "fieldTextDetailSubstreamInputRtspTransportUDP": "FFMPEG bunu önce deniyor.", + "fieldTextDetailSubstreamOutputHlsListSize": "Eski segmentleri otomatik olarak silmeden önce maksimum segment sayısı.", + "fieldTextDetailSubstreamOutputHlsTime": "Her video segmentinin ne kadar süre olması gerektiği, dakikalar içinde. Her segment istemci tarafından bir M3U8 dosyası aracılığıyla çizilecektir. Daha kısa segmentler daha az yer alır.", + "fieldTextDetailSubstreamOutputPresetStream": "Belirli video kodlayıcıları için önceden ayarlanmış bayrak. Kameranızın birkaç saniyede bir çöktüğünü görürseniz: boş bırakmayı deneyin.", + "fieldTextDetailSubstreamOutputStreamAcodec": "Akış için ses kodek.", + "fieldTextDetailSubstreamOutputStreamAcodecAac": "MP4 video için kullanılır.", + "fieldTextDetailSubstreamOutputStreamAcodecAc3": "MP4 video için kullanılır.", + "fieldTextDetailSubstreamOutputStreamAcodecAuto": "FFMPEG'nin seçmesine izin verin.", + "fieldTextDetailSubstreamOutputStreamAcodecCopy": "MP4 video için kullanılır. Çok düşük CPU kullanımı vardır, ancak bazı ses kodekleri AAC için -Strict 2 gibi özel bayraklara ihtiyaç duyar.", + "fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame": "MP4 video için kullanılır.", + "fieldTextDetailSubstreamOutputStreamAcodecLibopus": "WebM videosu için kullanılır.", + "fieldTextDetailSubstreamOutputStreamAcodecLibvorbis": "WebM videosu için kullanılır.", + "fieldTextDetailSubstreamOutputStreamAcodecNoAudio": "Ses yok, bu yasal nedenlerden dolayı dünyanın bazı bölgelerinde ayarlanması gereken bir seçenektir.", + "fieldTextDetailSubstreamOutputStreamFps": "Çerçevelerin saniyede kareler halinde müşterilere görüntülenme hızı. Varsayılan olmadığını unutmayın. Bu, yüksek bant genişliği kullanımına yol açabilir.", + "fieldTextDetailSubstreamOutputStreamQuality": "Düşük sayı daha yüksek kalite anlamına gelir. Daha yüksek sayı daha az kalite anlamına gelir.", + "fieldTextDetailSubstreamOutputStreamRotate": "Video akışının görüntüleme açısını değiştirin.", + "fieldTextDetailSubstreamOutputStreamScaleX": "İşlemden sonra çıktı olan akış görüntüsünün genişliği.", + "fieldTextDetailSubstreamOutputStreamScaleY": "İşlemden sonra çıktı olan akış görüntüsünün yüksekliği.", + "fieldTextDetailSubstreamOutputStreamType": "Video akışını tüketmek için kullanılacak yöntem.", + "fieldTextDetailSubstreamOutputStreamVcodec": "Akış için video kodek.", + "fieldTextDetailSubstreamOutputStreamVcodecAuto": "FFMPEG'nin seçmesine izin verin.", + "fieldTextDetailSubstreamOutputStreamVcodecCopy": "MP4 video için kullanılır. Çok düşük CPU kullanımı vardır, ancak video filtreleri kullanamaz ve dosya boyutları devasa olabilir. Bu seçeneği kullanırken MP4 ayarları kamera tarafınızı ayarlamak için en iyisi.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx264": "MP4 video için kullanılır.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx265": "MP4 video için kullanılır.", + "fieldTextDetailSubstreamOutputSvf": "Akış bölümünü etkilemek için FFMPEG video filtrelerini bu kutuya yerleştirin. Boşluksuz.", + "fieldTextDetector": "Bu, hareket dedektörü için FFMPEG komutuna başka bir çıktı ekleyecektir.", + "fieldTextDetectorAudio": "Bir certiain kararsızında ses olup olmadığını kontrol edin. Düzeltme okuması gerçek dünya ölçümü için doğru olmayabilir.", + "fieldTextDetectorBufferHlsListSize": "Eski segmentleri otomatik olarak silmeden önce maksimum segment sayısı.", + "fieldTextDetectorBufferHlsTime": "Her video segmentinin saniyeler içinde ne kadar olması gerektiği. Her segment istemci tarafından bir M3U8 dosyası aracılığıyla çizilecektir. Daha kısa segmentler daha az yer alır.", + "fieldTextDetectorColorThreshold": "Hareket olarak kabul edilmeden önce bir pikselde izin verilen fark miktarı.", + "fieldTextDetectorCommand": "Çalışacak komut. Bu, terminalden bir kabuk komutu çalıştırmanın eşdeğeridir.", + "fieldTextDetectorCommandTimeout": "Bu değer, betiğinizin bir sonraki çalışmasına izin veren bir zamanlayıcıdır. Bu değer dakikalar halinde.", + "fieldTextDetectorFps": "Hareket dedektörüne göndermek için saniyede kaç kare; 2 varsayılandır.", + "fieldTextDetectorFrame": "Bu, piksel farklılıkları için tüm çerçeveyi okuyacaktır. Bu, tüm ekranı kapsayan bir bölge oluşturmakla aynıdır.", + "fieldTextDetectorHttpApi": "Bu kameraya HTTP tetikleyicilerine izin vermek ister misiniz?", + "fieldTextDetectorLisencePlate": "Plaka tanımasını etkinleştirin. OpenAlpr eklentisi her zaman etkinleştirildi.", + "fieldTextDetectorLisencePlateCountry": "Tanıma plaka türünü seçin. Şu anda sadece biz ve AB destekleniyor.", + "fieldTextDetectorLockTimeout": "Veritabanının aşırı yüklenmesini ve istemcileri almayı önlemek için bir sonraki tetikleyiciye izin verildiğinde kilitleme. Milisaniye cinsinden ölçülür.", + "fieldTextDetectorMaxSensitivity": "Hareket güven derecesi, tetikleyici olarak görülmek için bu değerden daha düşük olmalıdır. Maksimum olmadan boş bırakın. Bu seçenek daha önce \"Max Kayıtsızlık\" olarak adlandırılmıştır.", + "fieldTextDetectorNoiseFilter": "Belirli bir kayıtsızlıkta tahıl veya tekrarlanan hareketi filtrelemeye çalışın.", + "fieldTextDetectorNoiseFilterRange": "Hareket olarak kabul edilmeden önce bir pikselde izin verilen fark miktarı.", + "fieldTextDetectorNotrigger": "Hareketin bir aralıkta meydana gelip gelmediğini kontrol edin. Hareket meydana geldiyse, çek sıfırlanacaktır.", + "fieldTextDetectorNotriggerCommand": "Çalışacak komut. Bu, terminalden bir kabuk komutu çalıştırmanın eşdeğeridir.", + "fieldTextDetectorNotriggerCommandTimeout": "Bu değer, betiğinizin bir sonraki çalışmasına izin veren bir zamanlayıcıdır. Bu değer dakikalar halinde.", + "fieldTextDetectorNotriggerDiscord": "Zaman aşımı döneminden sonra hareket tespit edilmediyse, bir anlaşmazlık bildirimi alacaksınız.", + "fieldTextDetectorNotriggerTimeout": "Zaman aşımı dakikalar içinde hesaplanır.", + "fieldTextDetectorNotriggerWebhook": "Etkinlikten bazı değerlerle bir URL'ye GET isteği gönderin.", + "fieldTextDetectorObjCount": "Tespit edilen nesneleri say.", + "fieldTextDetectorObjCountInRegion": "Nesneleri yalnızca bölgelerde say.", + "fieldTextDetectorPam": "Kevin Godell'ın hareket dedektörünü kullanın. Bu, Shinobi'ye yerleştirilmiştir ve etkinleştirmek için başka bir yapılandırma gerektirmez.", + "fieldTextDetectorPtzFollow": "PTZ ile tespit edilen en büyük nesneyi takip edin? Olaylarla birlikte çalışan bir nesne dedektörü veya matris gerektirir.", + "fieldTextDetectorRecordMethod": "Hareket gibi bir olay meydana geldiğinde kayda başlamanın birçok yolu vardır. Geleneksel kayıt en kullanıcı dostudur.", + "fieldTextDetectorSave": "SQL'de hareket etkinliklerini kaydedin. Bu, güç görüntüleyicisinde meydana gelen zaman hareketi olayları sırasında video üzerinden hareketin görüntülenmesine izin verecektir.", + "fieldTextDetectorScaleX": "Tespit edilen görüntünün genişliği. Daha küçük boyutlar daha az CPU alır.", + "fieldTextDetectorScaleY": "Tespit edilen görüntünün yüksekliği. Daha küçük boyutlar daha az CPU alır.", + "fieldTextDetectorSendFrames": "Analiz edilecek çerçeveleri bağlı eklentiye itin.", + "fieldTextDetectorSendFramesObject": "Analiz edilecek çerçeveleri bağlı eklentiye itin.", + "fieldTextDetectorSendVideoLength": "Saniyeler içinde. E -posta veya anlaşmazlık gibi bildirim hizmetinize gönderilen videonun uzunluğu.", + "fieldTextDetectorSensitivity": "Hareket güven derecesi bir tetikleyici olarak görülmek için bu değeri aşmalıdır. Bu sayı, hareket dedektörü tarafından döndürülen güven derecesi ile doğrudan ilişkilidir. Bu seçenek daha önce \"kayıtsızlık\" olarak adlandırılmıştır.", + "fieldTextDetectorThreshold": "Bir hareket olayını ateşlemek için minimum algılama sayısı. Tespitler dedektörün içinde eşiğin dedektör fps saniyesine bölünmesi gerekir. Örneğin, dedektör FPS 2 ise ve tetik eşiği 3 ise, bir hareket olayını tetiklemek için 1.5 saniye içinde üç algılama yapılmalıdır. Bu eşik algılama bölgesidir.", + "fieldTextDetectorTimeout": "\"Tetik kaydı\" süresinin uzunluğu için çalışacaktır. Bu dakikalar içinde okunur.", + "fieldTextDetectorTrigger": "Bu, bir olay algılandığında kameraya \"sadece izleme\" olarak ayarlanırsa kaydedilmesini emreder.", + "fieldTextDetectorUseDetectObject": "Bağlı eklentiye göndermek için çerçeveler oluşturun.", + "fieldTextDetectorWebhook": "Etkinlikten bazı değerlerle bir URL'ye GET isteği gönderin.", + "fieldTextDetectorWebhookTimeout": "Bu değer, webhook'unuzun bir sonraki çalışmasına izin veren bir zamanlayıcıdır. Bu değer dakikalar halinde.", + "fieldTextDir": "Kaydedilen dosyaların kaydedileceği yeri. addStorage değişkeniyle daha fazla konum yapılandırabilirsiniz.", + "fieldTextEventDays": "Temizlemeden önce olayları saklamak için gün sayısı.", + "fieldTextEventMonPop": "Bir olay oluştuğunda monitör akışını açın.", + "fieldTextEventRecordScaleX": "İşlemden sonra çıktı olan olay tabanlı kayıt görüntüsünün genişliği.", + "fieldTextEventRecordScaleY": "İşlemden sonra çıktı olan olay tabanlı kayıt görüntüsünün yüksekliği.", + "fieldTextExt": "Kaydedilen video dosyanız için dosya türü.", + "fieldTextExtMP4": "Bu dosya türü oynanabilir, mobil içeren neredeyse tüm modern web tarayıcılarıdır. Kaliteyi düşürmediğiniz sürece dosya boyutu daha büyük olma eğilimindedir.", + "fieldTextExtWebM": "Küçük dosya boyutu, düşük istemci uyumluluğu. YouTube gibi sitelere yükleme için iyi.", + "fieldTextFactorAuth": "Etkin yöntemlerden biri aracılığıyla giriş için ikincil bir gereksinimi etkinleştirin.", + "fieldTextFatalMax": "Monitörü devre dışı bırakmadan önce sunucu ve kamera arasındaki ağ bağlantısı için yeniden deneme sayısı. Ondalık değil. Sonsuza dek yeniden denemek için 0 olarak ayarlayın.", + "fieldTextFps": "Çerçevelerin dosyalara, saniyede çerçevelere kaydedilme hızı. Varsayılan olmadığını unutmayın. Bu büyük dosyalara yol açabilir. Bu kamerayı ayarlamak için en iyisi.", + "fieldTextHeight": "Akış görüntüsünün yüksekliği.", + "fieldTextHlsListSize": "Eski segmentleri otomatik olarak silmeden önce maksimum segment sayısı.", + "fieldTextHlsTime": "Her video segmentinin ne kadar süre olması gerektiği, dakikalar içinde. Her segment istemci tarafından bir M3U8 dosyası aracılığıyla çizilecektir. Daha kısa segmentler daha az yer alır.", + "fieldTextHost": "Bağlantı adresi", + "fieldTextHwaccel": "Kod çözme motoru", + "fieldTextHwaccelVcodec": "Kod çözme motoru", + "fieldTextInverseTrigger": "Belirtilen bölgelerin dışını tetiklemek için. Tam çerçeve algılama etkinken tetiklenmez.", + "fieldTextIp": "Menzil veya tek", + "fieldTextIrCutFilterAuto": "IR kesme filtresi cihaz tarafından otomatik olarak etkinleştirilir.", + "fieldTextIrCutFilterOff": "IR Kesme Fiter'i devre dışı bırakın. Tipik olarak gece modu.", + "fieldTextIrCutFilterOn": "IR Kesme Fiter'i etkinleştirin. Tipik olarak gündüz modu.", + "fieldTextIsOnvif": "Bu ONVIF uyumlu bir kamera mı?", + "fieldTextLang": "Metin öğelerinin birincil dili. Tam çeviri için dilinizi conf.json'a ekleyin.", + "fieldTextLogDays": "Tasfiye etmeden önce günlükleri saklamak için gün sayısı.", + "fieldTextLoglevel": "İşi yaparken sağlanacak veri miktarı.", + "fieldTextLoglevelAllWarnings": "Tüm uyarıları görüntüleyin. Kameranızda neyin yanlış olduğunu bulamıyorsanız bunu kullanın.", + "fieldTextLoglevelFatal": "Yalnızca ölümcül hatalar görüntüleyin.", + "fieldTextLoglevelOnError": "Tüm önemli hataları görüntüleyin. Not: Bu her zaman önemli bilgileri göstermez.", + "fieldTextLoglevelSilent": "Hiçbiri. Bu tüm günlüğü susturur.", + "fieldTextMail": "Hesaplar için giriş. Ana hesap sahibinin e -posta adresi bildirim alacaktır.", + "fieldTextMapRtspTransportAuto": "FFMPEG'nin karar vermesine izin verin. Normalde önce UDP'yi deneyecektir.", + "fieldTextMapRtspTransportTCP": "UDP istenmeyen sonuçlar vermeye başlarsa buna ayarlayın.", + "fieldTextMapRtspTransportUDP": "FFMPEG bunu önce deniyor.", + "fieldTextMaxKeepDays": "Özellikle bu monitör için temizlenmeden önce videoları saklamak için gün sayısı.", + "fieldTextMid": "Bu, monitör için değiştirilemez bir tanımlayıcıdır. Monitör kimliğini çift tıklayıp değiştirerek bir monitörü çoğaltabilirsiniz.", + "fieldTextMode": "Bu, monitörün birincil görevidir.", + "fieldTextModeDisabled": "Aktif olmayan monitör, bu modda hiçbir işlem oluşturulmayacaktır.", + "fieldTextModeRecord": "Sürekli kayıt. Segmentler varsayılan olarak her 15 dakikada bir yapılır.", + "fieldTextModeWatchOnly": "Monitör yalnızca akış yapar, API veya dedektör tarafından aksi belirtilmedikçe kayıt gerçekleşmez.", + "fieldTextMpass": "Kameranızın şifresi", + "fieldTextMuser": "Kameranız için kullanıcı girişi", + "fieldTextName": "Bu, monitör için insan tarafından okunabilir görüntüleme adıdır.", + "fieldTextNotes": "Bu kamera için ayrılmak istediğiniz yorumlar.", + "fieldTextOnvifNonStandard": "Bu standart olmayan bir onvif kamera mı?", + "fieldTextOnvifPort": "ONVIF genellikle 8000 bağlantı noktasında çalıştırılır. Bu, kamera modelinize bağlı olarak 80 olabilir.", + "fieldTextPass": "Ayarlar değişikliği sırasında aynı şifreyi tutmak için boş bırakın.", + "fieldTextPasswordAgain": "Değiştirmek istiyorsanız şifre alanını eşleştirmelidir.", + "fieldTextPath": "Kameranıza giden yol", + "fieldTextPort": "Virgül veya menzil ile ayrı", + "fieldTextPortForce": "Varsayılan Web bağlantı noktasını kullanmak, RTSP gibi akışlar için otomatik geçiş yapmasına izin verebilir.", + "fieldTextPresetRecord": "Belirli video kodlayıcıları için önceden ayarlanmış bayrak. Kameranızın birkaç saniyede bir çöktüğünü görürseniz: boş bırakmayı deneyin.", + "fieldTextPresetStream": "Belirli video kodlayıcıları için önceden ayarlanmış bayrak. Kameranızın birkaç saniyede bir çöktüğünü görürseniz: boş bırakmayı deneyin.", + "fieldTextProbesize": "Giriş için analizleme probunun ne kadar büyük yapabileceğini belirtin. RTSP kullanıyorsanız ve akış sorunları varsa 100000'e ayarlayın.", + "fieldTextProtocol": "Video akışını tüketmek için kullanılacak protokol.", + "fieldTextRecordScaleX": "Akış görüntüsünün genişliği.", + "fieldTextRecordScaleY": "Akış görüntüsünün yüksekliği.", + "fieldTextRecordTimelapse": "JPEG tabanlı bir Timelapse oluşturun.", + "fieldTextRecordTimelapseMp4": "Timelapse için her günün sonunda bir MP4 dosyası oluşturun.", + "fieldTextRecordTimelapseWatermark": "Kaydedilen videonun çerçevelerine yakılan bir görüntü.", + "fieldTextRecordTimelapseWatermarkLocation": "Filigran olarak kullanılacak görüntü konumu.", + "fieldTextRecordTimelapseWatermarkPosition": "Kaydedilen videonun çerçevelerine yakılan bir görüntü.", + "fieldTextRotate": "Video akışının kayıt açısını değiştirin.", + "fieldTextRtmpKey": "RTMP bağlantı noktasında gelen akışlar için akış anahtarı.", + "fieldTextRtspTransport": "Kameranızın kullanacağı taşıma protokolü. TCP genellikle en iyi seçimdir.", + "fieldTextRtspTransportAuto": "FFMPEG'nin karar vermesine izin verin. Normalde önce UDP'yi deneyecektir.", + "fieldTextRtspTransportHTTP": "Standart bağlantı yöntemi.", + "fieldTextRtspTransportTCP": "UDP istenmeyen sonuçlar vermeye başlarsa buna ayarlayın.", + "fieldTextRtspTransportUDP": "FFMPEG bunu önce deniyor.", + "fieldTextSfps": "Kameranın akışını sağladığı kare hızını (FPS) belirtin.", + "fieldTextSignalCheck": "Müşteriniz, canlı olup olmadığını görmek için akışı ne sıklıkla kontrol eder. Bu dakikalar içinde hesaplanır.", + "fieldTextSignalCheckLog": "Bu sadece müşteri tarafı içindir. İstemci tarafı sinyal kontrolleri meydana geldiğinde günlük iş parçacığında görüntülenir.", + "fieldTextSize": "SHINOBI disk alanı miktarı, temizlemeden önce tüketilmesine izin verecektir. Bu değer megabaytlarda okunur.", + "fieldTextSizeFilebinPercent": "FileBin Arşivinin kullanabileceği maksimum depolama miktarının yüzdesi.", + "fieldTextSizeTimelapsePercent": "Timelapse çerçevelerinin kaydedilebileceği maksimum depolama miktarının yüzdesi.", + "fieldTextSizeVideoPercent": "Videoların kaydedebileceği maksimum depolama miktarının yüzdesi.", + "fieldTextSkipPing": "Bir monitör işlemi başlatılmadan önce başarılı bir ping gerekli olup olmadığını seçin.", + "fieldTextSnap": "JPEG'deki en son çerçeveyi alın.", + "fieldTextSnapSecondsInward": "saniyeler içinde", + "fieldTextSqllog": "FFMPEG, çok fazla veritabanı satırına yol açabilecek gereksiz verileri atmayı sevdiği için bunu dikkatle kullanın.", + "fieldTextSqllogNo": "Hayır varsayılandır.", + "fieldTextSqllogYes": "Bunu sadece tekrar eden sorunlar yaşıyorsanız yapın.", + "fieldTextStreamAcodec": "Akış için ses kodek.", + "fieldTextStreamAcodecAac": "MP4 video için kullanılır.", + "fieldTextStreamAcodecAc3": "MP4 video için kullanılır.", + "fieldTextStreamAcodecAuto": "FFMPEG'nin seçmesine izin verin.", + "fieldTextStreamAcodecCopy": "MP4 video için kullanılır. Çok düşük CPU kullanımı vardır, ancak bazı ses kodekleri AAC için -Strict 2 gibi özel bayraklara ihtiyaç duyar.", + "fieldTextStreamAcodecLibmp3lame": "MP4 video için kullanılır.", + "fieldTextStreamAcodecLibopus": "WebM videosu için kullanılır.", + "fieldTextStreamAcodecLibvorbis": "WebM videosu için kullanılır.", + "fieldTextStreamAcodecNoAudio": "Ses yok, bu yasal nedenlerden dolayı dünyanın bazı bölgelerinde ayarlanması gereken bir seçenektir.", + "fieldTextStreamFlvType": "Bu sadece Shinobi Gösterge Paneli içindir. Her iki akış yöntemi hala aktif ve kullanıma hazır.", + "fieldTextStreamFps": "Çerçevelerin saniyede kareler halinde müşterilere görüntülenme hızı. Varsayılan olmadığını unutmayın. Bu, yüksek bant genişliği kullanımına yol açabilir.", + "fieldTextStreamLoop": "Dosya akışı canlı bir akış gibi davranması için statik bir dosyayı döndürün.", + "fieldTextStreamQuality": "Düşük sayı daha yüksek kalite anlamına gelir. Daha yüksek sayı daha az kalite anlamına gelir.", + "fieldTextStreamRotate": "Video akışının görüntüleme açısını değiştirin.", + "fieldTextStreamScaleX": "İşlemden sonra çıktı olan akış görüntüsünün genişliği.", + "fieldTextStreamScaleY": "İşlemden sonra çıktı olan akış görüntüsünün yüksekliği.", + "fieldTextStreamTimestamp": "Video akışının çerçevelerine yakılan bir saat.", + "fieldTextStreamTimestampBoxColor": "Timstamp zemin rengi.", + "fieldTextStreamTimestampColor": "Timstamp metin rengi.", + "fieldTextStreamTimestampFont": "Zaman damganızı şekillendirmek için yazı tipi dosyası.", + "fieldTextStreamTimestampFontSize": "Pt.", + "fieldTextStreamTimestampX": "Zaman damgasının ufiztonal konumu", + "fieldTextStreamTimestampY": "Zaman damgasının dikey konumu", + "fieldTextStreamType": "Video akışını tüketmek için kullanılacak yöntem.", + "fieldTextStreamTypeBase64OverWebsocket": "Base64 kodlu çerçeveleri WebSocket üzerinden gönderme. Bu önbelleklemeyi önler, ancak ses yoktur.", + "fieldTextStreamTypeFLV": "WebSocket üzerinden FLV kodlu çerçeveler gönderme.", + "fieldTextStreamTypeHLS(includesAudio)": "Facebook canlı akışlarına benzer bir yöntem. Ses içerir Giriş sağlarsa. Yaklaşık 4-6 saniyelik bir gecikme vardır, çünkü bu yöntem segmentleri kaydeder, daha sonra onları oluştururken itmek yerine müşteriye iter.", + "fieldTextStreamTypeMJPEG": "Standart hareket JPEG görüntüsü. Ses yok.", + "fieldTextStreamTypePoseidon": "Poseidon, Kevin Godell'ın MP4 işleme kodu üzerine kurulmuştur. Bir akış MP4 dosyasını simüle eder, ancak canlı akışın verilerini kullanarak. Ses içerir. Bazı tarayıcılar normal bir MP4 dosyası gibi oynayabilir. HTTP veya WebSocket üzerinden akışlar.", + "fieldTextStreamVcodec": "Akış için video kodek.", + "fieldTextStreamVcodecAuto": "FFMPEG'nin seçmesine izin verin.", + "fieldTextStreamVcodecCopy": "MP4 video için kullanılır. Çok düşük CPU kullanımı vardır, ancak video filtreleri kullanamaz ve dosya boyutları devasa olabilir. Bu seçeneği kullanırken MP4 ayarları kamera tarafınızı ayarlamak için en iyisi.", + "fieldTextStreamVcodecLibx264": "MP4 video için kullanılır.", + "fieldTextStreamVcodecLibx265": "MP4 video için kullanılır.", + "fieldTextStreamVf": "Akış bölümünü etkilemek için FFMPEG video filtrelerini bu kutuya yerleştirin. Boşluksuz.", + "fieldTextStreamWatermark": "Video akışının çerçevelerine yakılan bir görüntü.", + "fieldTextStreamWatermarkLocation": "Filigran olarak kullanılacak görüntü konumu.", + "fieldTextStreamWatermarkPosition": "Video akışının çerçevelerine yakılan bir görüntü.", + "fieldTextTimestamp": "Kaydedilen videonun çerçevelerine yakılan bir saat.", + "fieldTextTimestampBoxColor": "Timstamp zemin rengi.", + "fieldTextTimestampColor": "Timstamp metin rengi.", + "fieldTextTimestampFont": "Zaman damganızı şekillendirmek için yazı tipi dosyası.", + "fieldTextTimestampFontSize": "Pt.", + "fieldTextTimestampX": "Zaman damgasının ufiztonal konumu", + "fieldTextTimestampY": "Zaman damgasının dikey konumu", + "fieldTextTvChannel": "Bu monitör TV kanalı özellikleri etkin olacaktır. TV kanalı listenizde görüntüleyebileceksiniz.", + "fieldTextTvChannelGroupTitle": "Kanal için özel bir grup.", + "fieldTextTvChannelId": "Kanal için özel bir kimlik.", + "fieldTextType": "Video akışını tüketmek için kullanılacak yöntem.", + "fieldTextTypeDashcam(StreamerV2)": "WebSocket WebM tabanlı P2P akışı.", + "fieldTextTypeH.264/H.265/H.265+": "Bazen ses içeren yüksek kaliteli bir video telini okumak.", + "fieldTextTypeHLS(.m3u8)": "Bazen ses içeren yüksek kaliteli bir video telini okumak.", + "fieldTextTypeJPEG": "Bir URL'den anlık görüntüleri okumak ve onlardan bir akış ve/veya video yapmak.", + "fieldTextTypeLocal": "Yakalama kartlarını, web kameralarını veya entegre kameraları okumak.", + "fieldTextTypeMJPEG": "Çerçeve kullanımı hariç JPEG'ye benzer şekilde Shinobi değil FFMPEG tarafından yapılır.", + "fieldTextTypeMPEG4(.mp4/.ts)": "Statik bir dosya. Daha düşük bir oranda okuyun ve gerçek bir canlı akış için kullanılmamalıdır.", + "fieldTextTypeMxPEG": "Mobotix Mjpeg akışı", + "fieldTextTypeRTMP": "Buraya bağlanmayı öğrenin: Makale: RTMP aracılığıyla akışları Shinobi'ye nasıl itilir ", + "fieldTextTypeShinobiStreamer": "WebSocket JPEG tabanlı P2P akışı.", + "fieldTextVcodec": "Kayıt için video kodek.", + "fieldTextVf": "Kayıt bölümünü etkilemek için FFMPEG video filtrelerini bu kutuya yerleştirin. Boşluksuz.", + "fieldTextWallClockTimestampIgnore": "Gelen tüm kamera verilerini sunucu süresi yerine kamera zamanına dayandırın.", + "fieldTextWatchdogReset": "Tetik kaydında bir örtüşme varsa, sıfırlaması gerekir.", + "fieldTextWatermark": "Kaydedilen videonun çerçevelerine yakılan bir görüntü.", + "fieldTextWatermarkLocation": "Filigran olarak kullanılacak görüntü konumu.", + "fieldTextWatermarkPosition": "Kaydedilen videonun çerçevelerine yakılan bir görüntü.", + "fieldTextWidth": "Akış görüntüsünün genişliği.", + "fire hydrant": "yangın hidrant", + "flv": "flv", + "for Global Access": "Global erişim için", + "fork": "çatal", + "frisbee": "frizbi", + "getAMonitor": "Bir Monitör Alın", + "getATvChannel": "Monitör için TV kanalları alın", + "getATvChannelText": "Bir .m3u8 çalma listesinde tek bir monitörün kullanılabilir H.264 akışlarını alın.", + "getAllMonitors": "Tüm Monitörleri Alın", + "getAllTvChannels": "Tüm TV kanallarını alın", + "getAllTvChannelsText": "Tüm H.264 akışlarını .m3u8 çalma listesinde alın. Bu listede akışlarını görmek için monitörünüzün ayarlarındaki TV kanalı seçeneğini etkinleştirin.", + "getUserInfo": "Kullanıcı Bilgileri Al", + "getVideos": "Videolar Alın", + "getVideosForMonitor": "Monitör için videolar alın", + "giraffe": "zürafa", + "h264_cuvid": "H.264 Cuvid", + "h264_mmal": "H.264 (Raspberry Pi)", + "h264_nvenc": "H.264 NVENC (Nvidia HW Accel)", + "h264_omx": "H.264 Openmax (Raspberry Pi)", + "h264_qsv": "H.264 (Hızlı Senkronizasyon Videosu)", + "h264_vaapi": "H.264 VA-API (Intel HW Accel)", + "h265BrowserText1": "Bir H.265 dosyası oynamaya çalışıyorsanız, indirmeniz ve VLC gibi başka bir uygulamada açmanız gerekebilir.", + "hair drier": "saç kurutma makinesi", + "handbag": "el çantası", + "hevc_cuvid": "H.265 Cuvid", + "hevc_nvenc": "H.265 NVENC (Nvidia HW Accel)", + "hevc_qsv": "H.265 (Hızlı Senkronizasyon Videosu)", + "hevc_vaapi": "H.265 VA-API (Intel HW Accel)", + "hlsOptions": "HLS Seçenekleri", + "hlsOptionsInvalid": "HLS seçenekleri geçersiz", + "horse": "at", + "hot dog": "sosisli sandviç", + "hour": "saat", + "hours": "saat", + "hwaccel": "İvme motoru", + "hwaccel_device": "HWACCEL Cihazı", + "hwaccel_vcodec": "Video kod çözücü", + "in": "içinde", + "in Days": "Günlerde", + "in seconds": "saniyeler içinde", + "keyId": "Anahtar kimlik", + "keyboard": "tuş takımı", + "kite": "uçurtma", + "knife": "bıçak", + "laptop": "dizüstü bilgisayar", + "lastLogin": "Son giriş", + "libmp3lame": "Libmp3Lame", + "libopus": "Libopus", + "libvorbis (Default)": "libvorbis (varsayılan)", + "libvpx (Default)": "libvpx (varsayılan)", + "libvpx-vp9": "libvpx-vp9", + "libx264": "libx264", + "libx264 (Default)": "libx264 (varsayılan)", + "libx265": "libx265", + "liveGridDescription": "Canlı ızgara, Shinobi için çoklu akış ekranıdır. Bu görüntüleme yöntemi esas olarak masaüstü için tasarlanmıştır.", + "loginHandleUnbound": "Oturum açma bu hesaptan bağlantısız.", + "microwave": "mikrodalga", + "migrateText1": " Giriş Türü ayrıştırılamadı. Lütfen manuel olarak ayarlayın.", + "minute": "dakika", + "minutes": "dakikalar", + "mjpeg_cuvid": "Mjpeg cuvid", + "modifyVideoText1": "Yöntem yoktur. URL'nin son değerinin boş olmadığından emin olun.", + "monSavedButNotCopied": "Monitörünüz kaydedildi, ancak başka bir monitöre kopyalanmadı.", + "monitorConfigFinderDescription": "Bu araç, topluluk tarafından yayınlanan kameralar için yapılandırmalar aramanıza yardımcı olacaktır. Hepsi shinobihub . Sizinkini de gönderebilirsiniz, topluluğa gerçekten yardımcı olur :)", + "monitorEditFailedMaxReached": "Hesabınız oluşturulabilecek maksimum kamera sayısına ulaştı. Bunun değişmesini istiyorsanız bir yöneticiyle konuşun.", + "monitorEditText1": "Geçersiz veriler, bunun geçerli bir içe aktarma dizesi olduğunu kontrol edin.", + "monitorEditText2": "Geçersiz ayrıntılar dizesi. Bir JSON dizesi olduğunu görmek için kontrol edin ve normal bir nesne değil.", + "monitorGetText1": "Eksik istek, son eğik çizgi URL'de kaldırın veya kabul edilebilir bir değer koyun.", + "monitorStateNotEnoughChanges": "Bir ön ayara eklemeye çalışmadan önce monitör yapılandırmanızda bir değişiklik yapmanız gerekir.", + "monitorStatesError": "Ön Ayarlar Hatasını Monitör", + "months": "aylar", + "motorcycle": "motosiklet", + "mouse": "fare", + "mpeg2_mmal": "MPEG-2 (Raspberry Pi)", + "mpeg2_qsv": "MPEG2 (Hızlı Senkronizasyon Videosu)", + "mpeg4_cuvid": "Mpeg4 cuvid", + "mpeg4_mmal": "MPEG-4 (Raspberry Pi)", + "noLoginTokensAdded": "Bu hesapla ilişkili alternatif girişler yoktur.", + "noSpecialCharacters": "Hiçbir boşluk veya özel karakterler.", + "noTriggerText": "Zaman aşımı döneminden sonra hareket tespit edilmediyse, bir anlaşmazlık bildirimi alacaksınız.", + "noUndoForAction": "Bu eylemi geri alamazsınız.", + "notActivatedText": "Kurulumunuz başarısız olmuştur.", + "notEnoughFramesText1": "Derleme için yeterli çerçeve yok.", + "notPermitted1": "Bu eyleme hesabınızın yöneticisi tarafından izin verilmez. '", + "on": "üzerinde", + "on Error": "hatalı", + "on Event": "olayda", + "onvifdeviceManagerGlobalTip": "ONVIF, kameranın dahili ayarlarının değiştirilmesine izin verir. OnVIF bir şemsiye terimidir, maalesef birçok şey anlamına gelebilir. Bu durumda, bu araçta bir seçenek görebilirsiniz, ancak düzenlenebilir olmayabilir. Bunun nedeni genellikle kamera satıcısının bu yöntemi eklememesi veya amaçlanan kullanımından sapmasıdır. Bu durumlarda, kamera satıcısının öngörülen yöntemi aracılığıyla kameranın yapılandırmasını girmeniz gerekecektir, bu genellikle web tarayıcınızdaki kameranın IP adresini açar.", + "onvifdeviceSavedFoundErrorText": "Bazı ayarlar önceki bir değere dönmüş olabilir. OnVIF aracılığıyla bu kamera ile değiştirilmiş seçeneğin mevcut olmaması mümkündür.", + "onvifdeviceSavedText": "Kameranın dahili ayarları kaydedildi. Bu değişikliklerin geçerli olması için kamerayı yeniden başlatmanız gerekebilir.", + "openImagesDownloadConfirm": "OpenImages'ten resimleri ve sınırlayıcı kutuları (önceden ayarlanmış matrisler) indirmeye başlamak istediğinizden emin misiniz?", + "openImagesDownloadConfirmStop": "Eğitimi durdurmak istediğinden emin misiniz?", + "opencl": "Opencl", + "opencvCascadesText": "Burada hiçbir şey görmezseniz, bu paketin Cascades paketini indirin. Bunları eklentileri/opencv/cascades içine bırakın, ardından Yenileme tuşuna basın.", + "orange": "Portakal", + "oven": "fırın", + "p2pServerNotSelectedText": "Listeden bir sunucu seçin ve Kaydet'e basın. 10 saniye bekleyin, ardından gösterge tablosuna uzaktan açmaya çalışın.", + "p2pSettingsText1": "Uygulanacak değişiklikler için bu sayfayı yenilemeniz gerekecektir.", + "parking meter": "parkmetre", + "performanceOptimizeText1": "Kameranız H.264 akış verileri sağlıyor. Akış türünü kopyalamak için HLS, Poseidon ve Video Codec olarak ayarlayabilirsiniz.", + "person": "kişi", + "pizza": "Pizza", + "possibleInternalError": "Olası dahili hata", + "postDataBroken": "JSON formatını kontrol edin. 'Veriler' altında yayılmış ve tanımlandığından emin olun", + "potted plant": "saksı bitkisi", + "powerVideoEventLimit": "Yüksek bir olay sınırı belirlediniz. Bu isteği yapmak istediğinden emin misiniz?", + "privateKey": "Özel anahtar", + "qsv": "QSV", + "rebootingCamera": "Kamerayı yeniden başlatma", + "refrigerator": "buzdolabı", + "remote": "uzak", + "restartRequired": "Değişikliklerin yürürlüğe girmesi için Shinobi çekirdeğinin yeniden başlatılması gereklidir.", + "sandwich": "sandviç", + "scissors": "makas", + "separateByCommasOrRange": "Virgül veya menzil ile ayrı", + "setMaxStorageAmountText": "Maksimum depolama miktarınızı soldaki hesap ayarlarınızda ayarlamanız gerekir. Profil bölümünün altındaki seçeneği bulun. Varsayılan 10 GB'dır.", + "sheep": "koyun", + "sink": "lavabo", + "sizePurgeLockedText": "Boyut temizleme kilidi (deleteOvermax) kilidini açamadı. Şimdi kilidini açmak ...", + "skateboard": "kaykay", + "skipPingText1": "\"Ping at\" yı evet olarak ayarlamayı deneyin.", + "skis": "kayaklar", + "snowboard": "snowboard", + "sorryNo": "Üzgünüm hayır", + "sorryNothingWasFound": "Üzgünüm, hiçbir şey bulunamadı.", + "spoon": "kaşık", + "sports ball": "spor topu", + "startUpText0": "KONTROL DISK İSTİYOR ..", + "startUpText1": "Kullanılan disk tamamlandı.", + "startUpText2": "Tüm kullanıcılar kontrol etti, dosyaları açmak ve dosyaları kullanıcı limiti üzerinden kaldırmayı bekleyin", + "startUpText3": "Bitmemiş video vermek için bir süre kontrol edin. 3 saniye.", + "startUpText4": "Monitörler Başlat ... Lütfen bekleyin ...", + "startUpText5": "Shinobi hazır.", + "startUpText6": "Bulunan ve eklenen yetim videolar", + "stop sign": "dur işareti", + "subAccountManager": "Alt Hesap Yöneticisi", + "substreamConnectionText": "Yukarıda ayarlanan ana bağlantı bilgilerini kullanmasını istiyorsanız bağlantı detayını olduğu gibi bırakabilirsiniz.", + "substreamOutputText": "Burada isteğe bağlı akışın yapılandırmasını ayarlayabilirsiniz. akış türlerinin gecikmesi. ", + "substreamText": "Bu, canlı akışı görüntülemek için isteğe bağlı bir yöntemdir. Görüntüleme işleminin yalnızca birisi izlediğinde veya düşük ve yüksek çözünürlük arasında geçiş yapmak için kullanılacak şekilde kullanılabilir şekilde yapabilirsiniz.", + "suitcase": "bavul", + "superAdminText": "\"Super.json\" yok. Lütfen \"Super.smon\" adını \"Super.json\" olarak yeniden adlandırın.", + "superAdminTitle": "Shinobi: Süper Yönetici", + "surfboard": "sörf tahtası", + "teddy bear": "Oyuncak ayı", + "tennis racket": "Tenis raketi", + "tie": "bağlamak", + "toaster": "tost makinası", + "toilet": "tuvalet", + "tokenNotUserBound": "Bu giriş kolu bu sunucudaki bir kullanıcıya bağlı değildir!", + "tokenNotUserBoundPt2": "Kimlik bilgilerinizi yazın ve ardından hızlı bir şekilde bağlantı kurmak için Google Oturum Aç düğmesini kullanın.", + "toothbrush": "diş fırçası", + "total": "Toplam", + "traffic light": "trafik ışığı", + "train": "tren", + "truck": "kamyon", + "tv": "televizyon", + "umbrella": "şemsiye", + "undoAllUnsaveChanges": "Bunu yapmak istediğinden emin misin? Bu, kaydedilmemiş tüm değişiklikleri geri alacaktır.", + "unexpectedExitText": "Bu çıkış hakkında bilgi bu kütükten önce bulunacaktır. Ayrıca burada işlem çöktüğünde kullanılan FFMPEG komutu.", + "updateCamerasInternalSettings": "Kameranın dahili ayarlarını güncelleyin?", + "updateKeyText1": "\"UpdateKey\" \"Conf.json\" da eksik, siz ekleyene kadar bu şekilde güncellemeler yapamaz.", + "updateKeyText2": "\"UpdateKey\" yanlış.", + "updateNotice1": "Shinobi'nin güncellenmesi, dosyaların üzerine yazma anlamına gelir. Herhangi bir dosyayı kendiniz değiştirdiyseniz, Shinobi'yi manuel olarak güncellemelisiniz. Yapılandırmalarınız ve video dosyalarınız değiştirilmeyecektir.", + "useSubStreamOnlyWhenWatching": "Yalnızca izlerken alt akışı kullanın", + "vaapi": "Vaapi (VA-api)", + "vase": "vazo", + "vda": "VDA (Apple VDA Donanım Hızlanması)", + "vdpau": "VDPAU", + "videoBuildingText1": "Video şu anda oluşturuluyor. Daha sonra tekrar kontrol edin.", + "videotoolbox": "video kutusu", + "vp8_cuvid": "VP8 NVENC (Nvidia HW Accel)", + "vp8_qsv": "VP8 (Hızlı Senkronizasyon Videosu)", + "vp9_cuvid": "VP9 NVENC (Nvidia HW Accel)", + "wannaReset": "Sıfırlamak ister misiniz?", + "willTriggerAnEvent": "Bir etkinliği tetikleyecek", + "wine glass": "şarap bardağı", + "years": "yıllar", + "zebra": "zebra" +} \ No newline at end of file diff --git a/languages/vi.json b/languages/vi.json new file mode 100644 index 00000000..22d638ba --- /dev/null +++ b/languages/vi.json @@ -0,0 +1,1647 @@ +{ + "\"No Motion\" Detector": "Máy dò \"Không chuyển động\"", + "# of Allow MJPEG Clients": "# cho phép các máy khách MJPEG 0 cho vô hạn ", + "'Already Installing...'": "'Đã cài đặt ...'", + "180 Degrees": "180 độ", + "2-Factor Authentication": "Xác thực 2 yếu tố", + "90 Clockwise": "90 theo chiều kim đồng hồ", + "90 Clockwise and Vertical Flip": "90 theo chiều kim đồng hồ và lật dọc", + "90 Counter Clockwise and Vertical Flip (default)": "90 ngược chiều kim đồng hồ và lật dọc (mặc định)", + "AND": "VÀ", + "API": "API", + "API Key": "Mã API", + "API Key Action Failed": "Hành động khóa API không thành công", + "API Key Added": "Khóa API được thêm vào", + "API Key Deleted": "Khóa API bị xóa", + "API Keys": "Khóa API", + "APIKeyAddedText": "Bạn có thể sử dụng khóa này ngay bây giờ.", + "APIKeyDeletedText": "Key đã bị xóa. Nó sẽ không còn hoạt động nữa.", + "ASC": "TĂNG DẦN", + "Accelerator": "Máy gia tốc", + "Account Info": "Thông tin tài khoản", + "Account Information": "thông tin tài khoản", + "Account Privileges": "Đặc quyền tài khoản", + "Account Save": "Tài khoản lưu", + "Account Settings": "Cài đặt tài khoản", + "AccountEditText1": "Không thể chỉnh sửa. Làm mới trang nếu vấn đề tiếp tục.", + "Accounts": "Tài khoản", + "Action for Selected": "Hành động cho đã chọn", + "Activated": "Kích hoạt", + "Active Monitors": "Màn hình hoạt động", + "Add": "Thêm vào", + "Add All": "Thêm tất cả", + "Add Camera": "Thêm camera", + "Add Cameras": "Thêm máy ảnh", + "Add Channel": "Thêm kênh", + "Add Input Feed": "Thêm nguồn cấp dữ liệu đầu vào", + "Add Map": "Thêm bản đồ", + "Add Monitor": "Thêm màn hình", + "Add New": "Thêm mới", + "AddToPreset": "Thêm vào đặt trước", + "Additional Inputs": "Đầu vào bổ sung", + "Admin": "Quản trị viên", + "Advanced": "Trình độ cao", + "After": "Sau", + "Again": "Lần nữa", + "Age": "Già đi", + "Alert Sound": "Âm thanh cảnh báo", + "Alert Sound Delay": "Cảnh báo độ trễ âm thanh", + "All Logs": "Tất cả các bản ghi", + "All Monitors": "Tất cả các màn hình", + "All Monitors and Privileges": "Tất cả các màn hình và đặc quyền", + "All Privileges": "Tất cả các đặc quyền", + "All Warnings": "Tất cả các cảnh báo", + "All streams in first feed": "Tất cả các luồng trong nguồn cấp dữ liệu đầu tiên", + "Allow API Trigger": "Cho phép kích hoạt API", + "Allow Next Alert": "Cho phép cảnh báo tiếp theo", + "Allow Next Command": "Cho phép lệnh tiếp theo", + "Allow Next Discord Alert": "Cho phép cảnh báo Discrord tiếp theo trong vài phút ", + "Allow Next Email": "Cho phép email tiếp theo trong vài phút ", + "Allow Next Trigger": "Cho phép kích hoạt tiếp theo", + "Allow Next Webhook": "Cho phép webhook tiếp theo", + "Allowed IPs": "Cho phép IPS", + "Already Processing": "Đã xử lý", + "Already exists": "Đã tồn tại", + "Alternate Logins": "Đăng nhập thay thế", + "Always": "Luôn luôn", + "Amazon S3": "Amazon S3", + "Amazon S3 Upload Error": "Amazon S3 Lỗi tải lên", + "Analyzation Duration": "Thời gian phân tích", + "AppNotEnabledText": "Ứng dụng không được bật, bật nó trong cài đặt tài khoản của bạn.", + "April": "Tháng tư", + "Archive": "Lưu trữ", + "Are you sure?": "Bạn có chắc không?", + "Attach Snapshot": "Đính kèm ảnh chụp nhanh", + "Attach Video Clip": "Đính kèm video clip", + "Audio": "Âm thanh", + "Audio Bit Rate": "Tốc độ âm thanh", + "Audio Codec": "Audio codec", + "Audio Detection": "Phát hiện âm thanh", + "Audio Detector": "Máy dò âm thanh", + "Audio stream only from first feed": "Chỉ luồng âm thanh từ nguồn cấp dữ liệu đầu tiên", + "Audio streams only": "Chỉ luồng âm thanh", + "August": "Tháng tám", + "Authenticate": "Xác thực", + "Authenticated": "Xác thực", + "Authentication Failed": "Quá trình xác thực đã thất bại", + "Auto": "Tự động", + "Automatic": "Tự động", + "Automatic Checking Cancelled": "Kiểm tra tự động bị hủy", + "Automatic Codec Repair": "Sửa chữa codec tự động", + "Automatic Field Fill": "Lĩnh vực tự động điền", + "Autosave": "Tự động", + "Back": "Mặt sau", + "Backblaze B2": "Backblaze B2", + "Backblaze Error": "Lỗi backblaze", + "BacklightCompensation": "Thay đổi đèn nền", + "Backup": "Sao lưu", + "Bandwidth": "Băng thông", + "Base64 over Websocket": "Base64 trên WebSocket", + "Basic Authentication": "Xác thực cơ bản", + "Batch": "Lô hàng", + "Before": "Trước", + "Bind Credentials": "BIND Thông tin đăng nhập (Mật khẩu)", + "BitrateLimit": "Giới hạn bitrate", + "Blank for No Change": "Trống không thay đổi", + "Bottom Left": "Phía dưới bên trái", + "Bottom Right": "Góc phải ở phía dưới", + "Brightness": "độ sáng", + "Browser Console Log": "Nhật ký bảng điều khiển trình duyệt", + "Bucket": "Gầu múc", + "Buffer Preview": "Xem trước bộ đệm", + "Build": "Xây dựng", + "Build Video": "Xây dựng video", + "Building": "Xây dựng", + "CPU": "CPU", + "CPU indicator will not work. Continuing...": "Chỉ báo CPU sẽ không hoạt động. Tiếp tục ...", + "CPU used by this stream": "CPU được sử dụng bởi luồng này", + "CSS": "CSS Kiểu bảng điều khiển của bạn. ", + "Calendar": "Lịch", + "Call Method": "Phương thức gọi", + "Camera Password": "Mật khẩu camera", + "Camera Username": "Tên người dùng máy ảnh", + "Camera is not recording": "Camera không ghi", + "Camera is not running": "Camera không chạy", + "Camera is not streaming": "Camera không phát trực tuyến", + "CameraNotRecordingText": "Cài đặt có thể không tương thích. Kiểm tra bộ mã hóa. Khởi động lại ...", + "Can Authenticate Websocket": "Có thể xác thực WebSocket", + "Can Change User Settings": "Có thể thay đổi cài đặt người dùng", + "Can Control Monitors": "Có thể kiểm soát màn hình", + "Can Create and Delete Monitors": "Có thể tạo và xóa màn hình", + "Can Delete Videos": "Có thể xóa video", + "Can Delete Videos and Events": "Có thể xóa video và sự kiện", + "Can Edit Monitor": "Có thể chỉnh sửa giám sát", + "Can Get Logs": "Có thể nhận được nhật ký", + "Can Get Monitors": "Có thể có màn hình", + "Can View Logs": "Có thể xem nhật ký", + "Can View Monitor": "Có thể xem màn hình", + "Can View Snapshots": "Có thể xem ảnh chụp nhanh", + "Can View Streams": "Có thể xem các luồng", + "Can View Videos": "Có thể xem video", + "Can View Videos and Events": "Có thể xem video và sự kiện", + "Can edit Max Days": "Có thể chỉnh sửa ngày tối đa", + "Can edit Max Storage": "Có thể chỉnh sửa lưu trữ tối đa không", + "Can edit how long to keep Events": "Có thể chỉnh sửa thời gian để giữ các sự kiện", + "Can edit how long to keep Logs": "Có thể chỉnh sửa thời gian để giữ nhật ký", + "Can use Admin Panel": "Có thể sử dụng bảng quản trị", + "Can use Amazon S3": "Có thể sử dụng Amazon S3", + "Can use Discord Bot": "Có thể sử dụng bot discord", + "Can use LDAP": "Có thể sử dụng LDAP", + "Can use SFTP": "Có thể sử dụng SFTP", + "Can use Wasabi Hot Cloud Storage": "Có thể sử dụng lưu trữ đám mây nóng wasabi", + "Can use WebDAV": "Có thể sử dụng webdav", + "Can't Connect": "Không thể kết nối", + "Cannot watch a monitor that isn't running.": "Không thể xem một màn hình không chạy.", + "Cards": "thẻ", + "Carousel in Background": "Băng chuyền trong nền", + "Center": "Trung tâm Địa chỉ URL ", + "Channel": "Kênh", + "Channel ID": "kênh ID", + "Chat on Discord": "Trò chuyện về Discord", + "Check": "Kiểm tra", + "Check Signal Interval": "Kiểm tra khoảng thời gian tín hiệu tính bằng phút ", + "Check for Motion First": "Kiểm tra chuyển động trước", + "Check the Channel ID": "Kiểm tra ID kênh", + "Check the Recipient ID": "Kiểm tra ID người nhận", + "Clear": "Thông thoáng", + "Clear Recorder Process": "Xóa quy trình ghi", + "Close": "Đóng", + "Close All Monitors": "Đóng tất cả các màn hình", + "Closed": "Đóng", + "Cloud": "Mây", + "Codec Mismatch": "Codec không phù hợp", + "Color Threshold": "Ngưỡng màu", + "ColorSaturation": "Độ bão hòa màu", + "Command": "Yêu cầu", + "Command on Trigger": "Lệnh trên kích hoạt", + "Common Objects": "Những vật thông thường", + "Complete Stream URL": "Hoàn thành URL luồng", + "Conditions": "Điều kiện", + "Confidence": "Sự tự tin", + "Confidence of Detection": "Tự tin phát hiện", + "Configuration": "Cấu hình", + "Confirm": "Xác nhận", + "Connected": "Kết nối", + "Connected Users": "Người dùng được kết nối", + "Connection": "Sự liên quan", + "Connection Type": "Kiểu kết nối", + "Connection timed out": "Kết nối quá hạn", + "Contains": "Chứa", + "Contrast": "Tương phản", + "Control": "Điều khiển", + "Control Error": "Lỗi kiểm soát", + "ControlErrorText1": "Điều khiển không được bật", + "ControlErrorText2": "Kiểm tra chi tiết kết nối của bạn. Bạn có thể cần phải trỏ URL cơ sở tại cổng 8000 hoặc 80. Kiểm tra thông tin xác thực của bạn.", + "Controllable": "Có thể kiểm soát được", + "Controls and Logs": "Điều khiển và nhật ký", + "Copied": "Đã sao chép", + "Copied to Clipboard": "Sao chép vào clipboard", + "Copy": "Sao chép", + "Copy Connection Settings": "Sao chép cài đặt kết nối", + "Copy Custom Settings": "Sao chép cài đặt tùy chỉnh", + "Copy Detector Settings": "Cài đặt máy dò sao chép", + "Copy Group Settings": "Sao chép cài đặt nhóm", + "Copy Input Settings": "Sao chép cài đặt đầu vào", + "Copy JPEG API Settings": "Sao chép cài đặt API JPEG", + "Copy Logging Settings": "Sao chép cài đặt ghi nhật ký", + "Copy Mode": "Chế độ sao chép", + "Copy Recording Settings": "Sao chép cài đặt ghi âm", + "Copy Remote Link": "Sao chép liên kết từ xa", + "Copy Settings": "Cài đặt sao chép", + "Copy Stream Channel Settings": "Sao chép cài đặt kênh phát trực tuyến", + "Copy Stream Channels": "Sao chép các kênh phát trực tuyến", + "Copy Stream Settings": "Sao chép cài đặt luồng", + "Copy Stream URL": "Sao chép url luồng", + "Copy Timelapse Settings": "Sao chép cài đặt Timelapse", + "Copy to Settings": "Sao chép vào Cài đặt", + "Cores": "Lõi", + "Could not create Bucket.": "Không thể tạo ra xô.", + "Count Objects": "Đếm đối tượng", + "Count Objects only inside Regions": "Chỉ đếm các đối tượng bên trong các vùng", + "Country of Plates": "Đất nước của tấm", + "Counts of Motion": "Đếm chuyển động", + "Create Sub-Accounts at /admin": "Tạo các tài khoản phụ AT /admin", + "Creating New Account": "Tạo tài khoản mới", + "Creation Interval": "Khoảng thời gian sáng tạo", + "Current": "Hiện hành", + "Currently Active": "Hiện đang hoạt động", + "Currently viewing": "Hiện đang xem", + "Custom": "Tập quán", + "Custom Auto Load": "Tải tự động tùy chỉnh", + "Custom Base URL": "URL cơ sở tùy chỉnh Để trống để sử dụng URL máy chủ ", + "Custom Endpoint": "Điểm cuối tùy chỉnh", + "DB Lost.. Retrying..": "Cơ sở dữ liệu bị mất .. thử lại ..", + "DESC": "Desc", + "DHCP": "DHCP", + "DNS": "DNS", + "Daily Events": "Sự kiện hàng ngày", + "Dashboard": "bảng điều khiển", + "Dashboard Language": "Ngôn ngữ bảng điều khiển", + "Dashcam": "Dashcam", + "Dashcam (Streamer v2)": "Dashcam (Streamer v2)", + "Database": "Cơ sở dữ liệu", + "Database Not Found": "Cơ sở dữ liệu không tìm thấy", + "Database row does not exist": "Hàng cơ sở dữ liệu không tồn tại", + "Date": "Ngày tháng", + "Date Added": "Ngày thêm", + "Date Range": "Phạm vi ngày", + "Date Updated": "Ngày cập nhật", + "Date and Time": "Ngày và giờ", + "DateTimeType": "Quản lý ngày", + "DaylightSavings": "Tiết kiệm ánh sáng ban ngày", + "Days": "Ngày", + "Debug": "Gỡ lỗi", + "December": "Tháng 12", + "Default": "Mặc định", + "Delay for Snapshot": "Trì hoãn cho ảnh chụp nhanh", + "Delete": "Xóa bỏ", + "Delete Camera": "Xóa camera", + "Delete Filter": "Xóa bộ lọc", + "Delete Logs": "Xóa nhật ký", + "Delete Matches": "Xóa các trận đấu", + "Delete Monitor": "Xóa màn hình", + "Delete Monitor State?": "Xóa trạng thái giám sát", + "Delete Monitor States Preset": "Xóa các trạng thái màn hình được đặt trước", + "Delete Monitors and Files": "Xóa màn hình và tệp", + "Delete Motionless Video": "Xóa video bất động", + "Delete Motionless Videos (Record)": "Xóa video bất động (ghi)", + "Delete Region": "Xóa khu vực", + "Delete Schedule": "Xóa lịch trình", + "Delete Selected Videos": "Xóa các video đã chọn", + "Delete Timelapse Frame": "Xóa khung thời gian", + "Delete Video": "Xóa video", + "Delete selected": "Xóa đã chọn", + "DeleteMonitorText": "Bạn có muốn xóa màn hình này không? Bạn không thể phục hồi nó. Bạn có thể chọn cho các tệp để ở trong hệ thống tập tin. Nếu bạn chọn tạo lại một màn hình có cùng ID, các video và sự kiện sẽ được hiển thị trong bảng điều khiển.", + "DeleteMonitorsText": "Bạn có muốn xóa các màn hình này? Bạn không thể phục hồi chúng. Bạn có thể chọn giữ các tệp cho các ID này trong hệ thống tập tin. Nếu bạn chọn tạo lại một màn hình với một trong các ID, các video và sự kiện sẽ được hiển thị trong bảng điều khiển.", + "DeleteSelectedVideosMsg": "Bạn có muốn xóa những video này không? Bạn không thể phục hồi chúng.", + "DeleteThisMsg": "Bạn có muốn xóa cái này không? Bạn không thể phục hồi nó.", + "DeleteVideoMsg": "Bạn có muốn xóa video này không? Bạn không thể phục hồi nó.", + "Deleted": "Đã xóa", + "Deleted Schedule Configuration": "Xóa cấu hình lịch trình", + "Deleted State Configuration": "Xóa cấu hình trạng thái", + "Detect Objects": "Phát hiện các đối tượng Xem bên dưới ", + "Detection": "Phát hiện", + "Detection Engine": "Động cơ phát hiện", + "Detection Event": "Sự kiện phát hiện", + "Detector": "Máy dò", + "Detector Buffer": "Bộ đệm máy dò", + "Detector Filters": "Bộ lọc máy dò", + "Detector Flags": "Cờ phát hiện", + "Detector Grouping": "Nhóm phát hiện Thêm các nhóm trong Cài đặt ", + "Detector Rate": "Tỷ lệ máy dò (fps) ", + "Detector Recording Complete": "Ghi lại máy dò hoàn chỉnh", + "Detector Recording Process Exited Prematurely. Restarting.": "Quá trình ghi lại máy dò thoát sớm. Khởi động lại.", + "DetectorText": "

Khi các hộp chiều rộng và chiều cao được hiển thị, bạn nên đặt chúng thành 640x480 trở xuống. Điều này sẽ tối ưu hóa tốc độ đọc của các khung.

", + "Died": "Chết", + "Digest Authentication": "Xác thực tiêu hóa", + "Disable": "Vô hiệu", + "Disable Night Vision": "Tắt tầm nhìn ban đêm Địa chỉ URL ", + "Disable Nightvision": "Tắt ban đêm", + "Disabled": "Vô hiệu hóa", + "Discord": "Bất hòa", + "Discord Alert on Trigger": "Cảnh báo bất hòa trên kích hoạt", + "Discord Bot": "Discord bot", + "Discord on No Motion": "Discord về \"Không chuyển động\"", + "DiscordErrorText": "Gửi đến Discord gây ra lỗi", + "DiscordFailedText": "Gửi đến Discord không thành công", + "DiscordLoggedIn": "Discord bot xác thực", + "DiscordNotEnabledText": "Discord bot không được bật, bật nó trong cài đặt tài khoản của bạn.", + "Documentation": "Tài liệu", + "Does Not Contain": "Không chứa", + "Don't Show for 1 Week": "Không hiển thị trong 1 tuần", + "Don't show this anymore": "Đừng hiển thị điều này nữa", + "DontAddToPreset": "Đừng thêm vào cài đặt trước", + "Double Quote Directory": "Double Trích dẫn thư mục Một số thư mục có không gian. Sử dụng điều này có thể bị sập một số máy ảnh. ", + "Down": "Xuống Địa chỉ URL ", + "Down Stop": "Down Stop Địa chỉ URL ", + "Download": "Tải xuống", + "Download Bandwidth": "Tải xuống băng thông", + "Downloading Videos": "Tải xuống video", + "Downloading...": "Tải xuống ...", + "Duplicate": "Nhân bản", + "EU": "EU", + "Easy Remote Access (P2P)": "Truy cập từ xa dễ dàng (P2P)", + "Edit": "Biên tập", + "Edit Configuration": "Chỉnh sửa cấu hình", + "Edit Selected": "Chỉnh sửa đã chọn", + "Edited Schedule Configuration": "Cấu hình lịch trình chỉnh sửa", + "Edited State Configuration": "Cấu hình trạng thái chỉnh sửa", + "Email": "E-mail", + "Email Details": "Chi tiết email", + "Email address is in use.": "Địa chỉ email đang được sử dụng.", + "Email and Password fields cannot be empty": "Các trường email và mật khẩu không thể trống", + "Email on No Motion": "Email về \"Không chuyển động\"", + "Email on Trigger": "Email trên Trigger", + "Emotion": "Cảm xúc", + "Emotion Average": "Cảm xúc trung bình", + "Enable": "Cho phép", + "Enable Night Vision": "Bật tầm nhìn ban đêm Địa chỉ URL ", + "Enable Nightvision": "Bật Nightvision", + "Enabled": "Đã bật", + "Encoding": "Mã hóa", + "EncodingInterval": "I-frame", + "End": "Chấm dứt", + "End Time": "Thời gian kết thúc", + "Ended": "Đã kết thúc", + "Endpoint": "Điểm cuối", + "Endpoint Address": "Địa chỉ điểm cuối", + "Enlarge": "Phóng to", + "Enter at least one IP": "Nhập ít nhất một IP", + "Enter this code to proceed": "Nhập mã này để tiếp tục", + "Equal to": "Tương đương với", + "Error Connecting": "Lỗi kết nối", + "Error While Decoding": "Lỗi trong khi giải mã", + "ErrorWhileDecodingText": "Phần cứng của bạn có thể có kết nối không ổn định với mạng. Kiểm tra kết nối mạng của bạn.", + "ErrorWhileDecodingTextAudio": "Máy ảnh của bạn đang cung cấp dữ liệu bị hỏng. Hãy thử vô hiệu hóa âm thanh trong cài đặt nội bộ của máy ảnh.", + "Event": "Biến cố", + "Event Counts": "Số lượng sự kiện", + "Event Filter Error": "Lỗi bộ lọc sự kiện", + "Event Filters": "Bộ lọc sự kiện", + "Event Limit": "Giới hạn sự kiện", + "Event Occurred": "Sự kiện xảy ra", + "Event Rules": "Quy tắc sự kiện", + "Event Webhook Error": "Sự kiện lỗi webhook", + "EventText1": "Đã kích hoạt một sự kiện tại", + "EventText2": "Không thể gửi email hình ảnh, tệp không thể truy cập được", + "Events": "Sự kiện", + "Events Found": "Các sự kiện được tìm thấy", + "Example": "Ví dụ", + "Execute Command": "Thực thi lệnh", + "Executed": "Thực thi", + "Export": "Xuất khẩu", + "Export Selected Videos": "Xuất các video được chọn", + "Export Video": "Xuất video", + "ExportSelectedVideosMsg": "Bạn có muốn xuất các video này không? Có thể mất một thời gian để zip và tải xuống.", + "Exposure": "Phơi bày", + "FFmpegCantStart": "FFMPEG không thể bắt đầu", + "FFmpegCantStartText": "Công cụ ghi âm cho máy ảnh này không thể khởi động. Có thể có điều gì đó sai với cấu hình máy ảnh của bạn. Nếu có bất kỳ nhật ký nào khác ngoài cái này, vui lòng đăng chúng trong các vấn đề trên GitHub.", + "FFmpegTip": "FFProbe là một máy phân tích dòng đa phương tiện đơn giản. Bạn có thể sử dụng nó để xuất tất cả các loại thông tin về đầu vào bao gồm thời lượng, tốc độ khung, kích thước khung, v.v.", + "FFprobe": "Thăm dò", + "FLV": "Flv", + "FLV Stream Type": "Loại luồng FLV", + "FactorAuthText1": "Mã sẽ chỉ hoạt động trong 15 phút. Nếu bạn đăng nhập lại, bộ hẹn giờ sẽ được đặt lại thành 15 phút với cùng một mã.", + "Fatal": "Gây tử vong", + "Fatal Maximum Reached": "Tối đa gây tử vong, dừng camera.", + "FatalMaximumReachedText": "Lỗi JPEG gây tử vong.", + "February": "tháng 2", + "Feed-in Image Height": "Chiều cao hình ảnh thức ăn", + "Feed-in Image Width": "Chiều rộng hình ảnh của nguồn cấp dữ liệu", + "Female": "Giống cái", + "Field Missing Value": "Trường bị thiếu giá trị", + "Fields cannot be empty": "Các trường không thể trống", + "File Delete Error": "Tệp xóa lỗi", + "File Not Exist": "Tệp không tồn tại", + "File Not Found": "Tệp không tìm thấy", + "File Not Found in Database": "Tệp không tìm thấy trong cơ sở dữ liệu", + "File Not Found in Filesystem": "Tệp không tìm thấy trong hệ thống tập tin", + "File Type": "Loại tệp", + "FileBin Share": "FileBin chia sẻ", + "FileNotExistText": "Không thể lưu tệp không tồn tại. Có gì đó đã sai.", + "Filename": "Tên tệp", + "Filesize": "Kích thước tập tin", + "Filter ID": "ID lọc", + "Filter Matches": "Bộ lọc phù hợp", + "Filter Name": "Tên lọc", + "Filter for Objects only": "Chỉ lọc cho các đối tượng", + "FilterMatchesText1": "Bộ lọc này có điều kiện đáp ứng.", + "FilterMatchesText2": "Video được tìm thấy.", + "Filters": "Bộ lọc", + "Filters Updated": "Bộ lọc được cập nhật", + "FiltersUpdatedText": "Những thay đổi của bạn đã được lưu và áp dụng.", + "Find Where": "Tìm ở đâu", + "First stream in feed": "Luồng đầu tiên trong thức ăn", + "Fix": "Khắc phục", + "Fix Video": "Khắc phục video", + "FixVideoMsg": "Bạn có muốn sửa video này không? Bạn không thể hoàn tác hành động này.", + "Flush PM2 Logs": "Nhật ký pm2", + "Font Path": "Đường dẫn phông chữ", + "Font Size": "Cỡ chữ", + "For Group": "Cho nhóm", + "Force Monitors Per Row": "Lực lượng màn hình mỗi hàng", + "Force Port": "Cổng lực", + "Form Data Not Found": "Không tìm thấy dữ liệu hình thức", + "Found Devices": "Tìm thấy thiết bị", + "Frame Rate": "Tỷ lệ khung hình", + "FrameRateLimit": "Giới hạn tốc độ khung hình (FPS)", + "Frames": "Khung", + "Friday": "Thứ sáu", + "Frigate": "Tàu khu trục", + "Full Frame Detection": "Phát hiện khung đầy đủ", + "Full Stream URL": "URL luồng đầy đủ", + "Full URL Path": "Đường dẫn URL đầy đủ", + "Fullscreen": "Toàn màn hình", + "Gateway": "Cổng", + "Gender": "Giới tính", + "Generate Subtitles": "Tạo phụ đề", + "Get Code": "Nhận được mã", + "Get Logs to Client": "Nhận nhật ký cho khách hàng", + "Global Detector Settings": "Cài đặt máy dò toàn cầu", + "Google Drive": "Google Drive", + "GovLength": "Chính phủ", + "Greater Than": "Lớn hơn", + "Greater Than or Equal to": "Lớn hơn hoặc bằng", + "Grid": "Lưới", + "Group Key": "Khóa nhóm", + "Group Key is in use.": "Khóa nhóm đang được sử dụng.", + "Group Name": "Tên nhóm", + "Grouping": "Nhóm", + "H.264 / H.265 / H.265+": "H.264 / H.265 / H.265+", + "H264Profile": "H264 Hồ sơ", + "HEVC (H.265)": "HEVC (H.265)", + "HLS (.m3u8)": "HLS (.M3U8)", + "HLS (includes Audio)": "HLS (bao gồm âm thanh)", + "HLS Audio Encoder": "Bộ mã hóa âm thanh", + "HLS List Size": "Kích thước danh sách", + "HLS Live Start Index": "Chỉ số bắt đầu trực tiếp HLS", + "HLS Preset": "Mẫu đặt trước", + "HLS Segment Length": "Chiều dài phân đoạn tính bằng giây ", + "HLS Start Number": "Số bắt đầu của HLS", + "HLS Video Encoder": "Bộ mã hóa video", + "HTTP": "HTTP", + "HTTPS": "HTTPS", + "Hardware Accelerated": "Phần cứng tăng tốc", + "Height": "Chiều cao", + "Help": "Giúp đỡ", + "Hide List": "Danh sách ẩn", + "Hide Notes": "Ẩn ghi chú", + "Home": "Nhà", + "Host": "Chủ nhà", + "Host Type": "Loại máy chủ", + "Hostname": "Tên máy chủ", + "Hotswap Modes (Watch-Only)": "Chế độ HotSwap (chỉ có đồng hồ)", + "How to Record": "Làm thế nào để ghi lại", + "IP Address": "Địa chỉ IP", + "Identity": "Xác thực", + "IdentityText1": "Đây là cách hệ thống sẽ xác định dữ liệu cho luồng này. Bạn không thể thay đổi ID màn hình khi bạn đã nhấn Lưu. Nếu bạn muốn, bạn có thể tạo ID ID theo dõi có thể đọc được nhiều hơn trước khi bạn tiếp tục.", + "IdentityText2": "Bạn có thể sao chép một màn hình bằng cách sửa đổi ID Màn hình sau đó nhấn Save. Bạn không thể Sử dụng ID của màn hình đã tồn tại hoặc nó sẽ lưu qua thông tin cơ sở dữ liệu của màn hình đó.", + "Idle": "Nhàn rỗi", + "Image Height": "Chiều cao hình ảnh", + "Image Location": "Vị trí hình ảnh", + "Image Position": "Vị trí hình ảnh", + "Image Width": "Chiều rộng hình ảnh", + "Imaging": "Hình ảnh", + "Import": "Nhập khẩu", + "Import Monitor Configuration": "Nhập cấu hình màn hình", + "ImportMonitorConfigurationText": "Làm điều này sẽ ghi đè bất kỳ thay đổi hiện không được lưu. Thay đổi đã nhập sẽ chỉ được áp dụng khi bạn nhấn lưu .", + "ImportMultiMonitorConfigurationText": "Làm điều này sẽ ghi đè bất kỳ màn hình nào có ID hiện có trong tệp nhập.", + "In": "Trong", + "Incorrect Settings Chosen": "Cài đặt không chính xác được chọn", + "Indifference": "Thờ ơ", + "Info": "Thông tin", + "Information": "Thông tin", + "Input": "Đầu vào", + "Input Feed": "Nguồn cấp dữ liệu đầu vào", + "Input Feeds Selected": "Nguồn cấp dữ liệu đầu vào được chọn", + "Input Flags": "Cờ đầu vào", + "Input Map": "Bản đồ đầu vào", + "Input Selector": "Bộ chọn đầu vào", + "Input Settings": "Nhập vào cài đặt", + "Input Type": "Kiểu đầu vào", + "InputText1": "Phần này cho Shinobi biết cách tiêu thụ một luồng. Để có hiệu suất tối ưu, hãy, hãy, hãy thử điều chỉnh cài đặt nội bộ của máy ảnh của bạn. Để tìm máy ảnh của bạn, bạn có thể sử dụng được tích hợp trong máy quét ONVIF của shinobi. Để mở trình quét ONVIF, nhấp vào tên người dùng của bạn ở phía trên bên trái và sau đó là ONVIF.", + "InputText2": "Tìm hiểu về việc định cấu hình và điều chỉnh máy ảnh của bạn tại đây .", + "InputText3": "Nếu bạn cần trợ giúp để tìm ra loại máy ảnh đầu vào của bạn, bạn có thể xem trong ", + "Left Stop": "Dừng trái Địa chỉ URL ", + "Legacy Webhook": "Di sản Webhook", + "Less Than": "Ít hơn", + "Less Than or Equal to": "Ít hơn hoặc bằng", + "License Activated": "Giấy phép được kích hoạt", + "License Activation": "Kích hoạt giấy phép", + "License Activation Failed": "Kích hoạt giấy phép không thành công", + "License Key": "Mã bản quyền", + "License Plate Detector": "Máy dò biển số", + "Like": "Như", + "Limited": "Giới hạn", + "Link Google Account": "Liên kết tài khoản Google", + "Link LDAP Account": "Liên kết Tài khoản LDAP", + "Link Shinobi": "Liên kết Shinobi", + "List Toggle": "Danh sách chuyển đổi", + "List of Videos Delete Error": "Danh sách các video xóa lỗi", + "Live Grid": "Lưới trực tiếp", + "Live Stream Toggle": "Chuyển đổi luồng trực tiếp", + "Live View": "Xem trực tiếp", + "Local": "Địa phương", + "Log Level": "Mức đăng nhập", + "Log Signal Event": "Sự kiện tín hiệu nhật ký Chỉ phía máy khách ", + "Log Stream": "Nhật ký luồng", + "Logging": "Đăng nhập", + "Login": "Đăng nhập", + "Logout": "Đăng xuất", + "Logs": "Nhật ký", + "Loop Stream": "Luồng vòng lặp", + "MB": "MB", + "MJPEG": "Mjpeg", + "MP4 (copy, libx264, libx265)": "Mp4 (sao chép, libx264, libx265)", + "MPEG-4 (.mp4 / .ts)": "MPEG-4 (.mp4 / .ts)", + "MPEG-DASH (includes Audio)": "MPEG-Dash (bao gồm âm thanh)", + "MQTT Client": "Máy khách MQTT", + "MQTT Error": "Lỗi MQTT", + "MQTT Inbound": "MQTT trong nước", + "MQTT Outbound": "MQTT đi ra ngoài", + "MailError": "Lỗi thư: Không thể gửi email, kiểm tra conf.json. Bỏ qua bất kỳ tính năng nào dựa vào gửi thư.", + "Main": "Chủ yếu", + "Male": "Nam giới", + "Manual": "Thủ công", + "Map": "Bản đồ", + "March": "Bước đều", + "Matches": "Diêm", + "Matrices": "Ma trận", + "Max Indifference": "Sự thờ ơ tối đa", + "Max Latency": "Độ trễ tối đa", + "Max Number of Cameras": "Số lượng máy ảnh tối đa", + "Max Storage Amount": "Số lượng lưu trữ tối đa", + "MaxExposureTime": "Thời gian phơi sáng tối đa", + "MaxGain": "Tăng tối đa", + "Maximum Change": "Thay đổi tối đa", + "Maximum dB": "DB tối đa", + "May": "Có thể", + "Merge Selected Videos": "Hợp nhất các video đã chọn", + "Merge Video": "Hợp nhất video", + "Merge and Download": "Hợp nhất và tải xuống", + "MergeSelectedVideosMsg": "Bạn có muốn hợp nhất các video này không? Có thể mất một thời gian để hợp nhất và tải xuống. Thời điểm kết nối được đóng, tệp sẽ bị xóa. Đảm bảo bạn giữ cho trình duyệt mở cho đến khi hoàn thành.", + "Methods": "Phương pháp", + "Migrator": "Người di cư", + "MinExposureTime": "Thời gian phơi nhiễm tối thiểu", + "MinGain": "Tăng tối thiểu", + "Minimum Change": "Thay đổi tối thiểu", + "Minimum dB": "DB tối thiểu", + "Minutes": "Phút", + "Mode": "Cách thức", + "Monday": "Thứ hai", + "Monitor": "Màn hình", + "Monitor Added by user": "Giám sát được thêm bởi người dùng.", + "Monitor Capture Rate": "Giám sát tỷ lệ bắt giữ (FPS) ", + "Monitor Died": "Màn hình chết", + "Monitor Edit": "Giám sát chỉnh sửa", + "Monitor Groups": "Giám sát các nhóm", + "Monitor ID": "Giám sát ID", + "Monitor Idling": "Giám sát không hoạt động", + "Monitor Name": "Tên giám sát", + "Monitor Settings": "Cài đặt giám sát", + "Monitor Start": "Giám sát bắt đầu", + "Monitor States": "Giám sát trạng thái", + "Monitor States and Schedules": "Giám sát các trạng thái và lịch trình", + "Monitor Stop": "Theo dõi điểm dừng", + "Monitor Stopped": "Màn hình dừng lại", + "Monitor Updated by user": "Giám sát được cập nhật bởi người dùng.", + "Monitor is now Disabled": "Màn hình hiện bị vô hiệu hóa", + "Monitor is now Idle": "Màn hình hiện đang nhàn rỗi", + "Monitor is now Recording": "Màn hình hiện đang ghi", + "Monitor is now Watching": "Màn hình hiện đang theo dõi", + "Monitor mode changed": "Chế độ giám sát đã thay đổi", + "Monitor mode is already": "Chế độ giám sát đã", + "Monitor or Key does not exist.": "Giám sát hoặc khóa không tồn tại.", + "MonitorIdlingText": "Phiên giám sát đã được yêu cầu nhàn rỗi.", + "MonitorStatesText": "Bạn có thể tìm hiểu về cách sử dụng ở đây trên shinobihub .", + "MonitorStoppedText": "Phiên giám sát đã được yêu cầu dừng lại.", + "Monitors": "Màn hình", + "Monitors per row": "Màn hình trên mỗi hàng cho Montage ", + "Monitors to Copy to": "Màn hình để sao chép vào", + "Montage": "Dựng phim", + "Motion": "Cử động", + "Motion Detection": "Phát hiện chuyển động", + "Motion GUI": "GUI chuyển động", + "Motion Meter": "Đồng hồ đo chuyển động", + "Motion Threshold": "Ngưỡng chuyển động", + "Mp4Frag": "Mp4frag", + "Must be atleast one row": "Phải ít nhất một hàng", + "Mute Audio": "Tắt tiếng", + "NTP": "NTP", + "NTP Servers": "Máy chủ NTP", + "NVIDIA": "Nvidia", + "Name": "Tên", + "Name cannot be empty.": "Tên không thể trống.", + "Nameservers": "Máy chủ tên", + "Network": "Mạng", + "Network Manager": "Quản lý mạng", + "Never": "Không bao giờ", + "New Authentication Token": "Mã thông báo xác thực mới", + "New Monitor": "Màn hình mới", + "Newest": "Mới nhất", + "Next Video": "Video tiếp theo", + "No": "Không", + "No API Key": "Không có khóa API", + "No Audio": "Không có âm thanh", + "No Data": "Không có dữ liệu", + "No Events found for this video": "Không tìm thấy sự kiện nào cho video này", + "No Group with this key exists": "Không có nhóm nào có chìa khóa này tồn tại", + "No Monitor Exists with this ID.": "Không có màn hình tồn tại với ID này.", + "No Monitor Found, Ignoring Request": "Không tìm thấy màn hình, bỏ qua yêu cầu", + "No Monitor ID Present in Form": "Không có ID màn hình có hình thức", + "No Monitors Selected": "Không có màn hình được chọn", + "No Region": "Không có khu vực", + "No Rotation": "Không xoay vòng", + "No Sound": "Không có âm thanh", + "No Trigger": "Không có kích hoạt", + "No Videos Found": "Không tìm thấy video nào", + "No such file": "Không có tệp như vậy", + "NoLogsFoundForDateRange": "Không có nhật ký nào được tìm thấy trong phạm vi ngày này. Hãy thử mở rộng phạm vi ngày.", + "NoMotionEmailText1": "Không có chuyển động cho", + "NoMotionEmailText2": "Không có bất kỳ chuyển động nào được phát hiện trên máy ảnh cho", + "NoVideosFoundForDateRange": "Không có video tìm thấy trong phạm vi ngày này. Hãy thử đặt ngày bắt đầu trở lại.", + "Noise Filter": "Máy lọc tiếng ồn", + "Noise Filter Range": "Phạm vi bộ lọc nhiễu", + "Non-Standard ONVIF": "Onvif không chuẩn", + "Not Activated": "Chưa kích hoạt", + "Not Authorized": "Không được ủy quyền", + "Not Connected": "Không kết nối", + "Not Equal to": "Không bằng", + "Not Found": "Không tìm thấy", + "Not In": "Không phải vào", + "Not Matches": "Không phù hợp", + "Not Permitted": "Không được phép", + "Not Saved": "Chưa được lưu", + "Not an Administrator Account": "Không phải tài khoản quản trị viên", + "NotAuthorizedText1": "Không được ủy quyền, gửi lệnh init với \"auth\", \"ke\" và \"uid\"", + "Notes": "Ghi chú", + "NotesPlacholder": "Nhận xét bạn muốn để lại cho cài đặt máy ảnh này.", + "Nothing exists": "Không có gì tồn tại", + "Notice": "Lưu ý", + "Notification Sound": "Âm thanh thông báo", + "Notification Video Length": "Độ dài video thông báo", + "Notifications": "Thông báo", + "NotifyErrorText": "Gửi thông báo gây ra lỗi", + "November": "Tháng mười một", + "Number of Days to keep": "Số ngày để giữ", + "Numeric criteria unsupported for Region tests, Ignoring Conditional": "Tiêu chí số không được hỗ trợ cho các bài kiểm tra vùng, bỏ qua điều kiện", + "OAuth Code": "Mã OAuth", + "OAuth Credentials": "Thông tin OAuth", + "ONVIF": "Onvif", + "ONVIF Device Manager": "Trình quản lý thiết bị ONVIF", + "ONVIF Port": "Cổng Onvif", + "ONVIF Scanner": "Máy quét ONVIF", + "ONVIFErr400": "Tìm thấy cổng ONVIF nhưng ủy quyền không thành công khi truy xuất URL luồng. Kiểm tra tên người dùng và mật khẩu được sử dụng để quét. Đảm bảo thời gian máy ảnh và thời gian máy chủ của bạn được đồng bộ hóa.", + "ONVIFErr404": "Không tìm thấy. Đây có thể chỉ là bảng web cho một thiết bị mạng.", + "ONVIFErr405": "Phương pháp không được phép. Kiểm tra tên người dùng và mật khẩu được sử dụng để quét.", + "ONVIFEventsNotAvailable": "Sự kiện ONVIF không có sẵn", + "ONVIFEventsNotAvailableText1": "Dịch vụ này có thể không có sẵn cho máy ảnh này hoặc ONVIF chưa được khởi tạo.", + "ONVIFnotCompliantProfileT": "Camera không tuân thủ hồ sơ ONVIF", + "ONVIFnote": "Khám phá các thiết bị ONVIF trên các mạng bên ngoài hoặc để trống để quét mạng hiện tại của bạn.
Tên người dùng và mật khẩu có thể bị bỏ trống.", + "OR": "HOẶC", + "Object": "Vật", + "Object Count": "Số lượng đối tượng", + "Object Detection": "Phát hiện đối tượng", + "Object Detector Flags": "Cờ phát hiện đối tượng", + "Object Tag": "Thẻ đối tượng", + "Objects to look for": "Các đối tượng để tìm kiếm", + "October": "Tháng Mười", + "Off": "Tắt", + "Oldest": "Lâu đời nhất", + "On": "Trên", + "On Unexpected Exit": "Trên lối ra bất ngờ", + "Open": "Mở", + "Open All Monitors": "Mở tất cả các màn hình", + "Open Remote Dashboard": "Mở bảng điều khiển từ xa", + "OpenCV Cascades": "Cascades opencv", + "Operating Hours": "Thời gian hoạt động", + "Optional": "Không bắt buộc", + "Options": "Tùy chọn", + "Order Streams": "Đặt hàng", + "Original Choice": "Sự lựa chọn ban đầu", + "Other Devices": "Các thiết bị khác", + "Output": "Đầu ra", + "Output Method": "Phương pháp đầu ra", + "P2P API Key": "Khóa API P2P", + "P2P Host": "Máy chủ P2P", + "P2P Server Not Selected": "Máy chủ P2P không được chọn", + "P2P Settings Applied": "Cài đặt P2P được áp dụng", + "PTZ Tracking": "Theo dõi PTZ", + "PTZ Tracking Target": "Mục tiêu theo dõi PTZ", + "Password": "Mật khẩu", + "Password Again": "Nhập lại mật khẩu", + "Passwords don't match": "Mật khẩu không khớp", + "Paste JSON here.": "Dán JSON ở đây hoặc tải lên tệp.", + "Path": "Đường dẫn", + "Pause": "Tạm ngừng", + "Per Monitor": "Mỗi màn hình", + "Performance Optimization Possible": "Tối ưu hóa hiệu suất có thể", + "Permissions": "Quyền", + "Ping Failed": "Ping không thành công", + "Plain": "Trơn", + "Play": "Chơi", + "Playback": "Phát lại", + "Please Check Your Settings": "Vui lòng kiểm tra cài đặt của bạn", + "Please Wait for Completion": "Vui lòng đợi hoàn thành, tùy thuộc vào số lượng tệp được chọn, điều này có thể mất một thời gian.", + "Please Wait or Click to Stop Checking": "Vui lòng đợi hoặc nhấp để ngừng kiểm tra", + "Please Wait...": "Vui lòng chờ...", + "Plugin": "Cắm vào", + "Plugin Manager": "Plugin Manager", + "Points": "Điểm", + "Pop": "Nhạc pop", + "Popout Monitor on Event": "Màn hình bật lên sự kiện", + "Port": "Hải cảng", + "Pose": "Tư thế", + "Poseidon": "Poseidon", + "Position X": "Vị trí x", + "Position Y": "Vị trí y", + "Power Video Viewer": "Trình xem video điện", + "Power Viewer": "Người xem quyền lực", + "Preferences": "Sở thích", + "Prefix": "Tiếp đầu ngữ", + "Preset": "Đặt trước", + "Preset Name": "Tên đặt trước", + "Presets": "Cài đặt trước", + "Preview": "Xem trước", + "Previous Video": "Video trước", + "Primary Engine": "Động cơ chính", + "Primary Input": "Đầu vào chính", + "Privileges": "Đặc quyền", + "Probe Size": "Kích thước thăm dò", + "Process Already Running": "Quá trình đã chạy", + "Process Crashed for Monitor": "Quá trình bị sập để theo dõi", + "Process Not Running": "Quá trình không chạy", + "Process Started": "Quá trình bắt đầu", + "Process Unexpected Exit": "Quá trình thoát bất ngờ", + "Processor": "Bộ xử lý", + "Profile": "Hồ sơ", + "Protocol": "Giao thức", + "Public on ShinobiHub": "Công khai trên Shinobihub", + "Quality": "Phẩm chất", + "Query": "Truy vấn", + "Quick Settings": "Cài đặt nhanh", + "Quick Sync Video": "Video đồng bộ nhanh", + "RAM": "Ram", + "RTMP": "RTMP", + "RTMP Stream": "Luồng RTMP", + "RTMP Stream Flags": "Cờ luồng RTMP", + "RTMPS": "RTMP", + "RTSP": "RTSP", + "RTSP Transport": "Vận chuyển RTSP", + "Range or Single": "Phạm vi hoặc đơn", + "Raspberry Pi": "Raspberry pi", + "Rate": "Tỷ lệ (fps) ", + "Raw": "Thô", + "Raw H.264 Stream": "Dòng H.264 thô", + "Reason": "Lý do", + "Reboot": "Khởi động lại", + "Reboot Camera": "Khởi động lại camera", + "Recent Events": "Sự kiện gần đây", + "Recent Videos": "Video gần đây", + "Recipient ID": "ID người nhận", + "Recommended": "Khuyến khích", + "Reconnect Stream": "Kết nối lại luồng", + "Record": "Ghi lại", + "Record File Type": "Ghi loại tệp", + "Record Height": "Ghi chiều cao", + "Record Video Filter": "Ghi lại bộ lọc video", + "Record Width": "Ghi lại chiều rộng", + "Recorded Buffer": "Buffer được ghi lại", + "Recording": "ghi âm", + "Recording FPS": "Ghi FPS", + "Recording FPS Change on Start": "Ghi lại FPS thay đổi khi bắt đầu", + "Recording Flags": "Ghi cờ", + "Recording Segment Interval": "Ghi lại khoảng thời gian phân đoạn tính bằng phút ", + "Recording Timeout": "Ghi thời gian chờ tính bằng phút ", + "Recording Timestamp": "Ghi dấu thời gian", + "Recording Watermark": "Ghi lại hình mờ", + "RecordingText": "Bạn nên đặt bản ghi loại tệp thành webm codec video to libvpx libx264 vì loại đầu vào của bạn được đặt thành .", + "Refresh List of Cascades": "Làm mới danh sách các tầng", + "Region": "Vùng đất", + "Region Editor": "Biên tập viên khu vực", + "Region Name": "Tên khu vực", + "RegionNote": "Khi thêm điểm, nhấp vào cạnh của đa giác. Nhấp chuột phải vào một điểm để loại bỏ.", + "Regions": "Vùng", + "Registered": "Đăng ký", + "Registered Servers": "Máy chủ đã đăng ký", + "Remember Me": "Nhớ tôi", + "Request": "Yêu cầu", + "Require Object to be in Region": "Yêu cầu đối tượng phải ở trong khu vực", + "Reset": "Cài lại", + "Reset Form": "Đặt lại biểu mẫu", + "Reset Timer": "Đặt lại bộ đếm thời gian", + "Resolution": "Nghị quyết", + "Restart": "Khởi động lại", + "Restart CRON": "Khởi động lại Cron", + "Restart Core": "Khởi động lại lõi", + "Restarting": "Khởi động lại", + "Restarting Process": "Khởi động lại quá trình", + "Retry Connection": "Kết nối thử lại Số lần được phép thất bại ", + "Retrying...": "Thử lại ...", + "Right": "Đúng địa chỉ url ", + "Right Stop": "Dừng phải Địa chỉ URL ", + "Rotate": "Quay", + "Rule": "Luật lệ", + "Run Installer": "Chạy trình cài đặt", + "S3-Based Network Storage": "Lưu trữ mạng dựa trên S3", + "SFTP": "SFTP", + "SFTP (SSH File Transfer)": "SFTP (Truyền tệp SSH)", + "SFTP Error": "Lỗi SFTP", + "Saturday": "Thứ bảy", + "Save": "Cứu", + "Save Changes": "Lưu thay đổi", + "Save Directory": "Lưu thư mục", + "Save Events": "Lưu các sự kiện", + "Save Events to SQL": "Lưu các sự kiện vào SQL", + "Save Frames to Events": "Lưu khung vào các sự kiện", + "Save Links to Database": "Lưu liên kết vào cơ sở dữ liệu", + "Save Log in SQL": "Lưu Đăng nhập vào SQL Điều này có thể điền nhanh. ", + "Save New": "Lưu mới", + "Save as": "Lưu thành", + "Saved": "Đã lưu", + "Saved Filters": "Bộ lọc đã lưu", + "Saved Logs": "Nhật ký đã lưu", + "Saved Presets": "Đã lưu cài đặt trước", + "Saved Schedules": "Lịch trình lưu", + "Scan Settings": "Cài đặt quét", + "Schedule": "Lịch trình", + "Schedule Configuration Not Found": "Lịch trình cấu hình không tìm thấy", + "Schedules": "Lịch trình", + "Search": "Tìm kiếm", + "Search Base": "Cơ sở tìm kiếm", + "Search Filter": "Bộ lọc tìm kiếm", + "Search Images": "Tìm kiếm hình ảnh", + "Search Settings": "Thiết lập tìm kiếm", + "Second stream in feed": "Luồng thứ hai trong thức ăn", + "Secure": "Chắc chắn", + "Select a Monitor": "Chọn một màn hình", + "Select atleast one monitor to delete": "Chọn ít nhất một màn hình để xóa.", + "Selected": "Đã chọn", + "Send Frames": "Gửi khung Đẩy khung để được phân tích ", + "Send Notification": "Gửi thông báo", + "Send to": "Gửi đến", + "Separate with commas, no spaces": "Tách biệt với dấu phẩy, không có khoảng trống", + "September": "Tháng 9", + "Server URL": "URL máy chủ", + "Session Key": "Khóa phiên", + "Set Home": "Đặt nhà", + "Set Home Position (ONVIF-only)": "Đặt vị trí nhà (chỉ ONVIF)", + "Set Mode": "Đặt chế độ", + "Set to Watch Only": "Đặt để chỉ xem", + "Settings": "Cài đặt", + "Settings Changed": "Cài đặt đã thay đổi", + "SettingsChangedText": "Cài đặt của bạn đã được lưu và áp dụng. Một số cài đặt có thể yêu cầu làm mới trang này.", + "Sharpness": "Độ sắc nét", + "Shinobi": "Shinobi", + "Shinobi Ordered to Update": "Cập nhật Shinobi hoàn thành", + "Shinobi Streamer": "Shinobi Streamer", + "ShinobiHub": "Shinobihub", + "Show Logs": "Hiển thị nhật ký", + "Show Matrices": "Hiển thị ma trận", + "Show Matrix": "Hiển thị ma trận", + "Show Regions of Interest": "Hiển thị các khu vực quan tâm", + "Show Stream HUD": "Hiển thị luồng HUD", + "Show Thumbnails in Video List": "Hiển thị hình thu nhỏ trong danh sách video", + "Silent": "Im lặng", + "Simple": "Giản dị", + "Size (mb)": "Kích thước (MB)", + "Skip Ping": "Bỏ qua ping", + "Snapshot": "Ảnh chụp nhanh", + "Snapshot Flags": "Cờ nhanh", + "Snapshots": "Ảnh chụp nhanh", + "Sort By": "Sắp xếp theo", + "Space Used": "Không gian được sử dụng", + "Start": "Khởi đầu", + "Start Recording": "Bắt đầu ghi âm", + "Start Time": "Thời gian bắt đầu", + "Start Time cannot be empty.": "Thời gian bắt đầu không thể trống.", + "Started": "Đã bắt đầu", + "Started Building": "Bắt đầu xây dựng", + "Starting": "Bắt đầu", + "State Configuration Not Found": "Cấu hình trạng thái không tìm thấy", + "State Configuration has no monitors associated": "Cấu hình trạng thái không có màn hình liên quan", + "Status Changed": "Trạng thái đã thay đổi", + "Status Indicator": "Chỉ báo trạng thái", + "Stop": "Ngừng lại", + "Stop Command": "Dừng lệnh", + "Stop URL": "Dừng URL", + "Stopped": "Dừng lại", + "Stopping": "Dừng lại", + "Storage Location": "Khu vực lưu trữ", + "Storage Use": "Sử dụng lưu trữ", + "Stream": "Dòng", + "Stream Channel": "CHẠY CHẠY", + "Stream Flags": "Phát trực tuyến cờ", + "Stream Key": "Khóa phát trực tuyến", + "Stream Timestamp": "Phát trực tuyến dấu thời gian", + "Stream Type": "Loại phát trực tuyến", + "Stream Watermark": "Phát trực tiếp hình mờ", + "Stream in Background": "Phát trực tuyến trong nền", + "Stream to YouTube": "Phát trực tuyến trên YouTube", + "Stream to YouTube Flags": "Phát trực tiếp trên các cờ YouTube", + "StreamText": "

Phần này sẽ chỉ định phương pháp chính để phát trực tuyến và cài đặt của nó. Luồng này sẽ được hiển thị trong bảng điều khiển. Nếu bạn chọn sử dụng HLS, JPEG hoặc MJPEG thì bạn có thể tiêu thụ luồng qua các chương trình khác. khung.

", + "Streamed Logs": "Nhật ký phát trực tuyến", + "Streamer": "Streamer", + "Streams": "Dòng", + "Sub-Accounts": "Tài khoản phụ", + "Subdivision": "Phân khu", + "Substream": "Chất nền", + "Substream Process": "Quá trình con", + "SubstreamNotConfigured": "Subream không được cấu hình. Mở cài đặt màn hình của bạn và định cấu hình nó.", + "Subtitle": "Phụ đề", + "Success": "Sự thành công", + "Sunday": "Chủ nhật", + "Superuser": "Superuser", + "Superuser Logs": "Nhật ký Superuser", + "Switch on for Still Image": "Bật hình ảnh tĩnh", + "System": "Hệ thống", + "System Level": "Cấp độ hệ thống", + "TCP": "TCP", + "TV Channel": "Kênh truyền hình", + "TV Channel Group": "Nhóm kênh truyền hình", + "TV Channel ID": "ID kênh truyền hình", + "Telegram": "Telegram", + "Text Box Color": "Màu hộp văn bản", + "Text Color": "Văn bản màu", + "Text criteria unsupported for Object Count tests, Ignoring Conditional": "Tiêu chí văn bản không được hỗ trợ cho các bài kiểm tra số đối tượng, bỏ qua điều kiện", + "Themes": "Chủ đề", + "There are no monitors that you can view with this account.": "Không có màn hình nào mà bạn có thể xem với tài khoản này.", + "Threads": "Chủ đề", + "Thumbnail": "Hình nhỏ", + "Thursday": "thứ năm", + "Time": "Thời gian", + "Time Created": "Thời gian được tạo ra", + "Time Left": "Thời gian còn lại", + "Time Occurred": "Thời gian xảy ra", + "Time-lapse": "Thời gian trôi qua", + "Time-lapse Tool": "Công cụ thời gian trôi đi", + "TimeZone": "Múi giờ", + "Timelapse": "Dòng thời gian", + "Timelapse Frames Share": "Khung thời gian chia sẻ", + "Timelapse Watermark": "Hình mờ theo thời gian", + "Timeout": "Hết giờ", + "Timeout Reset on Next Event": "Đặt lại thời gian chờ vào sự kiện tiếp theo", + "Timeout Reset on Next Motion": "Đặt lại thời gian chờ trên chuyển động tiếp theo", + "Timezone": "Múi giờ", + "Timezone Offset": "Thời gian bù", + "Title": "Tiêu đề", + "Today": "Hôm nay", + "Toggle Sidebar": "Chuyển đổi thanh bên", + "Toggle Substream": "Chuyển đổi chuỗi con", + "Token": "Mã thông báo", + "Top Left": "Trên cùng bên trái", + "Top Right": "Trên cùng bên phải", + "Traditional (Watch-Only, Includes Buffer)": "Truyền thống (chỉ xem, bao gồm bộ đệm)", + "Traditional Recording": "Ghi âm truyền thống", + "Traditional Recording Flags": "Cờ ghi âm truyền thống", + "Train": "Tàu hỏa", + "TrainConfirm": "Bạn có chắc là bạn muốn bắt đầu đào tạo? Điều này có thể mất hơn 12 giờ với hơn 500 hình ảnh. Điều này sẽ tiêu thụ một lượng lớn tài nguyên, như RAM và/hoặc CPU.", + "TrainConfirmStop": "Bạn có chắc là bạn muốn ngừng đào tạo?", + "Trainer Engine": "Động cơ huấn luyện", + "Trigger Blocked": "Kích hoạt bị chặn", + "Trigger Camera Groups": "Kích hoạt nhóm camera", + "Trigger Event": "Sự kiện kích hoạt", + "Trigger Group to Record": "Nhóm kích hoạt để ghi lại", + "Trigger Record": "Kích hoạt hồ sơ", + "Trigger Successful": "Kích hoạt thành công", + "Trigger Threshold": "Ngưỡng kích hoạt", + "Tuesday": "Thứ ba", + "Turn Speed": "Tốc độ quay", + "Type": "Gõ phím", + "UDP": "UDP", + "URL": "URL", + "URL Stop Timeout": "Dừng thời gian chờ URL Chạy URL dừng sau X Milliseconds ", + "US": "CHÚNG TA", + "UTCDateTime": "Ngày tháng", + "Unable to Launch": "Không thể ra mắt", + "UnabletoLaunchText": "Vui lòng lưu màn hình mới trước. Sau đó cố gắng khởi chạy biên tập viên khu vực.", + "Uncommon Objects": "Đối tượng không phổ biến", + "Uniform": "Đồng dạng", + "Unlink": "Hủy bỏ", + "Unlink Login": "Hủy liên kết đăng nhập?", + "Unlinked": "Không liên kết", + "Up": "UP Địa chỉ URL ", + "Up Stop": "Dừng lại Địa chỉ URL ", + "Update": "Cập nhật", + "Update to Development": "Cập nhật để phát triển", + "Update to Master": "Cập nhật để làm chủ", + "Upload Bandwidth": "Tải băng thông", + "Upload File": "Cập nhật dử liệu", + "Uploaded Only": "Chỉ tải lên", + "Uploaders": "Người tải lên", + "Use Built-In": "Sử dụng tích hợp", + "Use Camera Timestamps": "Sử dụng dấu thời gian camera", + "Use Global Amazon S3 Video Storage": "Sử dụng lưu trữ video toàn cầu Amazon S3", + "Use Global Backblaze B2 Video Storage": "Sử dụng lưu trữ video B2 toàn cầu B2", + "Use Global Wasabi Hot Cloud Storage Video Storage": "Sử dụng lưu trữ video lưu trữ đám mây nóng toàn cầu Wasabi", + "Use Global WebDAV Video Storage": "Sử dụng lưu trữ video WebDav toàn cầu", + "Use HTML5 Play Method": "Sử dụng phương thức chơi HTML5", + "Use Max Storage Amount": "Sử dụng số lượng lưu trữ tối đa", + "Use Raw Snapshot": "Sử dụng ảnh chụp nhanh", + "Use Substream": "Sử dụng chuỗi con", + "Use coProcessor": "Sử dụng bộ đồng xử lý", + "UseCount": "Usecount", + "User Log": "Nhật ký người dùng", + "User Not Found": "Người dùng không tìm thấy", + "Username": "tên tài khoản", + "VA-API": "Va-api", + "Value": "Giá trị", + "Video": "Video", + "Video Bit Rate": "Tỷ lệ bit video", + "Video Codec": "Codec video", + "Video Configuration": "Cấu hình video", + "Video Filter": "Bộ lọc video", + "Video Finished": "Video kết thúc", + "Video Length (minutes) and Motion Count per video": "Chiều dài video (phút) và số lượng chuyển động cho mỗi video", + "Video Limit": "Giới hạn video", + "Video Record Rate": "Tỷ lệ bản ghi video", + "Video Set": "VIDEO SET", + "Video Share": "Chia sẻ video", + "Video Status": "Trạng thái video", + "Video and Time Span (Minutes)": "Video và khoảng thời gian (phút)", + "Video stream only from first feed": "Chỉ phát video từ nguồn cấp dữ liệu đầu tiên", + "Video streams only": "Chỉ các luồng video", + "Videos": "Video", + "Videos List": "Danh sách video", + "Videos Merge": "Video hợp nhất", + "Viewing Server Stats": "Xem số liệu thống kê máy chủ", + "Warning": "Cảnh báo", + "Wasabi Hot Cloud Storage": "Lưu trữ đám mây nóng Wasabi", + "Wasabi Hot Cloud Storage Upload Error": "Wasabi Hot Cloud lưu trữ lên lỗi tải lên", + "Watch": "Sự canh gác", + "Watch Only": "Chỉ xem", + "Watch-Only": "Chỉ xem", + "Watching": "Xem", + "Web Page": "Trang web", + "WebDAV": "Webdav", + "WebM (libvpx)": "Webm (libvpx)", + "Webdav Error": "Lỗi webDAV", + "WebdavErrorTextCreatingDir": "Không thể tạo thư mục.", + "WebdavErrorTextTryCreatingDir": "Không thể tiết kiệm. Cố gắng tạo thư mục.", + "Webhook": "Webhook", + "Webhook URL": "URL Webhook", + "Websocket": "WebSocket", + "Websocket Connected": "WebSocket kết nối", + "Websocket Disconnected": "WebSocket bị ngắt kết nối", + "Wednesday": "Thứ Tư", + "Welcome": "Chào mừng!", + "When Detector is Off": "Khi máy dò tắt", + "When Detector is On": "Khi máy dò được bật", + "WhiteBalance": "Cân bằng trắng", + "WideDynamicRange": "Phạm vi động rộng", + "Width": "Chiều rộng", + "X Point": "Điểm x", + "Y Point": "Y điểm", + "Yes": "Đúng", + "Zip and Download": "Zip và tải xuống", + "Zipping Videos": "Video zipping", + "Zones": "Khu vực", + "Zoom In": "Phóng to", + "Zoom In Stop": "Thu phóng trong Stop Địa chỉ URL ", + "Zoom Out": "Thu nhỏ", + "Zoom Out Stop": "Thu phóng Stop Địa chỉ URL ", + "a day": "một ngày", + "a few seconds": "một vài giây", + "a minute": "một phút", + "a month": "một tháng", + "a year": "một năm", + "aac": "AAC", + "aac (Default)": "AAC (mặc định)", + "ac3": "AC3", + "accountActionFailed": "Hành động tài khoản không thành công", + "accountAdded": "Tài khoản được thêm vào", + "accountAddedText": "Tài khoản đã được thêm vào.", + "accountDeleted": "Tài khoản đã bị xóa", + "accountDeletedText": "Tài khoản đã bị xóa.", + "accountId": "ID tài khoản", + "accountSettingsDescription": "Quản lý hồ sơ của bạn và đặt các tùy chọn như số lượng lưu trữ tối đa và số ngày tối đa để giữ video.", + "accountSettingsError": "Lỗi cài đặt tài khoản", + "activatedText": "Cài đặt của bạn đã được kích hoạt.", + "ago": "trước kia", + "airplane": "Máy bay", + "alreadyLinked": "Đã được liên kết với một tài khoản", + "an hour": "một giờ", + "apple": "táo", + "applicationKey": "Khóa ứng dụng", + "aws_accessKeyId": "Truy cập ID khóa", + "aws_secretAccessKey": "Khóa truy cập bí mật", + "backpack": "balo", + "banana": "trái chuối", + "baseball bat": "gậy bóng chày", + "baseball glove": "găng tay bóng chày", + "bear": "con gấu", + "bed": "Giường", + "bench": "Băng ghế", + "bicycle": "Xe đạp", + "bindDN": "Binddn", + "bird": "chim", + "blankPassword": "Để trống để giữ cùng một mật khẩu", + "boat": "thuyền", + "book": "sách", + "bottle": "chai", + "bowl": "bát", + "broccoli": "bông cải xanh", + "bus": "xe buýt", + "cake": "bánh", + "car": "xe ô tô", + "carrot": "Cà rốt", + "cat": "con mèo", + "cell phone": "điện thoại di động", + "chair": "cái ghế", + "clientStreamFailedattemptingReconnect": "Kiểm tra luồng phía máy khách không thành công, cố gắng kết nối lại.", + "clock": "đồng hồ", + "coProcess Crashed for Monitor": "Coprocess bị sập cho màn hình", + "coProcess Unexpected Exit": "Coprocess Lối ra bất ngờ", + "coProcessor": "Coprocessor", + "coProcessor Started": "Coprocessor bắt đầu", + "coProcessor Stopped": "Coprocessor dừng lại", + "coProcessorTextStarted": "Coprocessor đã bắt đầu cho CPU chỉ đầu ra.", + "coProcessorTextStopped": "Coprocessor đã kết thúc.", + "codecMismatchText1": "Máy ảnh của bạn đang cung cấp dữ liệu luồng H.265 (HEVC) và bạn đang sử dụng sao chép làm codec video cho phần luồng. Luồng của bạn từ Shinobi có thể không xuất hiện trên các thiết bị không thể sử dụng codec này. Ứng dụng di động Shinobi có thể xem các luồng này.", + "codecMismatchText2": "Codec video được chọn của bạn không được áp dụng. Máy ảnh của bạn đang cung cấp dữ liệu luồng MJPEG và bạn đang sử dụng sao chép làm codec video cho phần luồng. Thay đổi loại luồng thành MJPEG.", + "codecMismatchText3": "Codec video được chọn của bạn không được áp dụng. Máy ảnh của bạn đang cung cấp dữ liệu luồng MJPEG và bạn đang sử dụng sao chép làm codec video cho phần ghi. Đã thay đổi codec video thành libx264.", + "confirmDeleteFilter": "Bạn có muốn xóa bộ lọc này không? Bạn không thể phục hồi nó.", + "contactAdmin": "Liên hệ với người bảo trì cài đặt Shinobi của bạn.", + "copy": "sao chép", + "couch": "đi văng", + "cow": "bò", + "cuda": "Cuda (Nvidia NVENC)", + "cup": "tách", + "cuvid": "CUVID (NVIDIA NVENC)", + "days": "ngày", + "deleteApiKey": "Xóa khóa API", + "deleteApiKeyText": "Bạn có muốn xóa khóa API này không? Bạn không thể phục hồi nó.", + "deleteMonitorStateText1": "Bạn có muốn xóa các trạng thái màn hình này trước đây không? Các cấu hình màn hình liên quan không thể được phục hồi.", + "deleteMonitorStateText2": "Bạn có muốn xóa cài đặt trước của màn hình này không?", + "deleteScheduleText": "Bạn có muốn xóa lịch trình này không? Các cài đặt trước giám sát được liên kết sẽ không được sửa đổi ..", + "deleteSubAccount": "Xóa tài khoản phụ", + "deleteSubAccountText": "Bạn có muốn xóa tài khoản phụ này không? Bạn không thể phục hồi nó.", + "dining table": "bàn ăn", + "dog": "chú chó", + "donut": "bánh vòng", + "drm": "Chia sẻ đối tượng DRM", + "dropBoxSuccess": "Sự thành công! Các tập tin được lưu vào Dropbox của bạn.", + "dxva2": "DXVA2 (Video DirectX, Windows)", + "elephant": "con voi", + "eventFilterActionText": "Đây là những hành động xảy ra từ các điều kiện lọc đã thành công. \"Lựa chọn ban đầu\" đề cập đến tùy chọn bạn đã chọn trong cài đặt của màn hình.", + "eventFilterErrorBrackets": "Bạn có một số lượng dấu ngoặc. Họ đang bị bỏ qua.", + "eventFiltersDescription": "Bộ lọc thiết lập cho khi các sự kiện xảy ra.", + "failedLoginText1": "Bạn đã không đăng nhập quá nhiều lần. Bạn phải đợi 15 phút trước khi thử lại.", + "failedLoginText2": "Vui lòng kiểm tra thông tin đăng nhập của bạn.", + "fieldMissingValueText1": "Máy ảnh của bạn đang cung cấp dữ liệu luồng MJPEG. Bạn cần đặt tốc độ chụp màn hình. Shinobi sẽ cố gắng phát hiện nó và điền vào nó tự động.", + "fieldTextAccelerator": "Tăng tốc phần cứng (HWACCEL) để giải mã các luồng.", + "fieldTextAcodec": "Codec âm thanh để ghi.", + "fieldTextActionsCommand": "Bạn có thể sử dụng điều này để kích hoạt một tập lệnh trên lệnh.", + "fieldTextActionsHalt": "Làm cho sự kiện không làm gì cả, như thể nó không bao giờ xảy ra.", + "fieldTextActionsIndifference": "Sửa đổi sự thờ ơ tối thiểu cần thiết cho sự kiện.", + "fieldTextActionsRecord": "Sử dụng ghi âm truyền thống, hotswap hoặc xóa bất động với các tùy chọn hiện đang được đặt trong phần Cài đặt phát hiện toàn cầu.", + "fieldTextAduration": "Chỉ định có bao nhiêu micro giây được phân tích để thăm dò đầu vào. Đặt thành 100000 nếu bạn đang sử dụng RTSP và gặp sự cố luồng.", + "fieldTextAudioAlert": "Âm thanh khi sự kiện xảy ra.", + "fieldTextAudioDelay": "Trì hoãn cho đến lần tiếp theo một sự kiện có thể bắt đầu một cảnh báo. Đo bằng giây.", + "fieldTextAudioNote": "Âm thanh khi bong bóng thông tin xuất hiện.", + "fieldTextAutoHost": "URL luồng đầy đủ.", + "fieldTextAutoHostEnable": "Cho các mảnh riêng lẻ cần thiết để xây dựng một URL luồng hoặc cung cấp URL đầy đủ và cho phép Shinobi phân tích nó cho bạn.", + "fieldTextChannelHlsListSize": "Số lượng phân đoạn tối đa trước khi tự động xóa các phân đoạn cũ.", + "fieldTextChannelHlsTime": "Bao lâu mỗi phân đoạn video, trong vài phút. Mỗi phân đoạn sẽ được khách hàng rút ra thông qua một tệp M3U8. Các phân đoạn ngắn hơn mất ít không gian hơn.", + "fieldTextChannelPresetStream": "Cờ đặt trước cho một số bộ mã hóa video nhất định. Nếu bạn thấy máy ảnh của bạn bị sập cứ sau vài giây: hãy thử để trống.", + "fieldTextChannelStreamAcodec": "Codec âm thanh cho phát trực tuyến.", + "fieldTextChannelStreamAcodecAac": "Được sử dụng cho video MP4.", + "fieldTextChannelStreamAcodecAc3": "Được sử dụng cho video MP4.", + "fieldTextChannelStreamAcodecAuto": "Hãy để ffmpeg chọn.", + "fieldTextChannelStreamAcodecCopy": "Được sử dụng cho video MP4. Có cách sử dụng CPU rất thấp nhưng một số codec âm thanh cần các cờ tùy chỉnh như -strict 2 cho AAC.", + "fieldTextChannelStreamAcodecLibmp3lame": "Được sử dụng cho video MP4.", + "fieldTextChannelStreamAcodecLibopus": "Được sử dụng cho video webm.", + "fieldTextChannelStreamAcodecLibvorbis": "Được sử dụng cho video webm.", + "fieldTextChannelStreamAcodecNoAudio": "Không có âm thanh, đây là một lựa chọn phải được đặt ở một số nơi trên thế giới vì lý do pháp lý.", + "fieldTextChannelStreamFps": "Tốc độ mà các khung được hiển thị cho máy khách, tính theo khung mỗi giây. Hãy nhận biết không có mặc định. Điều này có thể dẫn đến việc sử dụng băng thông cao.", + "fieldTextChannelStreamQuality": "Số lượng thấp có nghĩa là chất lượng cao hơn. Số lượng cao hơn có nghĩa là ít chất lượng hơn.", + "fieldTextChannelStreamRotate": "Thay đổi góc xem của luồng video.", + "fieldTextChannelStreamScaleX": "Chiều rộng của hình ảnh luồng là đầu ra sau khi xử lý.", + "fieldTextChannelStreamScaleY": "Chiều cao của hình ảnh luồng là đầu ra sau khi xử lý.", + "fieldTextChannelStreamType": "Phương pháp sẽ được sử dụng để tiêu thụ luồng video.", + "fieldTextChannelStreamTypeFLV": "Gửi các khung được mã hóa FLV qua WebSocket.", + "fieldTextChannelStreamTypeHLS(includesAudio)": "Phương pháp tương tự với các luồng trực tiếp Facebook. bao gồm âm thanh nếu đầu vào cung cấp nó. Có độ trễ khoảng 4-6 giây vì phương pháp này ghi lại các phân đoạn sau đó đẩy chúng đến khách hàng thay vì đẩy như trong khi nó tạo ra chúng.", + "fieldTextChannelStreamTypeMJPEG": "Hình ảnh JPEG chuyển động tiêu chuẩn. Không có âm thanh.", + "fieldTextChannelStreamTypePoseidon": "Poseidon được xây dựng trên mã xử lý MP4 của Kevin Godell. Nó mô phỏng một tệp MP4 phát trực tuyến nhưng sử dụng dữ liệu của luồng trực tiếp. Bao gồm âm thanh. Một số trình duyệt có thể phát nó giống như một tệp MP4 thông thường. Các luồng qua HTTP hoặc WebSocket.", + "fieldTextChannelStreamVcodec": "Video codec để phát trực tuyến.", + "fieldTextChannelStreamVcodecAuto": "Hãy để ffmpeg chọn.", + "fieldTextChannelStreamVcodecCopy": "Được sử dụng cho video MP4. Có cách sử dụng CPU rất thấp nhưng không thể sử dụng các bộ lọc video và tập tin tệp có thể là khổng lồ. Tốt nhất để thiết lập phía camera cài đặt MP4 của bạn khi sử dụng tùy chọn này.", + "fieldTextChannelStreamVcodecLibx264": "Được sử dụng cho video MP4.", + "fieldTextChannelStreamVcodecLibx265": "Được sử dụng cho video MP4.", + "fieldTextChannelSvf": "Đặt các bộ lọc video FFMPEG vào hộp này để ảnh hưởng đến phần phát trực tuyến. Không có khoảng trắng.", + "fieldTextControlInvertY": "Vì khi máy ảnh của bạn được gắn lộn ngược hoặc sử dụng các điều khiển dọc đảo ngược.", + "fieldTextCrf": "Số lượng thấp có nghĩa là chất lượng cao hơn. Số lượng cao hơn có nghĩa là ít chất lượng hơn.", + "fieldTextCustDetect": "Các cờ tùy chỉnh liên kết với máy dò luồng sử dụng để phân tích.", + "fieldTextCustDetectObject": "Các cờ tùy chỉnh liên kết với máy dò luồng sử dụng để phân tích.", + "fieldTextCustInput": "Các cờ tùy chỉnh liên kết với đầu vào của quy trình FFMPEG.", + "fieldTextCustRecord": "Các cờ tùy chỉnh liên kết với việc ghi lại quy trình FFMPEG.", + "fieldTextCustSipRecord": "Các cờ tùy chỉnh liên kết với đầu ra mà các bản ghi dựa trên sự kiện hút từ.", + "fieldTextCustSnap": "Cờ tùy chỉnh liên kết với các ảnh chụp nhanh.", + "fieldTextCustStream": "Các cờ tùy chỉnh liên kết với luồng (chế độ xem phía máy khách) của quy trình FFMPEG.", + "fieldTextCustomOutput": "Thêm đầu ra tùy chỉnh như khung JPEG hoặc gửi dữ liệu thẳng đến máy chủ khác.", + "fieldTextCutoff": "Trong vài phút. Khi nào nên cắt bỏ và bắt đầu một tệp video mới.", + "fieldTextDays": "Số ngày để giữ video trước khi thanh trừng.", + "fieldTextDetailSubstreamInputRtspTransportAuto": "Hãy để FFMPEG quyết định. Thông thường nó sẽ thử UDP trước.", + "fieldTextDetailSubstreamInputRtspTransportTCP": "Đặt nó thành điều này nếu UDP bắt đầu cho kết quả không mong muốn.", + "fieldTextDetailSubstreamInputRtspTransportUDP": "FFMPEG thử điều này trước.", + "fieldTextDetailSubstreamOutputHlsListSize": "Số lượng phân đoạn tối đa trước khi tự động xóa các phân đoạn cũ.", + "fieldTextDetailSubstreamOutputHlsTime": "Bao lâu mỗi phân đoạn video, trong vài phút. Mỗi phân đoạn sẽ được khách hàng rút ra thông qua một tệp M3U8. Các phân đoạn ngắn hơn mất ít không gian hơn.", + "fieldTextDetailSubstreamOutputPresetStream": "Cờ đặt trước cho một số bộ mã hóa video nhất định. Nếu bạn thấy máy ảnh của bạn bị sập cứ sau vài giây: hãy thử để trống.", + "fieldTextDetailSubstreamOutputStreamAcodec": "Codec âm thanh cho phát trực tuyến.", + "fieldTextDetailSubstreamOutputStreamAcodecAac": "Được sử dụng cho video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecAc3": "Được sử dụng cho video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecAuto": "Hãy để ffmpeg chọn.", + "fieldTextDetailSubstreamOutputStreamAcodecCopy": "Được sử dụng cho video MP4. Có cách sử dụng CPU rất thấp nhưng một số codec âm thanh cần các cờ tùy chỉnh như -strict 2 cho AAC.", + "fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame": "Được sử dụng cho video MP4.", + "fieldTextDetailSubstreamOutputStreamAcodecLibopus": "Được sử dụng cho video webm.", + "fieldTextDetailSubstreamOutputStreamAcodecLibvorbis": "Được sử dụng cho video webm.", + "fieldTextDetailSubstreamOutputStreamAcodecNoAudio": "Không có âm thanh, đây là một lựa chọn phải được đặt ở một số nơi trên thế giới vì lý do pháp lý.", + "fieldTextDetailSubstreamOutputStreamFps": "Tốc độ mà các khung được hiển thị cho máy khách, tính theo khung mỗi giây. Hãy nhận biết không có mặc định. Điều này có thể dẫn đến việc sử dụng băng thông cao.", + "fieldTextDetailSubstreamOutputStreamQuality": "Số lượng thấp có nghĩa là chất lượng cao hơn. Số lượng cao hơn có nghĩa là ít chất lượng hơn.", + "fieldTextDetailSubstreamOutputStreamRotate": "Thay đổi góc xem của luồng video.", + "fieldTextDetailSubstreamOutputStreamScaleX": "Chiều rộng của hình ảnh luồng là đầu ra sau khi xử lý.", + "fieldTextDetailSubstreamOutputStreamScaleY": "Chiều cao của hình ảnh luồng là đầu ra sau khi xử lý.", + "fieldTextDetailSubstreamOutputStreamType": "Phương pháp sẽ được sử dụng để tiêu thụ luồng video.", + "fieldTextDetailSubstreamOutputStreamVcodec": "Video codec để phát trực tuyến.", + "fieldTextDetailSubstreamOutputStreamVcodecAuto": "Hãy để ffmpeg chọn.", + "fieldTextDetailSubstreamOutputStreamVcodecCopy": "Được sử dụng cho video MP4. Có cách sử dụng CPU rất thấp nhưng không thể sử dụng các bộ lọc video và tập tin tệp có thể là khổng lồ. Tốt nhất để thiết lập phía camera cài đặt MP4 của bạn khi sử dụng tùy chọn này.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx264": "Được sử dụng cho video MP4.", + "fieldTextDetailSubstreamOutputStreamVcodecLibx265": "Được sử dụng cho video MP4.", + "fieldTextDetailSubstreamOutputSvf": "Đặt các bộ lọc video FFMPEG vào hộp này để ảnh hưởng đến phần phát trực tuyến. Không có khoảng trắng.", + "fieldTextDetector": "Điều này sẽ thêm một đầu ra khác trong lệnh FFMPEG cho máy dò chuyển động.", + "fieldTextDetectorAudio": "Kiểm tra xem âm thanh đã xảy ra tại một certiain có thể giải quyết được. Đọc có thể phân giải có thể không chính xác để đo lường trong thế giới thực.", + "fieldTextDetectorBufferHlsListSize": "Số lượng phân đoạn tối đa trước khi tự động xóa các phân đoạn cũ.", + "fieldTextDetectorBufferHlsTime": "Bao lâu mỗi phân đoạn video, trong vài giây. Mỗi phân đoạn sẽ được khách hàng rút ra thông qua một tệp M3U8. Các phân đoạn ngắn hơn mất ít không gian hơn.", + "fieldTextDetectorColorThreshold": "Lượng chênh lệch cho phép trong một pixel trước khi nó được coi là chuyển động.", + "fieldTextDetectorCommand": "Lệnh sẽ chạy. Đây là tương đương với việc chạy một lệnh shell từ thiết bị đầu cuối.", + "fieldTextDetectorCommandTimeout": "Giá trị này là một bộ đếm thời gian để cho phép chạy tập lệnh tiếp theo của bạn. Giá trị này là trong vài phút.", + "fieldTextDetectorFps": "Có bao nhiêu khung hình một giây để gửi đến máy dò chuyển động; 2 là mặc định.", + "fieldTextDetectorFrame": "Điều này sẽ đọc toàn bộ khung cho sự khác biệt pixel. Điều này giống như tạo ra một khu vực bao gồm toàn bộ màn hình.", + "fieldTextDetectorHttpApi": "Bạn có muốn cho phép kích hoạt HTTP cho máy ảnh này không?", + "fieldTextDetectorLisencePlate": "Cho phép nhận dạng biển số xe. Plugin OpenalPR có điều này luôn được bật.", + "fieldTextDetectorLisencePlateCountry": "Chọn loại tấm để nhận ra. Chỉ có chúng tôi và EU được hỗ trợ tại thời điểm này.", + "fieldTextDetectorLockTimeout": "Khóa khi được cho phép kích hoạt tiếp theo, để tránh quá tải cơ sở dữ liệu và nhận máy khách. Được đo bằng mili giây.", + "fieldTextDetectorMaxSensitivity": "Xếp hạng tự tin chuyển động phải thấp hơn giá trị này được coi là một kích hoạt. Để trống mà không có tối đa. Tùy chọn này trước đây đã được đặt tên là \"Max DeDiferference\".", + "fieldTextDetectorNoiseFilter": "Cố gắng lọc hạt hoặc chuyển động lặp đi lặp lại ở một sự thờ ơ cụ thể.", + "fieldTextDetectorNoiseFilterRange": "Lượng chênh lệch cho phép trong một pixel trước khi nó được coi là chuyển động.", + "fieldTextDetectorNotrigger": "Kiểm tra nếu chuyển động đã xảy ra trong một khoảng thời gian. Nếu chuyển động đã xảy ra, séc sẽ được đặt lại.", + "fieldTextDetectorNotriggerCommand": "Lệnh sẽ chạy. Đây là tương đương với việc chạy một lệnh shell từ thiết bị đầu cuối.", + "fieldTextDetectorNotriggerCommandTimeout": "Giá trị này là một bộ đếm thời gian để cho phép chạy tập lệnh tiếp theo của bạn. Giá trị này là trong vài phút.", + "fieldTextDetectorNotriggerDiscord": "Nếu chuyển động chưa được phát hiện sau khoảng thời gian chờ, bạn sẽ nhận được thông báo bất hòa.", + "fieldTextDetectorNotriggerTimeout": "Thời gian chờ được tính trong vài phút.", + "fieldTextDetectorNotriggerWebhook": "Gửi yêu cầu nhận đến URL với một số giá trị từ sự kiện.", + "fieldTextDetectorObjCount": "Đếm các đối tượng được phát hiện.", + "fieldTextDetectorObjCountInRegion": "Đếm các đối tượng chỉ bên trong các vùng.", + "fieldTextDetectorPam": "Sử dụng máy dò chuyển động của Kevin Godell. Điều này được tích hợp vào Shinobi và không yêu cầu cấu hình khác để kích hoạt.", + "fieldTextDetectorPtzFollow": "Thực hiện theo đối tượng được phát hiện lớn nhất với PTZ? Yêu cầu một máy dò đối tượng chạy hoặc ma trận được cung cấp với các sự kiện.", + "fieldTextDetectorRecordMethod": "Có nhiều cách để bắt đầu ghi lại khi một sự kiện xảy ra, như chuyển động. Ghi âm truyền thống là thân thiện nhất với người dùng.", + "fieldTextDetectorSave": "Lưu các sự kiện chuyển động trong SQL. Điều này sẽ cho phép hiển thị chuyển động qua video trong thời gian các sự kiện chuyển động xảy ra trong trình xem Power.", + "fieldTextDetectorScaleX": "Chiều rộng của hình ảnh được phát hiện. Kích thước nhỏ hơn mất ít CPU hơn.", + "fieldTextDetectorScaleY": "Chiều cao của hình ảnh được phát hiện. Kích thước nhỏ hơn mất ít CPU hơn.", + "fieldTextDetectorSendFrames": "Đẩy khung vào plugin được kết nối để phân tích.", + "fieldTextDetectorSendFramesObject": "Đẩy khung vào plugin được kết nối để phân tích.", + "fieldTextDetectorSendVideoLength": "Trong vài giây. Độ dài của video được gửi đến dịch vụ thông báo của bạn, như email hoặc Discord.", + "fieldTextDetectorSensitivity": "Xếp hạng tự tin chuyển động phải vượt quá giá trị này để được coi là một kích hoạt. Số này tương quan trực tiếp với xếp hạng độ tin cậy được trả về bởi máy dò chuyển động. Tùy chọn này trước đây được đặt tên là \"Sự thờ ơ\".", + "fieldTextDetectorThreshold": "Số lượng phát hiện tối thiểu để bắn một sự kiện chuyển động. Phát hiện phải nằm trong máy dò, ngưỡng chia cho máy dò giây FPS. Ví dụ: nếu FPS của máy dò là 2 và ngưỡng kích hoạt là 3, thì ba phát hiện phải xảy ra trong vòng 1,5 giây để kích hoạt sự kiện chuyển động. Ngưỡng này là khu vực phát hiện.", + "fieldTextDetectorTimeout": "Khoảng thời gian của \"bản ghi kích hoạt\" sẽ chạy cho. Điều này được đọc trong vài phút.", + "fieldTextDetectorTrigger": "Điều này sẽ đặt hàng máy ảnh để ghi nếu nó được đặt thành \"chỉ xem\" khi phát hiện sự kiện.", + "fieldTextDetectorUseDetectObject": "Tạo khung để gửi đến bất kỳ plugin được kết nối.", + "fieldTextDetectorWebhook": "Gửi yêu cầu nhận đến URL với một số giá trị từ sự kiện.", + "fieldTextDetectorWebhookTimeout": "Giá trị này là một bộ đếm thời gian để cho phép chạy webhook tiếp theo của bạn. Giá trị này là trong vài phút.", + "fieldTextDir": "Vị trí của nơi các tập tin được ghi sẽ được lưu. Bạn có thể định cấu hình nhiều vị trí hơn với biến addStorage .", + "fieldTextEventDays": "Số ngày để giữ các sự kiện trước khi thanh trừng.", + "fieldTextEventMonPop": "Khi một sự kiện xảy ra bật lên luồng màn hình.", + "fieldTextEventRecordScaleX": "Chiều rộng của hình ảnh ghi dựa trên sự kiện là đầu ra sau khi xử lý.", + "fieldTextEventRecordScaleY": "Chiều cao của hình ảnh ghi dựa trên sự kiện là đầu ra sau khi xử lý.", + "fieldTextExt": "Loại tệp cho tệp video được ghi của bạn.", + "fieldTextExtMP4": "Loại tệp này có thể chơi được gần như tất cả các trình duyệt web hiện đại, bao gồm cả thiết bị di động. Các tập tin chỉ có xu hướng lớn hơn trừ khi bạn giảm chất lượng.", + "fieldTextExtWebM": "Tệp nhỏ, khả năng tương thích của khách hàng thấp. Tốt để tải lên các trang web như YouTube.", + "fieldTextFactorAuth": "Kích hoạt yêu cầu thứ cấp để đăng nhập thông qua một trong các phương thức đã bật.", + "fieldTextFatalMax": "Số lần thử lại cho kết nối mạng giữa máy chủ và máy ảnh trước khi đặt màn hình thành bị vô hiệu hóa. Không có số thập phân. Đặt thành 0 thành thử lại mãi mãi.", + "fieldTextFps": "Tốc độ trong đó các khung được ghi vào các tệp, khung mỗi giây. Hãy nhận biết không có mặc định. Điều này có thể dẫn đến các tập tin lớn. Tốt nhất để đặt phía máy ảnh này.", + "fieldTextHeight": "Chiều cao của hình ảnh luồng.", + "fieldTextHlsListSize": "Số lượng phân đoạn tối đa trước khi tự động xóa các phân đoạn cũ.", + "fieldTextHlsTime": "Bao lâu mỗi phân đoạn video, trong vài phút. Mỗi phân đoạn sẽ được khách hàng rút ra thông qua một tệp M3U8. Các phân đoạn ngắn hơn mất ít không gian hơn.", + "fieldTextHost": "Địa chỉ kết nối", + "fieldTextHwaccel": "Công cụ giải mã", + "fieldTextHwaccelVcodec": "Công cụ giải mã", + "fieldTextInverseTrigger": "Để kích hoạt các vùng được chỉ định bên ngoài. Sẽ không kích hoạt với phát hiện khung đầy đủ được bật.", + "fieldTextIp": "Phạm vi hoặc đơn", + "fieldTextIrCutFilterAuto": "Bộ lọc cắt IR được kích hoạt tự động bởi thiết bị.", + "fieldTextIrCutFilterOff": "Tắt ir cắt fiter. Thông thường chế độ ban đêm.", + "fieldTextIrCutFilterOn": "Bật IR Cut Fiter. Thông thường chế độ ban ngày.", + "fieldTextIsOnvif": "Đây có phải là một máy ảnh tuân thủ ONVIF?", + "fieldTextLang": "Ngôn ngữ chính của các yếu tố văn bản. Để dịch hoàn chỉnh, hãy thêm ngôn ngữ của bạn trong conf.json, ví dụ: \"ngôn ngữ\": \"en_ca\", ", + "fieldTextLogDays": "Số ngày để giữ nhật ký trước khi thanh trừng.", + "fieldTextLoglevel": "Số lượng dữ liệu để cung cấp trong khi thực hiện công việc.", + "fieldTextLoglevelAllWarnings": "Hiển thị tất cả các cảnh báo. Sử dụng điều này nếu bạn không thể tìm ra điều gì sai với máy ảnh của mình.", + "fieldTextLoglevelFatal": "Chỉ hiển thị các lỗi gây tử vong.", + "fieldTextLoglevelOnError": "Hiển thị tất cả các lỗi quan trọng. Lưu ý: Điều này không phải lúc nào cũng hiển thị thông tin quan trọng.", + "fieldTextLoglevelSilent": "Không có. Điều này sẽ im lặng tất cả ghi nhật ký.", + "fieldTextMail": "Đăng nhập cho tài khoản. Địa chỉ email của chủ tài khoản chính sẽ nhận được thông báo.", + "fieldTextMapRtspTransportAuto": "Hãy để FFMPEG quyết định. Thông thường nó sẽ thử UDP trước.", + "fieldTextMapRtspTransportTCP": "Đặt nó thành điều này nếu UDP bắt đầu cho kết quả không mong muốn.", + "fieldTextMapRtspTransportUDP": "FFMPEG thử điều này trước.", + "fieldTextMaxKeepDays": "Số ngày để giữ video trước khi thanh lọc cho màn hình này cụ thể.", + "fieldTextMid": "Đây là một định danh không thể thay đổi cho màn hình. Bạn có thể sao chép một màn hình bằng cách nhấp đúp vào ID màn hình và thay đổi nó.", + "fieldTextMode": "Đây là nhiệm vụ chính của màn hình.", + "fieldTextModeDisabled": "Màn hình không hoạt động, không có quy trình nào sẽ được tạo trong chế độ này.", + "fieldTextModeRecord": "Ghi âm liên tục. Các phân đoạn được thực hiện cứ sau 15 phút theo mặc định.", + "fieldTextModeWatchOnly": "Màn hình sẽ chỉ phát trực tuyến, không có bản ghi nào sẽ xảy ra trừ khi API hoặc máy dò được đặt hàng khác.", + "fieldTextMpass": "Mật khẩu cho máy ảnh của bạn", + "fieldTextMuser": "Người dùng đăng nhập cho máy ảnh của bạn", + "fieldTextName": "Đây là tên hiển thị có thể đọc được cho con người cho màn hình.", + "fieldTextNotes": "Nhận xét bạn muốn để lại cho máy ảnh này.", + "fieldTextOnvifNonStandard": "Đây có phải là một máy ảnh ONVIF không chuẩn?", + "fieldTextOnvifPort": "ONVIF thường được chạy trên cổng 8000 . Điều này có thể là 80 cũng như tùy thuộc vào mô hình camera của bạn.", + "fieldTextPass": "Để trống để giữ cùng một mật khẩu trong quá trình sửa đổi cài đặt.", + "fieldTextPasswordAgain": "Phải phù hợp với trường mật khẩu nếu bạn muốn thay đổi nó.", + "fieldTextPath": "Đường dẫn đến máy ảnh của bạn", + "fieldTextPort": "Tách biệt bằng dấu phẩy hoặc một phạm vi", + "fieldTextPortForce": "Sử dụng cổng web mặc định có thể cho phép chuyển đổi tự động sang các cổng khác cho các luồng như RTSP.", + "fieldTextPresetRecord": "Cờ đặt trước cho một số bộ mã hóa video nhất định. Nếu bạn thấy máy ảnh của bạn bị sập cứ sau vài giây: hãy thử để trống.", + "fieldTextPresetStream": "Cờ đặt trước cho một số bộ mã hóa video nhất định. Nếu bạn thấy máy ảnh của bạn bị sập cứ sau vài giây: hãy thử để trống.", + "fieldTextProbesize": "Chỉ định mức độ lớn để làm cho đầu dò phân tích cho đầu vào. Đặt thành 100000 nếu bạn đang sử dụng RTSP và gặp sự cố luồng.", + "fieldTextProtocol": "Giao thức sẽ được sử dụng để tiêu thụ luồng video.", + "fieldTextRecordScaleX": "Chiều rộng của hình ảnh luồng.", + "fieldTextRecordScaleY": "Chiều cao của hình ảnh luồng.", + "fieldTextRecordTimelapse": "Tạo một thời gian dựa trên JPEG.", + "fieldTextRecordTimelapseMp4": "Tạo một tệp MP4 vào cuối mỗi ngày cho Timelapse.", + "fieldTextRecordTimelapseWatermark": "Một hình ảnh được ghi vào khung của video được ghi.", + "fieldTextRecordTimelapseWatermarkLocation": "Vị trí hình ảnh sẽ được sử dụng làm hình mờ.", + "fieldTextRecordTimelapseWatermarkPosition": "Một hình ảnh được ghi vào khung của video được ghi.", + "fieldTextRotate": "Thay đổi góc ghi của luồng video.", + "fieldTextRtmpKey": "Khóa luồng cho các luồng đến trên cổng RTMP.", + "fieldTextRtspTransport": "Giao thức vận chuyển máy ảnh của bạn sẽ sử dụng. TCP thường là sự lựa chọn tốt nhất.", + "fieldTextRtspTransportAuto": "Hãy để FFMPEG quyết định. Thông thường nó sẽ thử UDP trước.", + "fieldTextRtspTransportHTTP": "Phương pháp kết nối tiêu chuẩn.", + "fieldTextRtspTransportTCP": "Đặt nó thành điều này nếu UDP bắt đầu cho kết quả không mong muốn.", + "fieldTextRtspTransportUDP": "FFMPEG thử điều này trước.", + "fieldTextSfps": "Chỉ định tốc độ khung hình (FPS) trong đó máy ảnh đang cung cấp luồng của nó.", + "fieldTextSignalCheck": "Tần suất khách hàng của bạn sẽ kiểm tra luồng để xem nó có còn sống không. Điều này được tính toán trong vài phút.", + "fieldTextSignalCheckLog": "Điều này chỉ dành cho phía khách hàng. Nó sẽ hiển thị trong luồng nhật ký khi kiểm tra tín hiệu phía máy khách xảy ra.", + "fieldTextSize": "Lượng không gian đĩa shinobi sẽ cho phép tiêu thụ trước khi thanh lọc. Giá trị này được đọc trong megabyte.", + "fieldTextSizeFilebinPercent": "Phần trăm số lượng lưu trữ tối đa mà kho lưu trữ FileBin có thể sử dụng.", + "fieldTextSizeTimelapsePercent": "Phần trăm số lượng lưu trữ tối đa mà các khung thời gian có thể ghi lại.", + "fieldTextSizeVideoPercent": "Phần trăm số lượng lưu trữ tối đa mà các video có thể ghi lại.", + "fieldTextSkipPing": "Chọn nếu một ping thành công được yêu cầu trước khi quá trình giám sát được bắt đầu.", + "fieldTextSnap": "Nhận khung mới nhất trong JPEG.", + "fieldTextSnapSecondsInward": "trong vài giây", + "fieldTextSqllog": "Sử dụng điều này một cách thận trọng vì FFMPEG thích ném dữ liệu thừa vào những thời điểm có thể dẫn đến rất nhiều hàng cơ sở dữ liệu.", + "fieldTextSqllogNo": "Không là mặc định.", + "fieldTextSqllogYes": "Làm điều này nếu bạn chỉ gặp vấn đề định kỳ.", + "fieldTextStreamAcodec": "Codec âm thanh cho phát trực tuyến.", + "fieldTextStreamAcodecAac": "Được sử dụng cho video MP4.", + "fieldTextStreamAcodecAc3": "Được sử dụng cho video MP4.", + "fieldTextStreamAcodecAuto": "Hãy để ffmpeg chọn.", + "fieldTextStreamAcodecCopy": "Được sử dụng cho video MP4. Có cách sử dụng CPU rất thấp nhưng một số codec âm thanh cần các cờ tùy chỉnh như -strict 2 cho AAC.", + "fieldTextStreamAcodecLibmp3lame": "Được sử dụng cho video MP4.", + "fieldTextStreamAcodecLibopus": "Được sử dụng cho video webm.", + "fieldTextStreamAcodecLibvorbis": "Được sử dụng cho video webm.", + "fieldTextStreamAcodecNoAudio": "Không có âm thanh, đây là một lựa chọn phải được đặt ở một số nơi trên thế giới vì lý do pháp lý.", + "fieldTextStreamFlvType": "Đây chỉ dành cho bảng điều khiển Shinobi. Cả hai phương thức luồng vẫn đang hoạt động và sẵn sàng để sử dụng.", + "fieldTextStreamFps": "Tốc độ mà các khung được hiển thị cho máy khách, tính theo khung mỗi giây. Hãy nhận biết không có mặc định. Điều này có thể dẫn đến việc sử dụng băng thông cao.", + "fieldTextStreamLoop": "Viết một tệp tĩnh để luồng tệp hoạt động giống như một luồng trực tiếp.", + "fieldTextStreamQuality": "Số lượng thấp có nghĩa là chất lượng cao hơn. Số lượng cao hơn có nghĩa là ít chất lượng hơn.", + "fieldTextStreamRotate": "Thay đổi góc xem của luồng video.", + "fieldTextStreamScaleX": "Chiều rộng của hình ảnh luồng là đầu ra sau khi xử lý.", + "fieldTextStreamScaleY": "Chiều cao của hình ảnh luồng là đầu ra sau khi xử lý.", + "fieldTextStreamTimestamp": "Một chiếc đồng hồ được ghi vào khung của luồng video.", + "fieldTextStreamTimestampBoxColor": "Màu sắc thời gian Timstamp.", + "fieldTextStreamTimestampColor": "Màu văn bản Timstamp.", + "fieldTextStreamTimestampFont": "Tệp phông chữ để tạo kiểu thời gian của bạn.", + "fieldTextStreamTimestampFontSize": "Kích thước phông chữ trong Pt.", + "fieldTextStreamTimestampX": "Vị trí thời gian của thời gian", + "fieldTextStreamTimestampY": "Vị trí dọc của dấu thời gian", + "fieldTextStreamType": "Phương pháp sẽ được sử dụng để tiêu thụ luồng video.", + "fieldTextStreamTypeBase64OverWebsocket": "Gửi các khung được mã hóa Base64 qua WebSocket. Điều này tránh bộ nhớ đệm nhưng không có âm thanh.", + "fieldTextStreamTypeFLV": "Gửi các khung được mã hóa FLV qua WebSocket.", + "fieldTextStreamTypeHLS(includesAudio)": "Phương pháp tương tự với các luồng trực tiếp Facebook. bao gồm âm thanh nếu đầu vào cung cấp nó. Có độ trễ khoảng 4-6 giây vì phương pháp này ghi lại các phân đoạn sau đó đẩy chúng đến khách hàng thay vì đẩy như trong khi nó tạo ra chúng.", + "fieldTextStreamTypeMJPEG": "Hình ảnh JPEG chuyển động tiêu chuẩn. Không có âm thanh.", + "fieldTextStreamTypePoseidon": "Poseidon được xây dựng trên mã xử lý MP4 của Kevin Godell. Nó mô phỏng một tệp MP4 phát trực tuyến nhưng sử dụng dữ liệu của luồng trực tiếp. Bao gồm âm thanh. Một số trình duyệt có thể phát nó giống như một tệp MP4 thông thường. Các luồng qua HTTP hoặc WebSocket.", + "fieldTextStreamVcodec": "Video codec để phát trực tuyến.", + "fieldTextStreamVcodecAuto": "Hãy để ffmpeg chọn.", + "fieldTextStreamVcodecCopy": "Được sử dụng cho video MP4. Có cách sử dụng CPU rất thấp nhưng không thể sử dụng các bộ lọc video và tập tin tệp có thể là khổng lồ. Tốt nhất để thiết lập phía camera cài đặt MP4 của bạn khi sử dụng tùy chọn này.", + "fieldTextStreamVcodecLibx264": "Được sử dụng cho video MP4.", + "fieldTextStreamVcodecLibx265": "Được sử dụng cho video MP4.", + "fieldTextStreamVf": "Đặt các bộ lọc video FFMPEG vào hộp này để ảnh hưởng đến phần phát trực tuyến. Không có khoảng trắng.", + "fieldTextStreamWatermark": "Một hình ảnh được ghi vào khung của luồng video.", + "fieldTextStreamWatermarkLocation": "Vị trí hình ảnh sẽ được sử dụng làm hình mờ.", + "fieldTextStreamWatermarkPosition": "Một hình ảnh được ghi vào khung của luồng video.", + "fieldTextTimestamp": "Một chiếc đồng hồ được ghi vào các khung của video được ghi lại.", + "fieldTextTimestampBoxColor": "Màu sắc thời gian Timstamp.", + "fieldTextTimestampColor": "Màu văn bản Timstamp.", + "fieldTextTimestampFont": "Tệp phông chữ để tạo kiểu thời gian của bạn.", + "fieldTextTimestampFontSize": "Kích thước phông chữ trong Pt.", + "fieldTextTimestampX": "Vị trí thời gian của thời gian", + "fieldTextTimestampY": "Vị trí dọc của dấu thời gian", + "fieldTextTvChannel": "Màn hình này sẽ có các tính năng kênh TV được bật. Bạn sẽ có thể xem nó trong danh sách kênh TV của bạn.", + "fieldTextTvChannelGroupTitle": "Một nhóm tùy chỉnh cho kênh.", + "fieldTextTvChannelId": "Một ID tùy chỉnh cho kênh.", + "fieldTextType": "Phương pháp sẽ được sử dụng để tiêu thụ luồng video.", + "fieldTextTypeDashcam(StreamerV2)": "Luồng P2P dựa trên webm Webm.", + "fieldTextTypeH.264/H.265/H.265+": "Đọc một video video chất lượng cao đôi khi bao gồm âm thanh.", + "fieldTextTypeHLS(.m3u8)": "Đọc một video video chất lượng cao đôi khi bao gồm âm thanh.", + "fieldTextTypeJPEG": "Đọc ảnh chụp nhanh từ URL và tạo luồng và/hoặc video từ chúng.", + "fieldTextTypeLocal": "Đọc thẻ chụp, webcam hoặc camera tích hợp.", + "fieldTextTypeMJPEG": "Tương tự như JPEG ngoại trừ việc xử lý khung được thực hiện bởi FFMPEG, không phải shinobi.", + "fieldTextTypeMPEG4(.mp4/.ts)": "Một tập tin tĩnh. Đọc với tốc độ thấp hơn và không nên được sử dụng cho một luồng trực tiếp thực tế.", + "fieldTextTypeMxPEG": "Mobotix MJPEG Stream", + "fieldTextTypeRTMP": "Tìm hiểu để kết nối tại đây: bài viết: Cách đẩy các luồng qua RTMP đến Shinobi ", + "fieldTextTypeShinobiStreamer": "Luồng P2P dựa trên WebSocket JPEG.", + "fieldTextVcodec": "Video codec để ghi.", + "fieldTextVf": "Đặt các bộ lọc video FFMPEG vào hộp này để ảnh hưởng đến phần ghi. Không có khoảng trắng.", + "fieldTextWallClockTimestampIgnore": "Dựa trên tất cả dữ liệu camera đến trong thời gian camera thay vì thời gian máy chủ.", + "fieldTextWatchdogReset": "Nếu có sự chồng chéo trong bản ghi kích hoạt nên đặt lại.", + "fieldTextWatermark": "Một hình ảnh được ghi vào khung của video được ghi.", + "fieldTextWatermarkLocation": "Vị trí hình ảnh sẽ được sử dụng làm hình mờ.", + "fieldTextWatermarkPosition": "Một hình ảnh được ghi vào khung của video được ghi.", + "fieldTextWidth": "Chiều rộng của hình ảnh luồng.", + "fire hydrant": "vòi chữa cháy", + "flv": "flv", + "for Global Access": "để truy cập toàn cầu", + "fork": "cái nĩa", + "frisbee": "chiếc dĩa nhựa ném", + "getAMonitor": "Nhận một màn hình", + "getATvChannel": "Nhận các kênh truyền hình cho màn hình", + "getATvChannelText": "Nhận một màn hình H.264 có sẵn trong danh sách phát .M3U8.", + "getAllMonitors": "Nhận tất cả các màn hình", + "getAllTvChannels": "Nhận tất cả các kênh TV", + "getAllTvChannelsText": "Nhận tất cả các luồng H.264 có sẵn trong danh sách phát .M3U8. Kích hoạt tùy chọn kênh TV trong cài đặt màn hình của bạn để xem các luồng của chúng trong danh sách này.", + "getUserInfo": "Nhận thông tin người dùng", + "getVideos": "Nhận video", + "getVideosForMonitor": "Nhận video cho màn hình", + "giraffe": "hươu cao cổ", + "h264_cuvid": "H.264 CUVID", + "h264_mmal": "H.264 (Raspberry Pi)", + "h264_nvenc": "H.264 NVENC (Nvidia HW Accel)", + "h264_omx": "H.264 OpenMax (Raspberry Pi)", + "h264_qsv": "H.264 (Video đồng bộ nhanh)", + "h264_vaapi": "H.264 VA-API (Intel HW Accel)", + "h265BrowserText1": "Nếu bạn đang cố gắng phát một tệp H.265, bạn có thể cần tải xuống và mở nó trong một ứng dụng khác như VLC.", + "hair drier": "máy sấy tóc", + "handbag": "túi xách tay", + "hevc_cuvid": "H.265 CUVID", + "hevc_nvenc": "H.265 NVENC (Nvidia HW Accel)", + "hevc_qsv": "H.265 (Video đồng bộ nhanh)", + "hevc_vaapi": "H.265 VA-API (Intel HW Accel)", + "hlsOptions": "Tùy chọn HLS", + "hlsOptionsInvalid": "Tùy chọn HLS không hợp lệ", + "horse": "con ngựa", + "hot dog": "bánh mì kẹp xúc xích", + "hour": "giờ", + "hours": "giờ", + "hwaccel": "Động cơ tăng tốc", + "hwaccel_device": "Thiết bị Hwaccel", + "hwaccel_vcodec": "Bộ giải mã video", + "in": "trong", + "in Days": "trong những ngày", + "in seconds": "trong vài giây", + "keyId": "ID chính", + "keyboard": "bàn phím", + "kite": "cánh diều", + "knife": "dao", + "laptop": "máy tính xách tay", + "lastLogin": "Lân đăng nhập cuôi", + "libmp3lame": "libmp3lame", + "libopus": "Libopus", + "libvorbis (Default)": "libvorbis (mặc định)", + "libvpx (Default)": "libvpx (mặc định)", + "libvpx-vp9": "libvpx-vp9", + "libx264": "libx264", + "libx264 (Default)": "libx264 (mặc định)", + "libx265": "libx265", + "liveGridDescription": "Live Grid là màn hình nhiều luồng cho Shinobi. Phương pháp xem này chủ yếu được thiết kế cho máy tính để bàn.", + "loginHandleUnbound": "Đăng nhập đã được liên kết từ tài khoản này.", + "microwave": "lò vi sóng", + "migrateText1": " Loại đầu vào không thể được phân tích cú pháp. Vui lòng đặt nó theo cách thủ công.", + "minute": "phút", + "minutes": "phút", + "mjpeg_cuvid": "MJPEG CUVID", + "modifyVideoText1": "Phương pháp không tồn tại. Kiểm tra để đảm bảo rằng giá trị cuối cùng của URL không trống.", + "monSavedButNotCopied": "Màn hình của bạn đã được lưu nhưng không được sao chép vào bất kỳ màn hình nào khác.", + "monitorConfigFinderDescription": "Công cụ này sẽ giúp bạn tìm kiếm cấu hình cho máy ảnh được đăng bởi cộng đồng. Tất cả được lưu trữ trên shinobihub . Bạn cũng có thể đăng của bạn, nó thực sự sẽ giúp cộng đồng :)", + "monitorEditFailedMaxReached": "Tài khoản của bạn đã đạt đến số lượng máy ảnh tối đa có thể được tạo. Nói chuyện với một quản trị viên nếu bạn muốn điều này đã thay đổi.", + "monitorEditText1": "Dữ liệu không hợp lệ, kiểm tra xem đây là một chuỗi nhập hợp lệ.", + "monitorEditText2": "Chuỗi chi tiết không hợp lệ. Kiểm tra xem đó là một chuỗi JSON và không phải là một đối tượng thông thường được truyền.", + "monitorGetText1": "Yêu cầu không đầy đủ, xóa dấu gạch chéo cuối cùng trong URL hoặc đặt giá trị chấp nhận được.", + "monitorStateNotEnoughChanges": "Bạn cần thực hiện thay đổi trong cấu hình màn hình của mình trước khi cố gắng thêm nó vào cài đặt trước.", + "monitorStatesError": "Giám sát lỗi đặt trước", + "months": "tháng", + "motorcycle": "xe máy", + "mouse": "con chuột", + "mpeg2_mmal": "MPEG-2 (Raspberry Pi)", + "mpeg2_qsv": "MPEG2 (Video đồng bộ nhanh)", + "mpeg4_cuvid": "MPEG4 CUVID", + "mpeg4_mmal": "MPEG-4 (Raspberry Pi)", + "noLoginTokensAdded": "Không có thông tin đăng nhập thay thế liên quan đến tài khoản này.", + "noSpecialCharacters": "Không có không gian hoặc ký tự đặc biệt.", + "noTriggerText": "Nếu chuyển động chưa được phát hiện sau khoảng thời gian chờ, bạn sẽ nhận được thông báo bất hòa.", + "noUndoForAction": "Bạn không thể hoàn tác hành động này.", + "notActivatedText": "Cài đặt của bạn đã không kích hoạt.", + "notEnoughFramesText1": "Không đủ khung để biên dịch.", + "notPermitted1": "Hành động này không được quản trị viên của tài khoản của bạn cho phép. '", + "on": "trên", + "on Error": "về lỗi", + "on Event": "về sự kiện", + "onvifdeviceManagerGlobalTip": "ONVIF cho phép sửa đổi cài đặt nội bộ của máy ảnh. Onvif là một phần của một thuật ngữ ô, nó có thể có nghĩa là nhiều điều không may. Với trường hợp đó, bạn có thể thấy một tùy chọn trong công cụ này nhưng nó có thể không thể chỉnh sửa được. Điều này thường là do nhà cung cấp máy ảnh không thêm phương pháp này hoặc đã đi chệch khỏi mức sử dụng dự định của nó. Trong những trường hợp đó, bạn sẽ cần nhập cấu hình của máy ảnh thông qua phương thức quy định của nhà cung cấp máy ảnh, đây thường là mở địa chỉ IP của máy ảnh trong trình duyệt web của bạn.", + "onvifdeviceSavedFoundErrorText": "Một số cài đặt có thể đã hoàn nguyên về một giá trị trước đó. Có thể tùy chọn sửa đổi không có sẵn với máy ảnh này thông qua ONVIF.", + "onvifdeviceSavedText": "Cài đặt nội bộ của máy ảnh đã được lưu. Bạn có thể cần phải khởi động lại máy ảnh để có những thay đổi này có hiệu lực.", + "openImagesDownloadConfirm": "Bạn có chắc là bạn muốn bắt đầu tải xuống hình ảnh và hộp giới hạn (ma trận đặt trước) từ OpenImages?", + "openImagesDownloadConfirmStop": "Bạn có chắc là bạn muốn ngừng đào tạo?", + "opencl": "Opencl", + "opencvCascadesText": "Nếu bạn không thấy gì ở đây thì chỉ cần tải xuống gói Cascades . Thả chúng vào plugin/opencv/cascades sau đó nhấn làm mới .", + "orange": "trái cam", + "oven": "lò", + "p2pServerNotSelectedText": "Chọn một máy chủ từ danh sách và nhấn Lưu. Đợi 10 giây sau đó cố gắng mở bảng điều khiển từ xa.", + "p2pSettingsText1": "Bạn sẽ cần phải làm mới trang này để các thay đổi được áp dụng.", + "parking meter": "Đồng hồ đỗ xe", + "performanceOptimizeText1": "Máy ảnh của bạn đang cung cấp dữ liệu luồng H.264. Bạn có thể đặt loại luồng thành HLS, Poseidon và video codec để sao chép.", + "person": "người", + "pizza": "pizza", + "possibleInternalError": "Có thể xảy ra lỗi nội bộ", + "postDataBroken": "Kiểm tra định dạng của JSON. Đảm bảo nó được xâu chuỗi và được xác định trong 'dữ liệu'", + "potted plant": "cây chậu", + "powerVideoEventLimit": "Bạn đã đặt giới hạn sự kiện cao. Bạn có chắc là bạn muốn thực hiện yêu cầu này?", + "privateKey": "Khóa riêng", + "qsv": "QSV", + "rebootingCamera": "Khởi động lại camera", + "refrigerator": "tủ lạnh", + "remote": "Xa xôi", + "restartRequired": "Khởi động lại lõi Shinobi là cần thiết cho các thay đổi để có hiệu lực.", + "sandwich": "bánh mì sandwich", + "scissors": "cây kéo", + "separateByCommasOrRange": "Tách biệt bằng dấu phẩy hoặc một phạm vi", + "setMaxStorageAmountText": "Bạn nên đặt số tiền lưu trữ tối đa của mình trong cài đặt tài khoản ở bên trái. Tìm tùy chọn trong phần Hồ sơ. Mặc định là 10 GB.", + "sheep": "con cừu", + "sink": "bồn rửa chén", + "sizePurgeLockedText": "Khóa thanh lọc kích thước (DeleteOverMax) dường như không mở khóa. Mở khóa ngay bây giờ ...", + "skateboard": "ván trượt", + "skipPingText1": "Hãy thử đặt \"Bỏ qua ping\" thành Có.", + "skis": "ván trượt", + "snowboard": "trượt tuyết", + "sorryNo": "Xin lỗi, không", + "sorryNothingWasFound": "Xin lỗi, không có gì được tìm thấy.", + "spoon": "cái thìa", + "sports ball": "bóng thể thao", + "startUpText0": "Kiểm tra đĩa được sử dụng ..", + "startUpText1": "Hoàn thành kiểm tra đĩa được sử dụng.", + "startUpText2": "Tất cả người dùng đã kiểm tra, đợi để đóng các tệp mở và xóa các tệp qua giới hạn người dùng", + "startUpText3": "Chờ đợi để cung cấp video chưa hoàn thành kiểm tra một thời gian. 3 giây.", + "startUpText4": "Bắt đầu màn hình ... Vui lòng đợi ...", + "startUpText5": "Shinobi đã sẵn sàng.", + "startUpText6": "Video mồ côi được tìm thấy và chèn", + "stop sign": "biển báo dừng", + "subAccountManager": "Quản lý tài khoản phụ", + "substreamConnectionText": "Bạn có thể để lại chi tiết kết nối nếu bạn muốn nó sử dụng thông tin kết nối chính ở trên.", + "substreamOutputText": "Tại đây bạn có thể đặt cấu hình của luồng theo yêu cầu. Tìm hiểu về độ trễ của các loại luồng tại đây. ", + "substreamText": "Đây là một phương pháp theo yêu cầu để xem luồng trực tiếp. Bạn có thể làm cho nó để quá trình xem chỉ có sẵn khi ai đó đang xem hoặc được sử dụng để chuyển đổi giữa độ phân giải thấp và cao.", + "suitcase": "chiếc vali", + "superAdminText": "\"Super.json\" không tồn tại. Vui lòng đổi tên \"Super.sample.json\" thành \"Super.json\".", + "superAdminTitle": "Shinobi: Super Admin", + "surfboard": "ván lướt sóng", + "teddy bear": "gấu bông", + "tennis racket": "vợt tennis", + "tie": "cà vạt", + "toaster": "Máy nướng bánh mì", + "toilet": "phòng vệ sinh", + "tokenNotUserBound": "Tay cầm đăng nhập này không được liên kết với người dùng trên máy chủ này!", + "tokenNotUserBoundPt2": "Nhập thông tin đăng nhập của bạn sau đó sử dụng nút Đăng nhập Google để liên kết nhanh chóng.", + "toothbrush": "Bàn chải đánh răng", + "total": "toàn bộ", + "traffic light": "đèn giao thông", + "train": "tàu hỏa", + "truck": "xe tải", + "tv": "TV", + "umbrella": "ô", + "undoAllUnsaveChanges": "Bạn có chắc chắn muốn làm điều này? Điều này sẽ hoàn tác tất cả các thay đổi chưa được lưu.", + "unexpectedExitText": "Thông tin về lối ra này sẽ được tìm thấy trước nhật ký này. Ngoài ra, đây là lệnh FFMPEG đã được sử dụng khi quá trình bị sập.", + "updateCamerasInternalSettings": "Cập nhật cài đặt nội bộ của camera?", + "updateKeyText1": "\"UpdateKey\" bị thiếu trong \"Conf.json\", không thể cập nhật theo cách này cho đến khi bạn thêm nó.", + "updateKeyText2": "\"UpdateKey\" không chính xác.", + "updateNotice1": "Cập nhật Shinobi có nghĩa là ghi đè các tập tin. Nếu bạn đã tự sửa đổi bất kỳ tệp nào, bạn nên cập nhật Shinobi theo cách thủ công. Cấu hình và tệp video của bạn sẽ không được sửa đổi.", + "useSubStreamOnlyWhenWatching": "Chỉ khi xem, hãy sử dụng Substream", + "vaapi": "Vaapi (VA-API)", + "vase": "lọ cắm hoa", + "vda": "VDA (gia tốc phần cứng của Apple VDA)", + "vdpau": "vdpau", + "videoBuildingText1": "Video hiện đang xây dựng. Kiểm tra lại sau.", + "videotoolbox": "Videotoolbox", + "vp8_cuvid": "VP8 NVENC (NVIDIA HW Accel)", + "vp8_qsv": "VP8 (Video đồng bộ nhanh)", + "vp9_cuvid": "VP9 NVENC (NVIDIA HW Accel)", + "wannaReset": "Bạn có muốn thiết lập lại?", + "willTriggerAnEvent": "sẽ kích hoạt một sự kiện", + "wine glass": "ly rượu", + "years": "năm", + "zebra": "ngựa rằn" +} \ No newline at end of file diff --git a/libs/auth.js b/libs/auth.js index c70355f7..79df5326 100644 --- a/libs/auth.js +++ b/libs/auth.js @@ -78,24 +78,21 @@ module.exports = function(s,config,lang){ var isSessionKey = false if(apiKey){ var sessionKey = params.auth - createSession(apiKey,{ - auth: sessionKey, - permissions: s.parseJSON(apiKey.details), - details: {} - }) getUserByUid(apiKey,'mail,details',function(err,user){ if(user){ - try{ - editSession({ - auth: sessionKey - },{ - mail: user.mail, - details: s.parseJSON(user.details), - lang: s.getLanguageFile(user.details.lang) - }) - }catch(er){ - console.log('FAILED TO EDIT',er) - } + createSession(apiKey,{ + auth: sessionKey, + permissions: s.parseJSON(apiKey.details), + mail: user.mail, + details: s.parseJSON(user.details), + lang: s.getLanguageFile(user.details.lang) + }) + }else{ + createSession(apiKey,{ + auth: sessionKey, + permissions: s.parseJSON(apiKey.details), + details: {} + }) } callback(err,s.api[params.auth]) }) @@ -132,9 +129,7 @@ module.exports = function(s,config,lang){ var editSession = function(user,additionalData){ if(user){ if(!additionalData)additionalData = {} - Object.keys(additionalData).forEach(function(value,key){ - s.api[user.auth][key] = value - }) + Object.assign(s.api[user.auth], additionalData) } } var failHttpAuthentication = function(res,req,message){ @@ -190,39 +185,37 @@ module.exports = function(s,config,lang){ activeSession.lang = s.copySystemDefaultLanguage() } onSuccessComplete(activeSession) - }else{ - if(s.api[params.auth] && s.api[params.auth].details){ - var activeSession = s.api[params.auth] - onSuccess(activeSession) - if(activeSession.timeout){ - resetActiveSessionTimer(activeSession) - } - }else{ - if(params.username && params.username !== '' && params.password && params.password !== ''){ - loginWithUsernameAndPassword(params,'*',function(err,user){ - if(user){ - params.auth = user.auth - createSession(user) - resetActiveSessionTimer(s.api[params.auth]) - onSuccess(user) - }else{ - onFail() - } - }) - }else{ - loginWithApiKey(params,function(err,user,isSessionKey){ - if(isSessionKey)resetActiveSessionTimer(s.api[params.auth]) - if(user){ - createSession(user,{ - auth: params.auth - }) - onSuccess(s.api[params.auth]) - }else{ - onFail() - } - }) - } + }else if(s.api[params.auth] && s.api[params.auth].details){ + var activeSession = s.api[params.auth] + onSuccess(activeSession) + if(activeSession.timeout){ + resetActiveSessionTimer(activeSession) } + }else if(params.username && params.username !== '' && params.password && params.password !== ''){ + loginWithUsernameAndPassword(params,'*',function(err,user){ + if(user){ + params.auth = user.auth + createSession(user) + resetActiveSessionTimer(s.api[params.auth]) + onSuccess(user) + }else{ + onFail() + } + }) + }else if(params.auth && params.ke){ + loginWithApiKey(params,function(err,user,isSessionKey){ + if(isSessionKey)resetActiveSessionTimer(s.api[params.auth]) + if(user){ + createSession(user,{ + auth: params.auth + }) + onSuccess(s.api[params.auth]) + }else{ + onFail() + } + }) + } else { + onFail() } } //super user authentication handler diff --git a/libs/basic.js b/libs/basic.js index bcfe4b15..1c427714 100644 --- a/libs/basic.js +++ b/libs/basic.js @@ -57,12 +57,15 @@ module.exports = function(s,config){ splitted[1] = user + ':' + pass + '@' + splitted[1] return splitted.join('://') } - s.checkCorrectPathEnding = function(x){ - var length=x.length - if(x.charAt(length-1)!=='/'){ - x=x+'/' + s.checkCorrectPathEnding = function(x,reverse){ + var newString = `${x}` + var length = x.length + if(reverse && x.charAt(length-1) === '/'){ + newString = x.slice(0, -1) + }else if(x.charAt(length-1) !== '/'){ + newString = x + '/' } - return x.replace('__DIR__',s.mainDirectory) + return newString.replace('__DIR__',s.mainDirectory) } s.mergeDeep = function(...objects) { const isObject = obj => obj && typeof obj === 'object'; diff --git a/libs/basic/utils.js b/libs/basic/utils.js index 3a99c0da..73bff949 100644 --- a/libs/basic/utils.js +++ b/libs/basic/utils.js @@ -1,7 +1,9 @@ const fs = require('fs'); const path = require('path'); const moment = require('moment'); -const request = require('request'); +const fetch = require('node-fetch'); +const { AbortController } = require('node-abort-controller') +const DigestFetch = require('digest-fetch') module.exports = (processCwd,config) => { const parseJSON = (string) => { var parsed @@ -28,6 +30,11 @@ module.exports = (processCwd,config) => { if(toLowerCase)newString = newString.toLowerCase() return newString.indexOf(find) > -1 } + function getFileDirectory(filePath){ + const fileParts = filePath.split('/') + fileParts.pop(); + return fileParts.join('/') + '/'; + } const checkCorrectPathEnding = (x) => { var length=x.length if(x.charAt(length-1)!=='/'){ @@ -79,6 +86,50 @@ module.exports = (processCwd,config) => { if(!e){e=new Date};if(!x){x='YYYY-MM-DDTHH-mm-ss'}; return moment(e).format(x); } + const fetchTimeout = (url, ms, { signal, ...options } = {}) => { + const controller = new AbortController(); + const promise = fetch(url, { signal: controller.signal, ...options }); + if (signal) signal.addEventListener("abort", () => controller.abort()); + const timeout = setTimeout(() => controller.abort(), ms); + return promise.finally(() => clearTimeout(timeout)); + } + async function fetchDownloadAndWrite(downloadUrl,outputPath,readFileAfterWrite,options){ + const writeStream = fs.createWriteStream(outputPath); + const downloadBuffer = await fetch(downloadUrl,options).then((res) => res.buffer()); + writeStream.write(downloadBuffer); + writeStream.end(); + if(readFileAfterWrite === 1){ + return fs.createReadStream(outputPath) + }else if(readFileAfterWrite === 2){ + return downloadBuffer + } + return null + } + function fetchWithAuthentication(requestUrl,options,callback){ + let hasDigestAuthEnabled = options.digestAuth; + let theRequester; + const hasUsernameAndPassword = options.username && typeof options.password === 'string' + const requestOptions = { + method : options.method || 'GET' + } + if(typeof options.postData === 'object'){ + const formData = new fetch.FormData() + const formKeys = Object.keys(options.postData) + formKeys.forEach(function(key){ + const value = formKeys[key] + formData.set(key, value) + }) + requestOptions.body = formData + } + if(hasUsernameAndPassword && hasDigestAuthEnabled){ + theRequester = (new DigestFetch(options.username, options.password)).fetch + }else if(hasUsernameAndPassword){ + theRequester = (new DigestFetch(options.username, options.password, { basic: true })).fetch + }else{ + theRequester = fetch + } + return theRequester(requestUrl,requestOptions) + } const checkSubscription = (subscriptionId,callback) => { function subscriptionFailed(){ console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') @@ -92,12 +143,12 @@ module.exports = (processCwd,config) => { if(subscriptionId && subscriptionId !== 'sub_XXXXXXXXXXXX' && !config.disableOnlineSubscriptionCheck){ var url = 'https://licenses.shinobi.video/subscribe/check?subscriptionId=' + subscriptionId var hasSubcribed = false - request(url,{ + fetchTimeout(url,30000,{ method: 'GET', - timeout: 30000 - }, function(err,resp,body){ + }) + .then(response => response.text()) + .then(function(body){ var json = s.parseJSON(body) - if(err)console.log(err,json) hasSubcribed = json && !!json.ok var i; for (i = 0; i < s.onSubscriptionCheckExtensions.length; i++) { @@ -113,8 +164,12 @@ module.exports = (processCwd,config) => { }else{ subscriptionFailed() } + }).catch((err) => { + if(err)console.log(err) + subscriptionFailed() }) }else{ + var i; for (i = 0; i < s.onSubscriptionCheckExtensions.length; i++) { const extender = s.onSubscriptionCheckExtensions[i] hasSubcribed = extender(false,{},subscriptionId) @@ -125,10 +180,53 @@ module.exports = (processCwd,config) => { callback(hasSubcribed) } } + function isEven(value) { + if (value%2 == 0) + return true; + else + return false; + } + function asyncSetTimeout(timeoutAmount) { + return new Promise((resolve,reject) => { + setTimeout(function(){ + resolve() + },timeoutAmount) + }) + } + function copyFile(inputFilePath,outputFilePath) { + const response = {ok: true} + return new Promise((resolve,reject) => { + function failed(err){ + response.ok = false + response.err = err + resolve(response) + } + const readStream = fs.createReadStream(inputFilePath) + const writeStream = fs.createWriteStream(outputFilePath) + writeStream.on('finish', () => { + resolve(response) + }) + writeStream.on('error', failed) + readStream.on('error', failed) + readStream.pipe(writeStream) + }) + } + function hmsToSeconds(str) { + var p = str.split(':'), + s = 0, m = 1; + + while (p.length > 0) { + s += m * parseFloat(p.pop(), 10); + m *= 60; + } + + return s; + } return { parseJSON: parseJSON, stringJSON: stringJSON, stringContains: stringContains, + getFileDirectory: getFileDirectory, checkCorrectPathEnding: checkCorrectPathEnding, nameToTime: nameToTime, mergeDeep: mergeDeep, @@ -137,5 +235,12 @@ module.exports = (processCwd,config) => { localToUtc: localToUtc, formattedTime: formattedTime, checkSubscription: checkSubscription, + isEven: isEven, + fetchTimeout: fetchTimeout, + fetchDownloadAndWrite: fetchDownloadAndWrite, + fetchWithAuthentication: fetchWithAuthentication, + asyncSetTimeout: asyncSetTimeout, + copyFile: copyFile, + hmsToSeconds, } } diff --git a/libs/basic/websocketTools.js b/libs/basic/websocketTools.js new file mode 100644 index 00000000..2c8c86dc --- /dev/null +++ b/libs/basic/websocketTools.js @@ -0,0 +1,32 @@ +const WebSocket = require('cws'); +function createWebSocketServer(options){ + const theWebSocket = new WebSocket.Server(options ? options : { + noServer: true + }); + theWebSocket.broadcast = function(data) { + theWebSocket.clients.forEach((client) => { + try{ + client.sendData(data) + }catch(err){ + // console.log(err) + } + }) + }; + return theWebSocket +} +function createWebSocketClient(connectionHost,options){ + const clientConnection = new WebSocket(connectionHost, options.engineOptions); + if(options.onMessage){ + const onMessage = options.onMessage; + clientConnection.on('message', message => { + const data = JSON.parse(message); + onMessage(data); + }); + } + return clientConnection +} + +module.exports = { + createWebSocketServer, + createWebSocketClient, +} diff --git a/libs/branding.js b/libs/branding.js index 982153c6..db7da985 100644 --- a/libs/branding.js +++ b/libs/branding.js @@ -1,15 +1,46 @@ module.exports = function(s,config,lang,app,io){ if(config.showPoweredByShinobi === undefined){config.showPoweredByShinobi=true} if(config.poweredByShinobi === undefined){config.poweredByShinobi='Powered by Shinobi.Systems'} - if(config.poweredByShinobiClass === undefined){config.poweredByShinobiClass='margin:15px 0 0 0;text-align:center;color:#777;font-family: sans-serif;text-transform: uppercase;letter-spacing: 3;font-size: 8pt;'} - if(config.webPageTitle === undefined){config.webPageTitle='Shinobi'} if(config.showLoginCardHeader === undefined){config.showLoginCardHeader=true} - if(config.webFavicon === undefined){config.webFavicon='libs/img/icon/favicon.ico'} - if(config.logoLocation76x76 === undefined){config.logoLocation76x76='libs/img/icon/apple-touch-icon-76x76.png'} + if(config.webFavicon === undefined){config.webFavicon = 'libs/img/icon/favicon.ico'} + if(!config.logoLocationAppleTouchIcon)config.logoLocationAppleTouchIcon = 'libs/img/icon/apple-touch-icon.png'; + if(!config.logoLocation57x57)config.logoLocation57x57 = 'libs/img/icon/apple-touch-icon-57x57.png'; + if(!config.logoLocation72x72)config.logoLocation72x72 = 'libs/img/icon/apple-touch-icon-72x72.png'; + if(!config.logoLocation76x76)config.logoLocation76x76 = 'libs/img/icon/apple-touch-icon-76x76.png'; + if(!config.logoLocation114x114)config.logoLocation114x114 = 'libs/img/icon/apple-touch-icon-114x114.png'; + if(!config.logoLocation120x120)config.logoLocation120x120 = 'libs/img/icon/apple-touch-icon-120x120.png'; + if(!config.logoLocation144x144)config.logoLocation144x144 = 'libs/img/icon/apple-touch-icon-144x144.png'; + if(!config.logoLocation152x152)config.logoLocation152x152 = 'libs/img/icon/apple-touch-icon-152x152.png'; + if(!config.logoLocation196x196)config.logoLocation196x196 = 'libs/img/icon/favicon-196x196.png'; if(config.logoLocation76x76Link === undefined){config.logoLocation76x76Link='https://shinobi.video'} if(config.logoLocation76x76Style === undefined){config.logoLocation76x76Style='border-radius:50%'} + if(config.loginScreenBackground === undefined){config.loginScreenBackground='assets/img/splash.avif'} if(config.showLoginSelector === undefined){config.showLoginSelector=true} - + if(config.defaultTheme === undefined)config.defaultTheme = 'Ice-v3'; + if(config.socialLinks === undefined){ + config.socialLinks = [ + { + icon: 'home', + href: 'https://shinobi.video', + title: 'Homepage' + }, + { + icon: 'facebook', + href: 'https://www.facebook.com/ShinobiCCTV', + title: 'Facebook' + }, + { + icon: 'twitter', + href: 'https://twitter.com/ShinobiCCTV', + title: 'Twitter' + }, + { + icon: 'youtube', + href: 'https://www.youtube.com/channel/UCbgbBLTK-koTyjOmOxA9msQ', + title: 'YouTube' + } + ] + } s.getConfigWithBranding = function(domain){ var configCopy = Object.assign({},config) if(config.brandingConfig && config.brandingConfig[domain]){ diff --git a/libs/cameraThread/detector.js b/libs/cameraThread/detector.js index 02f9102d..537044e9 100644 --- a/libs/cameraThread/detector.js +++ b/libs/cameraThread/detector.js @@ -1,30 +1,36 @@ module.exports = function(jsonData,pamDiffResponder){ const { - // see libs/detectorUtils.js for more parameters and functions - // - config, + completeMonitorConfig, groupKey, monitorId, + monitorName, monitorDetails, - completeMonitorConfig, + } = require('./libs/monitorUtils.js')(jsonData) + const { + convertRegionsToTiles, + } = require('./libs/tileCutter.js') + const loadDetectorUtils = require('./libs/detectorUtils.js') + let detectorUtils + let onMotionData = null + if(monitorDetails.detector_motion_tile_mode === '1'){ + const { + originalCords, + newRegionsBySquares, + } = convertRegionsToTiles(monitorDetails) + jsonData.rawMonitorConfig.details.cords = newRegionsBySquares; + detectorUtils = loadDetectorUtils(jsonData,pamDiffResponder) + detectorUtils.originalCords = originalCords; + onMotionData = detectorUtils.getTileMotionEvent() + }else{ + detectorUtils = loadDetectorUtils(jsonData,pamDiffResponder) + } + const { pamDetectorIsEnabled, - // attachPamPipeDrivers, - // - getAcceptedTriggers, - getRegionsWithMinimumChange, - getRegionsBelowMaximumChange, - getRegionsWithThresholdMet, - filterTheNoise, - filterTheNoiseFromMultipleRegions, - // - buildDetectorObject, - buildTriggerEvent, - sendDetectedData, - } = require('./libs/detectorUtils.js')(jsonData,pamDiffResponder) + } = detectorUtils; return function(cameraProcess){ if(pamDetectorIsEnabled){ - attachPamPipeDrivers(cameraProcess) + attachPamPipeDrivers(cameraProcess,onMotionData) } } } diff --git a/libs/cameraThread/libs/dataPortConnection.js b/libs/cameraThread/libs/dataPortConnection.js new file mode 100644 index 00000000..c5a0a4c4 --- /dev/null +++ b/libs/cameraThread/libs/dataPortConnection.js @@ -0,0 +1,10 @@ +module.exports = function(jsonData,onConnected,onError,onClose){ + const config = jsonData.globalInfo.config; + const dataPortToken = jsonData.dataPortToken; + const CWS = require('cws'); + const client = new CWS(`ws://localhost:${config.port}/dataPort`); + if(onError)client.on('error',onError); + if(onClose)client.on('close',onClose); + client.on('open',onConnected); + return client; +} diff --git a/libs/cameraThread/libs/detectorUtils.js b/libs/cameraThread/libs/detectorUtils.js index ed7f2764..abaf519c 100644 --- a/libs/cameraThread/libs/detectorUtils.js +++ b/libs/cameraThread/libs/detectorUtils.js @@ -1,5 +1,8 @@ const P2P = require('pipe2pam') let PamDiff = require('pam-diff') +const { + makeBigMatricesFromSmallOnes, +} = require('./tileCutter.js') module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){ if(alternatePamDiff)PamDiff = alternatePamDiff; const noiseFilterArray = {}; @@ -7,6 +10,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){ const completeMonitorConfig = jsonData.rawMonitorConfig const groupKey = completeMonitorConfig.ke const monitorId = completeMonitorConfig.mid + const monitorName = completeMonitorConfig.name const monitorDetails = completeMonitorConfig.details const triggerTimer = {} let regionJson @@ -71,12 +75,12 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){ pamDiffResponder(detectorObject) } }else{ - var sendDetectedData = function(detectorObject){ - pamDiffResponder.write(Buffer.from(JSON.stringify(detectorObject))) - } + var sendDetectedData = function(detectorObject){ + pamDiffResponder.write(Buffer.from(JSON.stringify(detectorObject))) + } } function logData(...args){ - process.logData(JSON.stringify(args)) + process.logData(args) } function getRegionsWithMinimumChange(data){ try{ @@ -240,6 +244,28 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){ pamDiff.on('diff',pamAnalyzer) cameraProcess.stdio[3].pipe(p2p).pipe(pamDiff) } + function getTileMotionEvent(){ + let pamAnalyzer = function(){} + if(monitorDetails.detector_noise_filter === '1'){ + pamAnalyzer = async (data) => { + const acceptedTriggers = getAcceptedTriggers(data.trigger) + const passedFilter = await filterTheNoiseFromMultipleRegions(acceptedTriggers) + if(passedFilter){ + const mergedTriggers = mergePamTriggers(acceptedTriggers) + mergedTriggers.matrices = makeBigMatricesFromSmallOnes(mergedTriggers.matrices) + buildTriggerEvent(mergedTriggers) + } + } + }else{ + pamAnalyzer = (data) => { + const acceptedTriggers = getAcceptedTriggers(data.trigger) + const mergedTriggers = mergePamTriggers(acceptedTriggers) + mergedTriggers.matrices = makeBigMatricesFromSmallOnes(mergedTriggers.matrices) + buildTriggerEvent(mergedTriggers) + } + } + return pamAnalyzer + } function createPamDiffRegionArray(regions,globalColorThreshold,globalSensitivity,fullFrame){ var pamDiffCompliantArray = [], arrayForOtherStuff = [], @@ -256,6 +282,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){ if(!region)return false; region.polygon = []; region.points.forEach(function(points){ + if(!points || isNaN(points[0]) || isNaN(points[1]))return; var x = parseFloat(points[0]); var y = parseFloat(points[1]); if(x < 0)x = 0; @@ -265,6 +292,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){ y: y }) }) + if(region.polygon.length < 4)return logData(`Failed to Create Region : ${monitorName} : ${region.name}`,region.points); if(region.sensitivity===''){ region.sensitivity = globalSensitivity }else{ @@ -369,6 +397,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){ getPropertiesFromBlob, createMatricesFromBlobs, logData, + getTileMotionEvent, // parameters pamDetectorIsEnabled, noiseFilterArray, diff --git a/libs/cameraThread/libs/monitorUtils.js b/libs/cameraThread/libs/monitorUtils.js new file mode 100644 index 00000000..7417b7d2 --- /dev/null +++ b/libs/cameraThread/libs/monitorUtils.js @@ -0,0 +1,14 @@ +module.exports = (jsonData) => { + const completeMonitorConfig = jsonData.rawMonitorConfig + const groupKey = completeMonitorConfig.ke + const monitorId = completeMonitorConfig.mid + const monitorName = completeMonitorConfig.name + const monitorDetails = completeMonitorConfig.details + return { + completeMonitorConfig, + groupKey, + monitorId, + monitorName, + monitorDetails, + } +} diff --git a/libs/cameraThread/libs/tileCutter.js b/libs/cameraThread/libs/tileCutter.js new file mode 100644 index 00000000..3c8fe8fa --- /dev/null +++ b/libs/cameraThread/libs/tileCutter.js @@ -0,0 +1,185 @@ +const SAT = require('sat') +const V = SAT.Vector; +const P = SAT.Polygon; +const B = SAT.Box; + +function intersectionY(edge, y) { + const [[x1, y1], [x2, y2]] = edge; + const dir = Math.sign(y2 - y1); + if (dir && (y1 - y)*(y2 - y) <= 0) return { x: x1 + (y-y1)/(y2-y1) * (x2-x1), dir }; +} + + +function tilePolygon(points, tileSize){ + // https://stackoverflow.com/questions/56827208/spilliting-polygon-into-square + const minY = Math.min(...points.map(p => p[1])); + const maxY = Math.max(...points.map(p => p[1])); + const minX = Math.min(...points.map(p => p[0])); + const gridPoints = []; + for (let y = minY; y <= maxY; y += tileSize) { + // Collect x-coordinates where polygon crosses this horizontal line (y) + const cuts = []; + let prev = null; + for (let i = 0; i < points.length; i++) { + const cut = intersectionY([points[i], points[(i+1)%points.length]], y); + if (!cut) continue; + if (!prev || prev.dir !== cut.dir) cuts.push(cut); + prev = cut; + } + if (prev && prev.dir === cuts[0].dir) cuts.pop(); + // Now go through those cuts from left to right toggling whether we are in/out the polygon + let dirSum = 0; + let startX = null; + for (let cut of cuts.sort((a, b) => a.x - b.x)) { + dirSum += cut.dir; + if (dirSum % 2) { // Entering polygon + if (startX === null) startX = cut.x; + } else if (startX !== null) { // Exiting polygon + // Genereate grid points on this horizontal line segement + for (let x = minX + Math.ceil((startX - minX) / tileSize)*tileSize; x <= cut.x; x += tileSize) { + gridPoints.push([x, y]); + } + startX = null; + } + } + } + return gridPoints; +} + +function convertStringPoints(oldPoints){ + // [["0","0"],["0","150"],["300","150"],["300","0"]] + var newPoints = [] + oldPoints.forEach((point) => { + newPoints.push([parseInt(point[0]),parseInt(point[1])]) + }) + return newPoints +} + +function createSquares(gridPoints,imgWidth,imgHeight){ + var rows = []; + var n = 0; + var curentLine = gridPoints[0][1] + gridPoints.forEach((point) => { + if(!rows[n])rows[n] = [] + rows[n].push(point) + if(curentLine !== point[1]){ + curentLine = point[1]; + ++n; + } + }); + var squares = []; + rows.forEach((row,n) => { + for (let i = 0; i < row.length; i += 2) { + if(!rows[n + 1] || !row[i + 1])return; + var square = [row[i],row[i + 1],rows[n + 1][i],rows[n + 1][i + 1]] + squares.push(square) + } + }) + return squares +} +const getAllSquaresTouchingRegion = function(region,squares){ + var matrixPoints = [] + var collisions = [] + var polyPoints = [] + region.points.forEach(function(point){ + polyPoints.push(new V(parseInt(point[0]),parseInt(point[1]))) + }) + var regionPoly = new P(new V(0,0), polyPoints) + squares.forEach(function(squarePoints){ + var firstPoint = squarePoints[0] + var thirdPoint = squarePoints[2] + var squareX = firstPoint[0] + var squareY = firstPoint[1] + var squareWidth = thirdPoint[0] - firstPoint[0] + var squareHeight = thirdPoint[1] - firstPoint[1] + var squarePoly = new B(new V(squareX, squareY), squareWidth, squareHeight).toPolygon() + var response = new SAT.Response() + var collided = SAT.testPolygonPolygon(squarePoly, regionPoly, response) + if(collided === true){ + collisions.push(squarePoints) + } + }) + return collisions +} +function makeBigMatricesFromSmallOnes(matrices){ + var bigMatrices = {} + matrices.forEach(function(matrix,n){ + const regionName = matrix.tag + if(!bigMatrices[regionName]){ + bigMatrices[regionName] = { + tag: regionName, + x: 9999999999, + y: 9999999999, + width: matrices.length > 1 ? matrices[0].width : 0, + height: matrices.length > 1 ? matrices[0].height : 0, + tilesCounted: 0, + confidence: 0, + } + } + var bigMatrix = bigMatrices[regionName]; + bigMatrix.x = bigMatrix.x > matrix.x ? matrix.x : bigMatrix.x; + bigMatrix.y = bigMatrix.y > matrix.y ? matrix.y : bigMatrix.y; + const newWidth = matrix.x - bigMatrix.x + const newHeight = matrix.y - bigMatrix.y + bigMatrix.width = bigMatrix.width < matrix.x ? newWidth === 0 ? matrix.width : newWidth : bigMatrix.width; + bigMatrix.height = bigMatrix.height < matrix.y ? newHeight === 0 ? matrix.height : newHeight : bigMatrix.height; + // bigMatrix.tag = matrix.tag; + bigMatrix.confidence += matrix.confidence; + bigMatrix.tilesCounted += 1; + }) + let allBigMatrices = Object.values(bigMatrices) + allBigMatrices.forEach(function(matrix,n){ + let bigMatrix = allBigMatrices[n] + bigMatrix.averageConfidence = bigMatrix.confidence / bigMatrix.tilesCounted; + }) + return allBigMatrices +} +function convertRegionsToTiles(monitorDetails){ + let originalCords; + //force full frame detection to be use for tracking blobs + monitorDetails.detector_frame = '1' + monitorDetails.detector_sensitivity = '1' + monitorDetails.detector_color_threshold = monitorDetails.detector_color_threshold || '7' + try{ + monitorDetails.cords = JSON.parse(monitorDetails.cords) + }catch(err){ + + } + originalCords = Object.values(monitorDetails.cords) + const regionKeys = Object.keys(monitorDetails.cords); + const newRegionsBySquares = {} + try{ + regionKeys.forEach(function(regionKey){ + const region = monitorDetails.cords[regionKey] + const tileSize = parseInt(region.detector_tile_size) || 20; + const gridPoints = tilePolygon([ + [0,0], + [0,height], + [width,height], + [width,0] + ],tileSize) + const squares = createSquares(gridPoints,width,height) + const squaresInRegion = getAllSquaresTouchingRegion(region,squares) + squaresInRegion.forEach((square,n) => { + newRegionsBySquares[`${regionKey}_${n}`] = Object.assign({},region,{ + "points": square + }) + }) + }) + // jsonData.rawMonitorConfig.details.cords = newRegionsBySquares; + }catch(err){ + process.logData(err) + } + // detectorUtils.originalCords = originalCords; + return { + originalCords, + newRegionsBySquares, + } +} +module.exports = { + tilePolygon, + createSquares, + convertRegionsToTiles, + getAllSquaresTouchingRegion, + makeBigMatricesFromSmallOnes, +} diff --git a/libs/cameraThread/singleCamera.js b/libs/cameraThread/singleCamera.js index 1976942d..28217840 100644 --- a/libs/cameraThread/singleCamera.js +++ b/libs/cameraThread/singleCamera.js @@ -1,5 +1,4 @@ const fs = require('fs') -const request = require('request') const exec = require('child_process').exec const spawn = require('child_process').spawn const isWindows = (process.platform === 'win32' || process.platform === 'win64') @@ -14,9 +13,34 @@ const stdioPipes = jsonData.pipes || [] var newPipes = [] var stdioWriters = []; -var writeToStderr = function(text){ +const { fetchTimeout } = require('../basic/utils.js')(process.cwd(),config) +const dataPort = require('./libs/dataPortConnection.js')(jsonData, +// onConnected +() => { + dataPort.send(jsonData.dataPortToken) +}, +// onError +(err) => { + writeToStderr([ + 'dataPort:Connection:Error', + err + ]) +}, +// onClose +(e) => { + writeToStderr([ + 'dataPort:Connection:Closed', + e + ]) +}) + +var writeToStderr = function(argsAsArray){ try{ - process.stderr.write(Buffer.from(`${text}`, 'utf8' )) + process.stderr.write(Buffer.from(`${JSON.stringify(argsAsArray)}`, 'utf8' )) + // dataPort.send({ + // f: 'debugLog', + // data: argsAsArray, + // }) // stdioWriters[2].write(Buffer.from(`${new Error('writeToStderr').stack}`, 'utf8' )) }catch(err){ } @@ -111,7 +135,9 @@ writeToStderr('Thread Opening') if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detector_pam === '1'){ try{ - const attachPamDetector = require(config.monitorDetectorDaemonPath ? config.monitorDetectorDaemonPath : __dirname + '/detector.js')(jsonData,stdioWriters[3]) + const attachPamDetector = require(config.monitorDetectorDaemonPath ? config.monitorDetectorDaemonPath : __dirname + '/detector.js')(jsonData,(detectorObject) => { + dataPort.send(JSON.stringify(detectorObject)) + }) attachPamDetector(cameraProcess) }catch(err){ writeToStderr(err.stack) @@ -119,7 +145,6 @@ if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detecto } if(rawMonitorConfig.type === 'jpeg'){ - var recordingSnapRequest var recordingSnapper var errorTimeout var errorCount = 0 @@ -137,16 +162,11 @@ if(rawMonitorConfig.type === 'jpeg'){ setTimeout(() => { if(!cameraProcess.stdio[0])return writeToStderr('No Camera Process Found for Snapper'); const captureOne = function(f){ - recordingSnapRequest = request({ - url: buildMonitorUrl(rawMonitorConfig), + fetchTimeout(buildMonitorUrl(rawMonitorConfig),15000,{ method: 'GET', - encoding: null, - timeout: 15000 - },function(err,data){ - if(err){ - writeToStderr(JSON.stringify(err)) - return; - } + }) + .then(response => response.text()) + .then(function(body){ // writeToStderr(data.body.length) cameraProcess.stdio[0].write(data.body) recordingSnapper = setTimeout(function(){ @@ -159,7 +179,7 @@ if(rawMonitorConfig.type === 'jpeg'){ delete(errorTimeout) },3000) } - }).on('error', function(err){ + }).catch(function(err){ ++errorCount clearTimeout(errorTimeout) errorTimeout = null diff --git a/libs/childNode.js b/libs/childNode.js index eaf3ba4a..6d1d9879 100644 --- a/libs/childNode.js +++ b/libs/childNode.js @@ -1,215 +1,147 @@ -var fs = require('fs'); -var http = require('http'); -var https = require('https'); -var express = require('express'); +const fs = require('fs'); +const url = require('url'); +const http = require('http'); +const https = require('https'); +const express = require('express'); +const { createWebSocketServer, createWebSocketClient } = require('./basic/websocketTools.js') module.exports = function(s,config,lang,app,io){ - const { cameraDestroy } = require('./monitor/utils.js')(s,config,lang) //setup Master for childNodes - if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){ + if( + config.childNodes.enabled === true && + config.childNodes.mode === 'master' + ){ + const { + getIpAddress, + initiateDataConnection, + initiateVideoTransferConnection, + onWebSocketDataFromChildNode, + onDataConnectionDisconnect, + initiateVideoWriteFromChildNode, + initiateTimelapseFrameWriteFromChildNode, + } = require('./childNode/utils.js')(s,config,lang,app,io) s.childNodes = {}; - var childNodeHTTP = express(); - var childNodeServer = http.createServer(app); - var childNodeWebsocket = new (require('socket.io'))() - childNodeServer.listen(config.childNodes.port,config.bindip,function(){ - console.log(lang.Shinobi+' - CHILD NODE PORT : '+config.childNodes.port); - }); - s.debugLog('childNodeWebsocket.attach(childNodeServer)') - childNodeWebsocket.attach(childNodeServer,{ - path:'/socket.io', - transports: ['websocket'] - }); - //send data to child node function (experimental) - s.cx = function(z,y,x){ - if(!z.mid && !z.d){ - console.error('Missing ID') - }else if(x){ - x.broadcast.to(y).emit('c',z) - }else{ - childNodeWebsocket.to(y).emit('c',z) + const childNodesConnectionIndex = {}; + const childNodeHTTP = express(); + const childNodeServer = http.createServer(app); + const childNodeWebsocket = createWebSocketServer(); + const childNodeFileRelay = createWebSocketServer(); + childNodeServer.on('upgrade', function upgrade(request, socket, head) { + const pathname = url.parse(request.url).pathname; + if (pathname === '/childNode') { + childNodeWebsocket.handleUpgrade(request, socket, head, function done(ws) { + childNodeWebsocket.emit('connection', ws, request) + }) + } else if (pathname === '/childNodeFileRelay') { + childNodeFileRelay.handleUpgrade(request, socket, head, function done(ws) { + childNodeFileRelay.emit('connection', ws, request) + }) + } else { + socket.destroy(); } + }); + const childNodeBindIP = config.childNodes.ip || config.bindip; + childNodeServer.listen(config.childNodes.port,childNodeBindIP,function(){ + console.log(lang.Shinobi+' - CHILD NODE SERVER : ' + config.childNodes.port); + }); + //send data to child node function + s.cx = function(data,connectionId){ + childNodesConnectionIndex[connectionId].sendJson(data) } //child Node Websocket - childNodeWebsocket.on('connection', function (cn) { + childNodeWebsocket.on('connection', function (client, req) { //functions for dispersing work to child servers; - var ipAddress - cn.on('c',function(d){ - if(config.childNodes.key.indexOf(d.socketKey) > -1){ - if(!cn.shinobi_child&&d.f=='init'){ - ipAddress = cn.request.connection.remoteAddress.replace('::ffff:','')+':'+d.port - cn.ip = ipAddress - cn.shinobi_child = 1 - cn.tx = function(z){ - cn.emit('c',z) - } - if(!s.childNodes[cn.ip]){ - s.childNodes[cn.ip] = {} - }; - s.childNodes[cn.ip].dead = false - s.childNodes[cn.ip].cnid = cn.id - s.childNodes[cn.ip].cpu = 0 - s.childNodes[cn.ip].ip = ipAddress - s.childNodes[cn.ip].activeCameras = {} - d.availableHWAccels.forEach(function(accel){ - if(config.availableHWAccels.indexOf(accel) === -1)config.availableHWAccels.push(accel) - }) - cn.tx({ - f : 'init_success', - childNodes : s.childNodes - }) - s.childNodes[cn.ip].coreCount = d.coreCount - }else{ - switch(d.f){ - case'cpu': - s.childNodes[ipAddress].cpu = d.cpu; - break; - case'sql': - s.sqlQuery(d.query,d.values,function(err,rows){ - cn.emit('c',{f:'sqlCallback',rows:rows,err:err,callbackId:d.callbackId}); - }); - break; - case'knex': - s.knexQuery(d.options,function(err,rows){ - cn.emit('c',{f:'sqlCallback',rows:rows,err:err,callbackId:d.callbackId}); - }); - break; - case'clearCameraFromActiveList': - if(s.childNodes[ipAddress])delete(s.childNodes[ipAddress].activeCameras[d.ke + d.id]) - break; - case'camera': - s.camera(d.mode,d.data) - break; - case's.tx': - s.tx(d.data,d.to) - break; - case's.userLog': - if(!d.mon || !d.data)return console.log('LOG DROPPED',d.mon,d.data); - s.userLog(d.mon,d.data) - break; - case'open_timelapse_file_transfer': - var location = s.getTimelapseFrameDirectory(d.d) + `${d.currentDate}/` - if(!fs.existsSync(location)){ - fs.mkdirSync(location) - } - break; - case'created_timelapse_file_chunk': - if(!s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename]){ - var dir = s.getTimelapseFrameDirectory(d.d) + `${d.currentDate}/` - s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename] = fs.createWriteStream(dir+d.filename) - } - s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename].write(d.chunk) - break; - case'created_timelapse_file': - if(!s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename]){ - return console.log('FILE NOT EXIST') - } - s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename].end() - cn.tx({ - f: 'deleteTimelapseFrame', - file: d.filename, - currentDate: d.currentDate, - d: d.d, //monitor config - ke: d.ke, - mid: d.mid - }) - s.insertTimelapseFrameDatabaseRow({ - ke: d.ke - },d.queryInfo) - break; - case'created_file_chunk': - if(!s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename]){ - d.dir = s.getVideoDirectory(s.group[d.ke].rawMonitorConfigurations[d.mid]) - if (!fs.existsSync(d.dir)) { - fs.mkdirSync(d.dir, {recursive: true}, (err) => {s.debugLog(err)}) - } - s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename] = fs.createWriteStream(d.dir+d.filename) - } - s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename].write(d.chunk) - break; - case'created_file': - if(!s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename]){ - return console.log('FILE NOT EXIST') - } - s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename].end(); - cn.tx({ - f:'delete', - file:d.filename, - ke:d.ke, - mid:d.mid - }) - s.txWithSubPermissions({ - f:'video_build_success', - hrefNoAuth:'/videos/'+d.ke+'/'+d.mid+'/'+d.filename, - filename:d.filename, - mid:d.mid, - ke:d.ke, - time:d.time, - size:d.filesize, - end:d.end - },'GRP_'+d.ke,'video_view') - //save database row - var insert = { - startTime : d.time, - filesize : d.filesize, - endTime : d.end, - dir : s.getVideoDirectory(d.d), - file : d.filename, - filename : d.filename, - filesizeMB : parseFloat((d.filesize/1048576).toFixed(2)) - } - s.insertDatabaseRow(d.d,insert) - s.insertCompletedVideoExtensions.forEach(function(extender){ - extender(d.d,insert) - }) - //purge over max - s.purgeDiskForGroup(d.ke) - //send new diskUsage values - s.setDiskUsedForGroup(d.ke,insert.filesizeMB) - clearTimeout(s.group[d.ke].activeMonitors[d.mid].recordingChecker) - clearTimeout(s.group[d.ke].activeMonitors[d.mid].streamChecker) - break; - } - } + const ipAddress = getIpAddress(req) + const connectionId = s.gid(10); + s.debugLog('Child Node Connection!',new Date(),ipAddress) + client.id = connectionId; + function onAuthenticate(d){ + const data = JSON.parse(d); + const childNodeKeyAccepted = config.childNodes.key.indexOf(data.socketKey) > -1; + if(!client.shinobiChildAlreadyRegistered && data.f === 'init' && childNodeKeyAccepted){ + initiateDataConnection(client,req,data,connectionId); + childNodesConnectionIndex[connectionId] = client; + client.removeListener('message',onAuthenticate) + client.on('message',(d) => { + const data = JSON.parse(d); + onWebSocketDataFromChildNode(client,data) + }) + }else{ + s.debugLog('Child Node Force Disconnected!',new Date(),ipAddress) + client.destroy() } + } + client.on('message',onAuthenticate) + client.on('close',() => { + onDataConnectionDisconnect(client, req) }) - cn.on('disconnect',function(){ - console.log('childNodeWebsocket.disconnect',ipAddress) - if(s.childNodes[ipAddress]){ - var monitors = Object.values(s.childNodes[ipAddress].activeCameras) - if(monitors && monitors[0]){ - var loadCompleted = 0 - var loadMonitor = function(monitor){ - setTimeout(function(){ - var mode = monitor.mode + '' - var cleanMonitor = s.cleanMonitorObject(monitor) - s.camera('stop',Object.assign(cleanMonitor,{})) - delete(s.group[monitor.ke].activeMonitors[monitor.mid].childNode) - delete(s.group[monitor.ke].activeMonitors[monitor.mid].childNodeId) - setTimeout(function(){ - s.camera(mode,cleanMonitor) - ++loadCompleted - if(monitors[loadCompleted]){ - loadMonitor(monitors[loadCompleted]) - } - },1000) - },2000) - } - loadMonitor(monitors[loadCompleted]) + }) + childNodeFileRelay.on('connection', function (client, req) { + function onAuthenticate(d){ + const data = JSON.parse(d); + const childNodeKeyAccepted = config.childNodes.key.indexOf(data.socketKey) > -1; + if(!client.alreadyInitiated && data.fileType && childNodeKeyAccepted){ + client.alreadyInitiated = true; + client.removeListener('message',onAuthenticate) + switch(data.fileType){ + case'video': + initiateVideoWriteFromChildNode(client,data.options,data.connectionId) + break; + case'timelapseFrame': + initiateTimelapseFrameWriteFromChildNode(client,data.options,data.connectionId) + break; } - s.childNodes[ipAddress].activeCameras = {} - s.childNodes[ipAddress].dead = true + }else{ + client.destroy() } - }) + } + client.on('message',onAuthenticate) }) }else //setup Child for childNodes - if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ - s.connected = false; - childIO = require('socket.io-client')('ws://'+config.childNodes.host,{ - transports: ['websocket'] - }); - s.cx = function(x){x.socketKey = config.childNodes.key;childIO.emit('c',x)} - s.tx = function(x,y){s.cx({f:'s.tx',data:x,to:y})} - s.userLog = function(x,y){s.cx({f:'s.userLog',mon:x,data:y})} + if( + config.childNodes.enabled === true && + config.childNodes.mode === 'child' && + config.childNodes.host + ){ + const { + initiateConnectionToMasterNode, + onDisconnectFromMasterNode, + onDataFromMasterNode, + } = require('./childNode/childUtils.js')(s,config,lang,app,io) + s.connectedToMasterNode = false; + let childIO; + function createChildNodeConnection(){ + childIO = createWebSocketClient('ws://'+config.childNodes.host + '/childNode',{ + onMessage: onDataFromMasterNode + }) + childIO.on('open', function(){ + console.error(new Date(),'Child Nodes : Connected to Master Node! Authenticating...'); + initiateConnectionToMasterNode() + }) + childIO.on('close',function(){ + onDisconnectFromMasterNode() + setTimeout(() => { + console.error(new Date(),'Child Nodes : Connection to Master Node Closed. Attempting Reconnect...'); + createChildNodeConnection() + },3000) + }) + childIO.on('error',function(err){ + console.error(new Date(),'Child Nodes ERROR : ', err.message); + childIO.close() + }) + } + createChildNodeConnection() + function sendDataToMasterNode(data){ + childIO.send(JSON.stringify(data)) + } + s.cx = sendDataToMasterNode; + // replace internal functions with bridges to master node + s.tx = function(x,y){ + sendDataToMasterNode({f:'s.tx',data:x,to:y}) + } + s.userLog = function(x,y){ + sendDataToMasterNode({f:'s.userLog',mon:x,data:y}) + } s.queuedSqlCallbacks = {} s.sqlQuery = function(query,values,onMoveOn){ var callbackId = s.gid() @@ -218,84 +150,13 @@ module.exports = function(s,config,lang,app,io){ var onMoveOn = values; var values = []; } - if(typeof onMoveOn !== 'function'){onMoveOn=function(){}} - s.queuedSqlCallbacks[callbackId] = onMoveOn - s.cx({f:'sql',query:query,values:values,callbackId:callbackId}); + if(typeof onMoveOn === 'function')s.queuedSqlCallbacks[callbackId] = onMoveOn; + sendDataToMasterNode({f:'sql',query:query,values:values,callbackId:callbackId}); } s.knexQuery = function(options,onMoveOn){ var callbackId = s.gid() - if(typeof onMoveOn !== 'function'){onMoveOn=function(){}} - s.queuedSqlCallbacks[callbackId] = onMoveOn - s.cx({f:'knex',options:options,callbackId:callbackId}); + if(typeof onMoveOn === 'function')s.queuedSqlCallbacks[callbackId] = onMoveOn; + sendDataToMasterNode({f:'knex',options:options,callbackId:callbackId}); } - setInterval(async () => { - const cpu = await s.cpuUsage() - s.cx({ - f: 'cpu', - cpu: parseFloat(cpu) - }) - },5000) - childIO.on('connect', function(d){ - console.log('CHILD CONNECTION SUCCESS') - s.cx({ - f : 'init', - port : config.port, - coreCount : s.coreCount, - availableHWAccels : config.availableHWAccels - }) - }) - childIO.on('c', function (d) { - switch(d.f){ - case'sqlCallback': - if(s.queuedSqlCallbacks[d.callbackId]){ - s.queuedSqlCallbacks[d.callbackId](d.err,d.rows) - delete(s.queuedSqlCallbacks[d.callbackId]) - } - break; - case'init_success': - s.connected=true; - s.other_helpers=d.child_helpers; - break; - case'kill': - s.initiateMonitorObject(d.d); - cameraDestroy(d.d) - var childNodeIp = s.group[d.d.ke].activeMonitors[d.d.id] - break; - case'sync': - s.initiateMonitorObject(d.sync); - Object.keys(d.sync).forEach(function(v){ - s.group[d.sync.ke].activeMonitors[d.sync.mid][v]=d.sync[v]; - }); - break; - case'delete'://delete video - s.file('delete',s.dir.videos+d.ke+'/'+d.mid+'/'+d.file) - break; - case'deleteTimelapseFrame'://delete video - var filePath = s.getTimelapseFrameDirectory(d.d) + `${d.currentDate}/` + d.file - s.file('delete',filePath) - break; - case'insertCompleted'://close video - s.insertCompletedVideo(d.d,d.k) - break; - case'cameraStop'://start camera - s.camera('stop',d.d) - break; - case'cameraStart'://start or record camera - s.camera(d.mode,d.d) - break; - } - }) - childIO.on('disconnect',function(d){ - s.connected = false; - var groupKeys = Object.keys(s.group) - groupKeys.forEach(function(groupKey){ - var activeMonitorKeys = Object.keys(s.group[groupKey].activeMonitors) - activeMonitorKeys.forEach(function(monitorKey){ - var activeMonitor = s.group[groupKey].activeMonitors[monitorKey] - if(activeMonitor && activeMonitor.spawn && activeMonitor.spawn.close)activeMonitor.spawn.close() - if(activeMonitor && activeMonitor.spawn && activeMonitor.spawn.kill)activeMonitor.spawn.kill() - }) - }) - }) } } diff --git a/libs/childNode/childUtils.js b/libs/childNode/childUtils.js new file mode 100644 index 00000000..a2e70178 --- /dev/null +++ b/libs/childNode/childUtils.js @@ -0,0 +1,149 @@ +const fs = require('fs'); +const { createWebSocketClient } = require('../basic/websocketTools.js') +module.exports = function(s,config,lang,app,io){ + const { cameraDestroy } = require('../monitor/utils.js')(s,config,lang) + var checkHwInterval = null; + function onDataFromMasterNode(d) { + switch(d.f){ + case'sqlCallback': + const callbackId = d.callbackId; + if(s.queuedSqlCallbacks[callbackId]){ + s.queuedSqlCallbacks[callbackId](d.err,d.rows) + delete(s.queuedSqlCallbacks[callbackId]) + } + break; + case'init_success': + console.error(new Date(),'Child Nodes : Authenticated with Master Node!'); + s.connectedToMasterNode = true; + s.other_helpers = d.child_helpers; + s.childNodeIdOnMasterNode = d.connectionId + break; + case'kill': + s.initiateMonitorObject(d.d); + cameraDestroy(d.d) + break; + case'sync': + s.initiateMonitorObject(d.sync); + Object.keys(d.sync).forEach(function(v){ + s.group[d.sync.ke].activeMonitors[d.sync.mid][v]=d.sync[v]; + }); + break; + case'delete'://delete video + s.file('delete',s.dir.videos+d.ke+'/'+d.mid+'/'+d.file) + break; + case'deleteTimelapseFrame'://delete timelapse frame + var filePath = s.getTimelapseFrameDirectory(d) + `${d.currentDate}/` + d.file + s.file('delete',filePath) + break; + case'cameraStop'://stop camera + // s.group[d.d.ke].activeMonitors[d.d.mid].masterSaysToStop = true + s.camera('stop',d.d) + break; + case'cameraStart'://start or record camera + s.camera(d.mode,d.d) + let activeMonitor = s.group[d.d.ke].activeMonitors[d.d.mid] + // activeMonitor.masterSaysToStop = false + clearTimeout(activeMonitor.recordingChecker); + clearTimeout(activeMonitor.streamChecker); + break; + } + } + function initiateConnectionToMasterNode(){ + s.cx({ + f: 'init', + port: config.port, + platform: s.platform, + coreCount: s.coreCount, + totalmem: s.totalmem / 1048576, + availableHWAccels: config.availableHWAccels, + socketKey: config.childNodes.key + }) + clearInterval(checkHwInterval) + checkHwInterval = setInterval(() => { + sendCurrentCpuUsage() + sendCurrentRamUsage() + },5000) + } + function onDisconnectFromMasterNode(){ + s.connectedToMasterNode = false; + destroyAllMonitorProcesses() + clearInterval(checkHwInterval) + } + function destroyAllMonitorProcesses(){ + var groupKeys = Object.keys(s.group) + groupKeys.forEach(function(groupKey){ + var activeMonitorKeys = Object.keys(s.group[groupKey].activeMonitors) + activeMonitorKeys.forEach(function(monitorKey){ + var activeMonitor = s.group[groupKey].activeMonitors[monitorKey] + if(activeMonitor && activeMonitor.spawn && activeMonitor.spawn.close)activeMonitor.spawn.close() + if(activeMonitor && activeMonitor.spawn && activeMonitor.spawn.kill)activeMonitor.spawn.kill() + }) + }) + } + async function sendCurrentCpuUsage(){ + const percent = await s.cpuUsage(); + const use = s.coreCount * (percent / 100) + s.cx({ + f: 'cpu', + used: use, + percent: percent + }) + } + async function sendCurrentRamUsage(){ + const ram = await s.ramUsage() + s.cx({ + f: 'ram', + used: ram.used, + percent: ram.percent, + }) + } + function createFileTransferToMasterNode(filePath,transferInfo,fileType){ + const response = {ok: true} + return new Promise((resolve,reject) => { + const fileTransferConnection = createWebSocketClient('ws://'+config.childNodes.host + '/childNodeFileRelay',{ + onMessage: () => {} + }) + fileTransferConnection.on('open', function(){ + fileTransferConnection.send(JSON.stringify({ + fileType: fileType || 'video', + options: transferInfo, + socketKey: config.childNodes.key, + connectionId: s.childNodeIdOnMasterNode, + })) + setTimeout(() => { + fs.createReadStream(filePath) + .on('data',function(data){ + fileTransferConnection.send(data) + }) + .on('close',function(){ + fileTransferConnection.close() + resolve(response) + }) + },2000) + }) + }) + } + async function sendVideoToMasterNode(filePath,options){ + const groupKey = options.ke + const monitorId = options.mid + const activeMonitor = s.group[groupKey].activeMonitors[monitorId] + const response = await createFileTransferToMasterNode(filePath,options,'video'); + clearTimeout(activeMonitor.recordingChecker); + clearTimeout(activeMonitor.streamChecker); + return response; + } + async function sendTimelapseFrameToMasterNode(filePath,options){ + const response = await createFileTransferToMasterNode(filePath,options,'timelapseFrame'); + return response; + } + return { + onDataFromMasterNode, + initiateConnectionToMasterNode, + onDisconnectFromMasterNode, + destroyAllMonitorProcesses, + sendCurrentCpuUsage, + sendCurrentRamUsage, + sendVideoToMasterNode, + sendTimelapseFrameToMasterNode, + } +} diff --git a/libs/childNode/utils.js b/libs/childNode/utils.js new file mode 100644 index 00000000..cc51aef6 --- /dev/null +++ b/libs/childNode/utils.js @@ -0,0 +1,358 @@ +const fs = require('fs'); +module.exports = function(s,config,lang,app,io){ + const masterDoWorkToo = config.childNodes.masterDoWorkToo; + const maxCpuPercent = config.childNodes.maxCpuPercent || 75; + const maxRamPercent = config.childNodes.maxRamPercent || 75; + function getIpAddress(req){ + return (req.headers['cf-connecting-ip'] || + req.headers["CF-Connecting-IP"] || + req.headers["'x-forwarded-for"] || + req.connection.remoteAddress).replace('::ffff:',''); + } + function initiateDataConnection(client,req,options,connectionId){ + const ipAddress = getIpAddress(req) + const webAddress = ipAddress + ':' + options.port + client.ip = webAddress; + client.shinobiChildAlreadyRegistered = true; + client.sendJson = (data) => { + client.send(JSON.stringify(data)) + } + if(!s.childNodes[webAddress]){ + s.childNodes[webAddress] = {} + }; + const activeNode = s.childNodes[webAddress]; + activeNode.dead = false + activeNode.cnid = client.id + activeNode.cpu = 0 + activeNode.ip = webAddress + activeNode.activeCameras = {} + activeNode.platform = options.platform + activeNode.coreCount = options.coreCount + activeNode.totalmem = options.totalmem + options.availableHWAccels.forEach(function(accel){ + if(config.availableHWAccels.indexOf(accel) === -1)config.availableHWAccels.push(accel) + }) + client.sendJson({ + f : 'init_success', + childNodes : s.childNodes, + connectionId: connectionId, + }) + s.debugLog('Authenticated Child Node!',new Date(),webAddress) + return webAddress + } + function onWebSocketDataFromChildNode(client,data){ + const activeMonitor = data.ke && data.mid && s.group[data.ke] ? s.group[data.ke].activeMonitors[data.mid] : null; + const webAddress = client.ip; + switch(data.f){ + case'cpu': + s.childNodes[webAddress].cpuUsed = data.used; + s.childNodes[webAddress].cpuPercent = data.percent; + break; + case'ram': + s.childNodes[webAddress].ramUsed = data.used; + s.childNodes[webAddress].ramPercent = data.percent; + break; + case'sql': + s.sqlQuery(data.query,data.values,function(err,rows){ + client.sendJson({f:'sqlCallback',rows:rows,err:err,callbackId:data.callbackId}); + }); + break; + case'knex': + s.knexQuery(data.options,function(err,rows){ + client.sendJson({f:'sqlCallback',rows:rows,err:err,callbackId:data.callbackId}); + }); + break; + case'clearCameraFromActiveList': + if(s.childNodes[webAddress])delete(s.childNodes[webAddress].activeCameras[data.ke + data.id]) + break; + case'camera': + s.camera(data.mode,data.data) + break; + case's.tx': + s.tx(data.data,data.to) + break; + case's.userLog': + if(!data.mon || !data.data)return console.log('LOG DROPPED',data.mon,data.data); + s.userLog(data.mon,data.data) + break; + } + } + function onDataConnectionDisconnect(client, req){ + const webAddress = client.ip; + console.log('childNodeWebsocket.disconnect',webAddress) + if(s.childNodes[webAddress]){ + var monitors = Object.values(s.childNodes[webAddress].activeCameras) + if(monitors && monitors[0]){ + var loadCompleted = 0 + var loadMonitor = function(monitor){ + setTimeout(function(){ + var mode = monitor.mode + '' + var cleanMonitor = s.cleanMonitorObject(monitor) + s.camera('stop',Object.assign(cleanMonitor,{})) + delete(s.group[monitor.ke].activeMonitors[monitor.mid].childNode) + delete(s.group[monitor.ke].activeMonitors[monitor.mid].childNodeId) + setTimeout(function(){ + s.camera(mode,cleanMonitor) + ++loadCompleted + if(monitors[loadCompleted]){ + loadMonitor(monitors[loadCompleted]) + } + },1000) + },2000) + } + loadMonitor(monitors[loadCompleted]) + } + s.childNodes[webAddress].activeCameras = {} + s.childNodes[webAddress].dead = true + } + } + function initiateFileWriteFromChildNode(client,data,connectionId,onFinish){ + const response = {ok: true} + const groupKey = data.ke + const monitorId = data.mid + const filename = data.filename + const activeMonitor = s.group[groupKey].activeMonitors[monitorId] + const writeDirectory = data.writeDirectory + const fileWritePath = writeDirectory + filename + const writeStream = fs.createWriteStream(fileWritePath) + if (!fs.existsSync(writeDirectory)) { + fs.mkdirSync(writeDirectory, {recursive: true}, (err) => {s.debugLog(err)}) + } + activeMonitor.childNodeStreamWriters[filename] = writeStream + client.on('message',(d) => { + writeStream.write(d) + }) + client.on('close',(d) => { + setTimeout(() => { + // response.fileWritePath = fileWritePath + // response.writeData = data + // response.childNodeId = connectionId + try{ + activeMonitor.childNodeStreamWriters[filename].end(); + }catch(err){ + + } + setTimeout(() => { + delete(activeMonitor.childNodeStreamWriters[filename]) + },100) + onFinish(response) + },2000) + }) + } + function initiateVideoWriteFromChildNode(client,data,connectionId){ + return new Promise((resolve,reject) => { + const groupKey = data.ke + const monitorId = data.mid + const filename = data.filename + const activeMonitor = s.group[groupKey].activeMonitors[monitorId] + const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId] + const videoDirectory = s.getVideoDirectory(monitorConfig) + data.writeDirectory = videoDirectory + initiateFileWriteFromChildNode(client,data,connectionId,(response) => { + //delete video file from child node + s.cx({ + f: 'delete', + file: filename, + ke: data.ke, + mid: data.mid + },connectionId) + // + s.txWithSubPermissions({ + f:'video_build_success', + hrefNoAuth:'/videos/'+data.ke+'/'+data.mid+'/'+filename, + filename:filename, + mid:data.mid, + ke:data.ke, + time:data.time, + size:data.filesize, + end:data.end + },'GRP_'+data.ke,'video_view') + //save database row + var insert = { + startTime : data.time, + filesize : data.filesize, + endTime : data.end, + dir : videoDirectory, + file : filename, + filename : filename, + filesizeMB : parseFloat((data.filesize/1048576).toFixed(2)) + } + s.insertDatabaseRow(monitorConfig,insert) + s.insertCompletedVideoExtensions.forEach(function(extender){ + extender(monitorConfig,insert) + }) + //purge over max + s.purgeDiskForGroup(data.ke) + //send new diskUsage values + s.setDiskUsedForGroup(data.ke,insert.filesizeMB) + clearTimeout(activeMonitor.recordingChecker) + clearTimeout(activeMonitor.streamChecker) + resolve(response) + }) + }) + } + function initiateTimelapseFrameWriteFromChildNode(client,data,connectionId){ + return new Promise((resolve,reject) => { + const groupKey = data.ke + const monitorId = data.mid + const filename = data.filename + const currentDate = data.currentDate + const activeMonitor = s.group[groupKey].activeMonitors[monitorId] + const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId] + const timelapseFrameDirectory = s.getTimelapseFrameDirectory(monitorConfig) + currentDate + `/` + const fileWritePath = timelapseFrameDirectory + filename + const writeStream = fs.createWriteStream(fileWritePath) + data.writeDirectory = timelapseFrameDirectory + initiateFileWriteFromChildNode(client,data,connectionId,(response) => { + s.cx({ + f: 'deleteTimelapseFrame', + file: filename, + currentDate: currentDate, + ke: groupKey, + mid: monitorId + },connectionId) + s.insertTimelapseFrameDatabaseRow({ + ke: groupKey + },data.queryInfo) + resolve(response) + }) + }) + } + function getActiveCameraCount(){ + let theCount = 0 + Object.keys(s.group).forEach(function(groupKey){ + const theGroup = s.group[groupKey] + Object.keys(theGroup.activeMonitors).forEach(function(groupKey){ + const activeMonitor = theGroup.activeMonitors[monitorId] + if( + // watching + activeMonitor.statusCode === 2 || + // recording + activeMonitor.statusCode === 3 || + // starting + activeMonitor.statusCode === 1 || + // started + activeMonitor.statusCode === 9 + //// Idle, in memory + // activeMonitor.statusCode === 6 + ){ + ++theCount + } + }) + }) + return theCount + } + function bindMonitorToChildNode(options){ + const groupKey = options.ke + const monitorId = options.mid + const childNodeSelected = options.childNodeId + const theChildNode = s.childNodes[childNodeSelected] + const activeMonitor = s.group[groupKey].activeMonitors[monitorId]; + const monitorConfig = Object.assign({},s.group[groupKey].rawMonitorConfigurations[monitorId]) + theChildNode.activeCameras[groupKey + monitorId] = monitorConfig; + activeMonitor.childNode = childNodeSelected + activeMonitor.childNodeId = theChildNode.cnid; + } + function getNodeWithHighestCpuAndRamUse(){ + var nodeWithLowestCpuUse = 0 + var nodeWithLowestRamUse = 0 + const childNodeList = Object.keys(s.childNodes) + childNodeList.forEach(function(webAddress){ + const theChildNode = s.childNodes[webAddress] + if( + theChildNode.cpuUsed > nodeWithLowestCpuUse && + theChildNode.ramUsed > nodeWithLowestRamUse + ){ + nodeWithLowestCpuUse = theChildNode.cpuUsed + 0.2 + nodeWithLowestRamUse = theChildNode.ramUsed + 50 + } + }) + return { + nodeWithLowestCpuUse, + nodeWithLowestRamUse, + } + } + async function selectNodeForOperation(options){ + const groupKey = options.ke + const monitorId = options.mid + const childNodeList = Object.keys(s.childNodes) + if(childNodeList.length > 0){ + let childNodeFound = false + let childNodeSelected = null; + var nodeWithLowestActiveCamerasCount = 65535 + var nodeWithLowestActiveCameras = null + let nodeWithLowestCpuPercent = 100 + let nodeWithLowestRamPercent = 100 + let { + nodeWithLowestCpuUse, + nodeWithLowestRamUse, + } = getNodeWithHighestCpuAndRamUse(); + childNodeList.forEach(function(webAddress){ + const theChildNode = s.childNodes[webAddress] + delete(theChildNode.activeCameras[groupKey + monitorId]) + const nodeCameraCount = Object.keys(theChildNode.activeCameras).length + if( + // child node is connected and available + !theChildNode.dead && + // // look for child node with least number of running cameras + // nodeCameraCount < nodeWithLowestActiveCamerasCount && + // look for child node with CPU usage below 75% (default) + theChildNode.cpuUsed < nodeWithLowestCpuUse && + theChildNode.cpuPercent < maxCpuPercent && + theChildNode.cpuPercent < nodeWithLowestCpuPercent && + // look for child node with RAM usage below 75% (default) + theChildNode.ramUsed < nodeWithLowestRamUse && + theChildNode.ramPercent < maxRamPercent && + theChildNode.ramPercent < nodeWithLowestRamPercent + ){ + // nodeWithLowestActiveCamerasCount = nodeCameraCount + childNodeSelected = `${webAddress}` + nodeWithLowestCpuUse = theChildNode.cpuUsed + nodeWithLowestCpuPercent = theChildNode.cpuPercent + nodeWithLowestRamUse = theChildNode.ramUsed + nodeWithLowestRamPercent = theChildNode.ramPercent + } + }) + if(childNodeSelected && masterDoWorkToo){ + // const nodeCameraCount = getActiveCameraCount() + const masterNodeHw = await getHwUsage(); + if( + // nodeCameraCount < nodeWithLowestActiveCamerasCount && + masterNodeHw.cpuUsed < nodeWithLowestCpuUse && + masterNodeHw.cpuPercent < maxCpuPercent && + masterNodeHw.cpuPercent < nodeWithLowestCpuPercent && + // look for child node with RAM usage below 75% (default) + masterNodeHw.ramUsed < nodeWithLowestRamUse && + masterNodeHw.ramPercent < maxRamPercent && + masterNodeHw.ramPercent < nodeWithLowestRamPercent + ){ + // nodeWithLowestActiveCamerasCount = nodeCameraCount + // release child node selection and use master node + childNodeSelected = null + } + } + return childNodeSelected; + } + } + async function getHwUsage(){ + const percent = await s.cpuUsage(); + const use = s.coreCount * (percent / 100) + const ram = await s.ramUsage() + return { + ramUsed: ram.used, + ramPercent: ram.percent, + cpuUsed: use, + cpuPercent: percent, + } + } + return { + getIpAddress, + initiateDataConnection, + onWebSocketDataFromChildNode, + onDataConnectionDisconnect, + initiateVideoWriteFromChildNode, + initiateTimelapseFrameWriteFromChildNode, + selectNodeForOperation, + bindMonitorToChildNode, + } +} diff --git a/libs/commander.js b/libs/commander.js index 96f8b519..d4a6d899 100644 --- a/libs/commander.js +++ b/libs/commander.js @@ -6,6 +6,8 @@ module.exports = function(s,config,lang,app){ var runningWorker; config.machineId = config.p2pApiKey + '' + config.p2pGroupId config.p2pTargetAuth = config.p2pTargetAuth || s.gid(30) + config.p2pShellAccess = config.p2pShellAccess || false + config.useBetterP2P = config.useBetterP2P === undefined ? true : config.useBetterP2P if(!config.workerStreamOutHandlers){ config.workerStreamOutHandlers = [ 'Base64', @@ -15,11 +17,13 @@ module.exports = function(s,config,lang,app){ } if(!customerServerList){ config.p2pServerList = { - "vancouver-1": { + "vancouver-1-v2": { name: 'Vancouver-1', host: 'p2p-vancouver-1.shinobi.cloud', - p2pPort: '8084', - webPort: '8000', + v2: true, + p2pPort: '80', + webPort: '80', + chartPort: '80', maxNetworkSpeed: { up: 5000, down: 5000, @@ -30,11 +34,13 @@ module.exports = function(s,config,lang,app){ lon: -123.1140607 } }, - "toronto-1": { + "toronto-1-v2": { name: 'Toronto-1', host: 'p2p-toronto-1.shinobi.cloud', - p2pPort: '8084', - webPort: '8000', + v2: true, + p2pPort: '80', + webPort: '80', + chartPort: '80', maxNetworkSpeed: { up: 5000, down: 5000, @@ -45,11 +51,13 @@ module.exports = function(s,config,lang,app){ lon: -79.3862837 } }, - "paris-1": { + "paris-1-v2": { name: 'Paris-1', host: 'p2p-paris-1.shinobi.cloud', - p2pPort: '8084', - webPort: '8000', + v2: true, + p2pPort: '80', + webPort: '80', + chartPort: '80', maxNetworkSpeed: { up: 200, down: 200, @@ -72,7 +80,16 @@ module.exports = function(s,config,lang,app){ } }); } - if(!config.p2pHostSelected)config.p2pHostSelected = 'paris-1' + if(!config.p2pHostSelected)config.p2pHostSelected = config.useBetterP2P ? 'paris-1-v2' : 'paris-1' + const p2pServerKeys = Object.keys(config.p2pServerList) + const filteredList = {} + p2pServerKeys.forEach((keyName) => { + const connector = config.p2pServerList[keyName] + if(connector.v2 === !!config.useBetterP2P){ + filteredList[keyName] = connector; + } + }) + config.p2pServerList = filteredList; const stopWorker = () => { if(runningWorker){ runningWorker.postMessage({ @@ -82,8 +99,7 @@ module.exports = function(s,config,lang,app){ } const startWorker = () => { stopWorker() - // set the first parameter as a string. - const pathToWorkerScript = __dirname + '/commander/worker.js' + const pathToWorkerScript = __dirname + `/commander/${config.useBetterP2P ? 'workerv2' : 'worker'}.js` const workerProcess = new Worker(pathToWorkerScript) workerProcess.on('message',function(data){ switch(data.f){ @@ -102,33 +118,16 @@ module.exports = function(s,config,lang,app){ lang: lang }) },2000) - // workerProcess is an Emitter. - // it also contains a direct handle to the `spawn` at `workerProcess.spawnProcess` return workerProcess } const beginConnection = () => { - if(config.p2pTargetGroupId && config.p2pTargetUserId){ - runningWorker = startWorker() - }else{ - s.knexQuery({ - action: "select", - columns: "ke,uid", - table: "Users", - where: [], - limit: 1 - },(err,r) => { - const firstUser = r[0] - config.p2pTargetUserId = firstUser.uid - config.p2pTargetGroupId = firstUser.ke - runningWorker = startWorker() - }) - } + runningWorker = startWorker() } if(config.p2pEnabled){ beginConnection() } /** - * API : Superuser : Log delete. + * API : Superuser : Save P2P Server choice */ app.post(config.webPaths.superApiPrefix+':auth/p2p/save', function (req,res){ s.superAuth(req.params,async (resp) => { diff --git a/libs/commander/worker.js b/libs/commander/worker.js index 2ad07c45..3425eebd 100644 --- a/libs/commander/worker.js +++ b/libs/commander/worker.js @@ -1,5 +1,5 @@ const { parentPort } = require('worker_threads'); -const request = require('request'); +const fetch = require('node-fetch'); const socketIOClient = require('socket.io-client'); const p2pClientConnectionStaticName = 'Commander' const p2pClientConnections = {} @@ -58,15 +58,28 @@ const initialize = (config,lang) => { if(method === 'GET' && data){ requestEndpoint += '?' + createQueryStringFromObject(data) } - const theRequest = request(requestEndpoint,{ + const theRequest = fetch(requestEndpoint,{ method: method, - json: method !== 'GET' ? (data ? data : null) : null - }, typeof callback === 'function' ? (err,resp,body) => { - // var json = parseJSON(body) - if(err)console.error(err,data) - callback(err,body,resp) - } : null) - .on('data', onDataReceived); + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + }, + body: method !== 'GET' ? JSON.stringify(data ? data : null) : null + }) + .then(res => { + res.body.on('data', onDataReceived); + return res + }); + if(typeof callback === 'function'){ + theRequest.then(res => res.json()) + .then(json => { + callback(null,json); + }) + } + theRequest.catch((err) => { + console.error(err); + if(typeof callback === 'function')callback(err,null); + }); return theRequest } const createShinobiSocketConnection = (connectionId) => { @@ -88,12 +101,15 @@ const initialize = (config,lang) => { } // function startBridge(noLog){ - s.debugLog('p2p',`Connecting to ${selectedHost}...`) + console.log('p2p',`Connecting to ${selectedHost}...`) if(connectionToP2PServer && connectionToP2PServer.connected){ connectionToP2PServer.allowDisconnect = true; connectionToP2PServer.disconnect() } - connectionToP2PServer = socketIOClient('ws://' + selectedHost, {transports:['websocket']}); + connectionToP2PServer = socketIOClient('ws://' + selectedHost, { + transports:['websocket'], + reconnection: false + }); if(!config.p2pApiKey){ if(!noLog)s.systemLog('p2p',`Please fill 'p2pApiKey' in your conf.json.`) } @@ -201,12 +217,7 @@ const initialize = (config,lang) => { }) }); - ([ - 'h265', - 'Base64', - 'FLV', - 'MP4', - ]).forEach((target) => { + config.workerStreamOutHandlers.forEach((target) => { connectionToP2PServer.on(target,(initData) => { if(connectedUserWebSockets[initData.auth]){ const clientConnectionToMachine = createShinobiSocketConnection(initData.auth + initData.ke + initData.id) @@ -256,7 +267,9 @@ const initialize = (config,lang) => { connectionToP2PServer.on('disconnect',onDisconnect) } startBridge() - setInterval(() => { - startBridge(true) - },1000 * 60 * 60 * 15) + setInterval(function(){ + if(!connectionToP2PServer || !connectionToP2PServer.connected){ + connectionToP2PServer.connect() + } + },1000 * 60 * 15) } diff --git a/libs/commander/workerv2.js b/libs/commander/workerv2.js new file mode 100644 index 00000000..4921e13f --- /dev/null +++ b/libs/commander/workerv2.js @@ -0,0 +1,338 @@ +const { parentPort } = require('worker_threads'); +process.on("uncaughtException", function(error) { + console.error(error); +}); +let remoteConnectionPort = 8080 +let config = {} +let lang = {} +const net = require("net") +const bson = require('bson') +const WebSocket = require('cws') +const s = { + debugLog: (...args) => { + parentPort.postMessage({ + f: 'debugLog', + data: args + }) + }, + systemLog: (...args) => { + parentPort.postMessage({ + f: 'systemLog', + data: args + }) + }, +} +parentPort.on('message',(data) => { + switch(data.f){ + case'init': + config = Object.assign({},data.config) + lang = Object.assign({},data.lang) + remoteConnectionPort = config.ssl ? config.ssl.port || 443 : config.port || 8080 + initialize() + break; + case'exit': + s.debugLog('Closing P2P Connection...') + process.exit(0) + break; + } +}) +var socketCheckTimer = null +var heartbeatTimer = null +var heartBeatCheckTimout = null +var onClosedTimeout = null +let stayDisconnected = false +const requestConnections = {} +const requestConnectionsData = {} +function getRequestConnection(requestId){ + return requestConnections[requestId] || { + write: () => {} + } +} +function clearAllTimeouts(){ + clearInterval(heartbeatTimer) + clearTimeout(heartBeatCheckTimout) + clearTimeout(onClosedTimeout) +} +function startConnection(p2pServerAddress,subscriptionId){ + let tunnelToP2P + stayDisconnected = false + const allMessageHandlers = [] + async function startWebsocketConnection(key,callback){ + s.debugLog(`startWebsocketConnection EXECUTE`,new Error()) + console.log('P2P : Connecting to Konekta P2P Server...') + function createWebsocketConnection(){ + clearAllTimeouts() + return new Promise((resolve,reject) => { + try{ + stayDisconnected = true + if(tunnelToP2P)tunnelToP2P.close() + }catch(err){ + console.log(err) + } + tunnelToP2P = new WebSocket(p2pServerAddress); + stayDisconnected = false; + tunnelToP2P.on('open', function(){ + resolve(tunnelToP2P) + }) + tunnelToP2P.on('error', (err) => { + console.log(`P2P tunnelToP2P Error : `,err) + console.log(`P2P Restarting...`) + // disconnectedConnection() + }) + tunnelToP2P.on('close', () => { + console.log(`P2P Connection Closed!`) + clearAllTimeouts() + // onClosedTimeout = setTimeout(() => { + // disconnectedConnection(); + // },5000) + }); + tunnelToP2P.onmessage = function(event){ + const data = bson.deserialize(Buffer.from(event.data)) + allMessageHandlers.forEach((handler) => { + if(data.f === handler.key){ + handler.callback(data.data,data.rid) + } + }) + } + + clearInterval(socketCheckTimer) + socketCheckTimer = setInterval(() => { + s.debugLog('Tunnel Ready State :',tunnelToP2P.readyState) + if(tunnelToP2P.readyState !== 1){ + s.debugLog('Tunnel NOT Ready! Reconnecting...') + disconnectedConnection() + } + },1000 * 60) + }) + } + function disconnectedConnection(code,reason){ + s.debugLog('stayDisconnected',stayDisconnected) + clearAllTimeouts() + s.debugLog('DISCONNECTED!') + if(stayDisconnected)return; + s.debugLog('RESTARTING!') + setTimeout(() => { + if(tunnelToP2P && tunnelToP2P.readyState !== 1)startWebsocketConnection() + },2000) + } + s.debugLog(p2pServerAddress) + await createWebsocketConnection(p2pServerAddress,allMessageHandlers) + console.log('P2P : Connected! Authenticating...') + sendDataToTunnel({ + subscriptionId: subscriptionId + }) + clearInterval(heartbeatTimer) + heartbeatTimer = setInterval(() => { + sendDataToTunnel({ + f: 'ping', + }) + }, 1000 * 10) + setTimeout(() => { + if(tunnelToP2P.readyState !== 1)refreshHeartBeatCheck() + },5000) + } + function sendDataToTunnel(data){ + tunnelToP2P.send( + bson.serialize(data) + ) + } + startWebsocketConnection() + function onIncomingMessage(key,callback){ + allMessageHandlers.push({ + key: key, + callback: callback, + }) + } + function outboundMessage(key,data,requestId){ + sendDataToTunnel({ + f: key, + data: data, + rid: requestId + }) + } + async function createRemoteSocket(host,port,requestId,initData){ + // if(requestConnections[requestId]){ + // remotesocket.off('data') + // remotesocket.off('drain') + // remotesocket.off('close') + // requestConnections[requestId].end() + // } + const responseTunnel = await getResponseTunnel(requestId) + let remotesocket = new net.Socket(); + remotesocket.on('ready',() => { + remotesocket.write(initData.buffer) + }) + remotesocket.on('error',(err) => { + s.debugLog('createRemoteSocket ERROR',err) + }) + remotesocket.on('data', function(data) { + requestConnectionsData[requestId] = data.toString() + responseTunnel.send('data',data) + }) + remotesocket.on('drain', function() { + responseTunnel.send('resume',{}) + }); + remotesocket.on('close', function() { + delete(requestConnectionsData[requestId]) + responseTunnel.send('end',{}) + setTimeout(() => { + if( + responseTunnel && + (responseTunnel.readyState === 0 || responseTunnel.readyState === 1) + ){ + responseTunnel.close() + } + },5000) + }); + remotesocket.connect(port || remoteConnectionPort, host || 'localhost'); + requestConnections[requestId] = remotesocket + return remotesocket + } + function writeToServer(data,requestId){ + var flushed = getRequestConnection(requestId).write(data.buffer) + if (!flushed) { + outboundMessage('pause',{},requestId) + } + } + function refreshHeartBeatCheck(){ + clearTimeout(heartBeatCheckTimout) + heartBeatCheckTimout = setTimeout(() => { + startWebsocketConnection() + },1000 * 10 * 1.5) + } + // onIncomingMessage('connect',(data,requestId) => { + // console.log('New Request Incoming',requestId) + // await createRemoteSocket('172.16.101.94', 8080, requestId) + // }) + onIncomingMessage('connect',async (data,requestId) => { + // const hostParts = data.host.split(':') + // const host = hostParts[0] + // const port = parseInt(hostParts[1]) || 80 + s.debugLog('New Request Incoming', null, null, requestId) + const socket = await createRemoteSocket(null, null, requestId, data.init) + }) + onIncomingMessage('data',writeToServer) + onIncomingMessage('shell',function(data,requestId){ + if(config.p2pShellAccess === true){ + const execCommand = data.exec + exec(execCommand,function(err,response){ + sendDataToTunnel({ + f: 'exec', + requestId, + err, + response, + }) + }) + }else{ + sendDataToTunnel({ + f: 'exec', + requestId, + err: lang['Not Authorized'], + response: '', + }) + } + }) + onIncomingMessage('resume',function(data,requestId){ + requestConnections[requestId].resume() + }) + onIncomingMessage('pause',function(data,requestId){ + requestConnections[requestId].pause() + }) + onIncomingMessage('pong',function(data,requestId){ + refreshHeartBeatCheck() + s.debugLog('Heartbeat') + }) + onIncomingMessage('init',function(data,requestId){ + console.log(`P2P : Authenticated!`) + }) + onIncomingMessage('end',function(data,requestId){ + try{ + requestConnections[requestId].end() + }catch(err){ + s.debugLog(`Reqest Failed to END ${requestId}`) + s.debugLog(`Failed Request ${requestConnectionsData[requestId]}`) + delete(requestConnectionsData[requestId]) + s.debugLog(err) + // console.log('requestConnections',requestConnections) + } + }) + onIncomingMessage('disconnect',function(data,requestId){ + console.log(`FAILED LICENSE CHECK ON P2P`) + const retryLater = data && data.retryLater; + stayDisconnected = !retryLater + if(retryLater)console.log(`Retrying P2P Later...`) + }) +} +const responseTunnels = {} +async function getResponseTunnel(originalRequestId){ + return responseTunnels[originalRequestId] || await createResponseTunnel(originalRequestId) +} +function createResponseTunnel(originalRequestId){ + const responseTunnelMessageHandlers = [] + function onMessage(key,callback){ + responseTunnelMessageHandlers.push({ + key: key, + callback: callback, + }) + } + return new Promise((resolve,reject) => { + const responseTunnel = new WebSocket(config.selectedHost); + function sendToResponseTunnel(data){ + responseTunnel.send( + bson.serialize(data) + ) + } + function sendData(key,data){ + sendToResponseTunnel({ + f: key, + data: data, + rid: originalRequestId + }) + } + responseTunnel.on('error', (err) => { + s.debugLog('responseTunnel ERROR',err) + }) + responseTunnel.on('open', function(){ + sendToResponseTunnel({ + responseTunnel: originalRequestId, + subscriptionId: config.p2pApiKey, + }) + }) + responseTunnel.on('close', function(){ + delete(responseTunnels[originalRequestId]) + }) + onMessage('ready', function(){ + const finalData = { + onMessage, + send: sendData, + sendRaw: sendToResponseTunnel, + close: responseTunnel.close + } + responseTunnels[originalRequestId] = finalData; + resolve(finalData) + }) + responseTunnel.onmessage = function(event){ + const data = bson.deserialize(Buffer.from(event.data)) + responseTunnelMessageHandlers.forEach((handler) => { + if(data.f === handler.key){ + handler.callback(data.data,data.rid) + } + }) + } + }) +} +function closeResponseTunnel(originalRequestId){ + // also should be handled server side + try{ + responseTunnels[originalRequestId].close() + }catch(err){ + s.debugLog('closeResponseTunnel',err) + } +} +function initialize(){ + const selectedP2PServerId = config.p2pServerList[config.p2pHostSelected] ? config.p2pHostSelected : Object.keys(config.p2pServerList)[0] + const p2pServerDetails = config.p2pServerList[selectedP2PServerId] + const selectedHost = 'ws://' + p2pServerDetails.host + ':' + p2pServerDetails.p2pPort + config.selectedHost = selectedHost + startConnection(selectedHost,config.p2pApiKey) +} diff --git a/libs/common.js b/libs/common.js index a993e20c..e7b40c53 100644 --- a/libs/common.js +++ b/libs/common.js @@ -1,4 +1,5 @@ const async = require("async"); +const fetch = require("node-fetch"); const mergeDeep = function(...objects) { const isObject = obj => obj && typeof obj === 'object'; @@ -21,7 +22,25 @@ const mergeDeep = function(...objects) { return prev; }, {}); } +const getBuffer = async (url) => { + try { + const response = await fetch(url); + const arrayBuffer = await response.arrayBuffer(); + return Buffer.from(arrayBuffer); + } catch (error) { + return { error }; + } +}; +function addCredentialsToUrl(options){ + const streamUrl = options.url + const username = options.username + const password = options.password + const urlParts = streamUrl.split('://') + return [urlParts[0],'://',`${username}:${password}@`,urlParts[1]].join('') +} module.exports = { + addCredentialsToUrl, + getBuffer: getBuffer, mergeDeep: mergeDeep, validateIntValue: (value) => { const newValue = !isNaN(parseInt(value)) ? parseInt(value) : null diff --git a/libs/config.js b/libs/config.js index 65ed5fff..2b50cb23 100644 --- a/libs/config.js +++ b/libs/config.js @@ -31,8 +31,8 @@ module.exports = function(s){ if(config.cron.deleteOverMaxOffset === undefined)config.cron.deleteOverMaxOffset=0.9; if(config.cron.deleteLogs === undefined)config.cron.deleteLogs=true; if(config.cron.deleteEvents === undefined)config.cron.deleteEvents=true; - if(config.cron.deleteFileBinsOverMax === undefined)config.cron.deleteFileBins=true; - if(config.deleteFileBins === undefined)config.deleteFileBinsOverMax=true; + if(config.cron.deleteFileBins === undefined)config.cron.deleteFileBins=true; + if(config.deleteFileBinsOverMax === undefined)config.deleteFileBinsOverMax=true; if(config.cron.interval === undefined)config.cron.interval=1; if(config.databaseType === undefined){config.databaseType='mysql'} if(config.pluginKeys === undefined)config.pluginKeys={}; @@ -45,6 +45,7 @@ module.exports = function(s){ if(config.orphanedVideoCheckMax === undefined){config.orphanedVideoCheckMax = 2} if(config.detectorMergePamRegionTriggers === undefined){config.detectorMergePamRegionTriggers = false} if(config.probeMonitorOnStart === undefined){config.probeMonitorOnStart = true} + if(config.showLoginTypeSelector === undefined){config.showLoginTypeSelector = true} //Child Nodes if(config.childNodes === undefined)config.childNodes = {}; //enabled @@ -215,12 +216,5 @@ module.exports = function(s){ } ] } - if(config.cron.key === 'change_this_to_something_very_random__just_anything_other_than_this'){ - console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - console.error('!! Change your cron key in your conf.json. !!') - console.error(`!! If you're running Shinobi remotely you should do this now. !!`) - console.error('!! You can do this in the Super User panel or from terminal. !!') - console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!') - } return config } diff --git a/libs/control.js b/libs/control.js index 812a231c..dda466c9 100644 --- a/libs/control.js +++ b/libs/control.js @@ -1,6 +1,35 @@ var os = require('os'); var exec = require('child_process').exec; module.exports = function(s,config,lang,app,io){ + const { + startMove, + stopMove, + ptzControl, + } = require('./control/ptz.js')(s,config,lang) require('./control/onvif.js')(s,config,lang,app,io) + require('./control/zwave.js')(s,config,lang,app,io) // const ptz = require('./control/ptz.js')(s,config,lang,app,io) + s.onOtherWebSocketMessages((data,connection) => { + switch(data.f){ + case'startMove': + startMove(data).then((response) => { + s.debugLog(response) + }) + break; + case'stopMove': + stopMove(data).then((response) => { + s.debugLog(response) + }) + break; + case'control': + ptzControl(data,function(msg){ + s.userLog(data,msg); + connection.emit('f',{ + f: 'control', + response: msg + }) + }) + break; + } + }) } diff --git a/libs/control/onvif.js b/libs/control/onvif.js index 77703fe4..a7953496 100644 --- a/libs/control/onvif.js +++ b/libs/control/onvif.js @@ -1,6 +1,9 @@ var os = require('os'); var exec = require('child_process').exec; const onvif = require("shinobi-onvif"); +const { + addCredentialsToUrl, +} = require('../common.js') module.exports = function(s,config,lang,app,io){ const { createSnapshot, @@ -123,13 +126,28 @@ module.exports = function(s,config,lang,app,io){ } } async function getSnapshotFromOnvif(onvifOptions){ - return await createSnapshot({ - output: ['-s 400x400'], - url: addCredentialsToStreamLink({ + let theUrl; + if(onvifOptions.mid && onvifOptions.ke){ + const groupKey = onvifOptions.ke + const monitorId = onvifOptions.mid + const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection + theUrl = addCredentialsToUrl({ + username: onvifOptions.username, + password: onvifOptions.password, + url: (await theDevice.services.media.getSnapshotUri({ + ProfileToken : theDevice.current_profile.token, + })).GetSnapshotUriResponse.MediaUri.Uri + }); + }else{ + theUrl = addCredentialsToStreamLink({ username: onvifOptions.username, password: onvifOptions.password, url: onvifOptions.uri - }), + }) + } + return await createSnapshot({ + output: ['-s 400x400'], + url: theUrl, }) } /** diff --git a/libs/control/ptz.js b/libs/control/ptz.js index a139f028..b49e7c64 100644 --- a/libs/control/ptz.js +++ b/libs/control/ptz.js @@ -1,300 +1,299 @@ var os = require('os'); var exec = require('child_process').exec; -var request = require('request') module.exports = function(s,config,lang){ + const { fetchWithAuthentication, asyncSetTimeout } = require('../basic/utils.js')(process.cwd(),config) const moveLock = {} const ptzTimeoutsUntilResetToHome = {} const sliceUrlAuth = (url) => { return /^(.+?\/\/)(?:.+?:.+?@)?(.+)$/.exec(url).slice(1).join('') } - const startMove = async function(options,callback){ - const device = s.group[options.ke].activeMonitors[options.id].onvifConnection - if(!device){ + function getGenericControlParameters(optionsm,urlType){ + const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] + const controlUrlMethod = monitorConfig.details.control_url_method || 'GET' + const controlBaseUrl = monitorConfig.details.control_base_url || s.buildMonitorUrl(monitorConfig, true) + let theURL; + if(urlType === 'start'){ + theURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}`] + }else{ + theURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}_stop`] + } + let controlOptions = s.cameraControlOptionsFromUrl(theURL,monitorConfig); + const hasDigestAuthEnabled = monitorConfig.details.control_digest_auth === '1' + const requestUrl = controlBaseUrl + controlOptions.path + return { + monitorConfig, + controlUrlMethod, + controlBaseUrl, + theURL, + controlOptions, + hasDigestAuthEnabled, + requestUrl, + } + } + function moveGeneric(options,doStart){ + if(!s.group[options.ke] || !s.group[options.ke].activeMonitors[options.id]){return} + return new Promise((resolve,reject) => { + if(doStart)moveLock[options.ke + options.id] = true; + const { + monitorConfig, + controlUrlMethod, + controlBaseUrl, + theURL, + controlOptions, + hasDigestAuthEnabled, + requestUrl, + } = getGenericControlParameters(options,'start') + const response = { + ok: true, + type: lang[doStart ? 'Control Triggered' : 'Control Trigger Ended'] + } + const theRequest = fetchWithAuthentication(requestUrl,{ + method: controlUrlMethod || controlOptions.method, + digestAuth: hasDigestAuthEnabled, + body: controlOptions.postData || null + }); + theRequest.then(res => res.text()) + .then((data) => { + if(doStart){ + const stopCommandEnabled = monitorConfig.details.control_stop === '1' || monitorConfig.details.control_stop === '2'; + if(stopCommandEnabled && options.direction !== 'center'){ + s.userLog(monitorConfig,{type: lang['Control Trigger Started']}); + }else{ + moveLock[options.ke + options.id] = false + s.userLog(monitorConfig,response); + } + }else{ + moveLock[options.ke + options.id] = false + s.userLog(monitorConfig,response); + } + resolve(response) + }); + theRequest.catch((err) => { + response.ok = false + response.type = lang['Control Error'] + response.msg = err + resolve(response) + }) + }) + } + function getOnvifControlOptions(options){ + const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] + const invertedVerticalAxis = monitorConfig.details.control_invert_y === '1' + const turnSpeed = parseFloat(monitorConfig.details.control_turn_speed) || 0.1 + const controlOptions = { + Velocity : {} + } + if(options.axis){ + options.axis.forEach((axis) => { + controlOptions.Velocity[axis.direction] = axis.amount < 0 ? -turnSpeed : axis.amount > 0 ? turnSpeed : 0 + }) + }else{ + const onvifDirections = { + "left": [-turnSpeed,'x'], + "right": [turnSpeed,'x'], + "down": [invertedVerticalAxis ? turnSpeed : -turnSpeed,'y'], + "up": [invertedVerticalAxis ? -turnSpeed : turnSpeed,'y'], + "zoom_in": [turnSpeed,'z'], + "zoom_out": [-turnSpeed,'z'] + } + const direction = onvifDirections[options.direction] + controlOptions.Velocity[direction[1]] = direction[0] + } + (['x','y','z']).forEach(function(axis){ + if(!controlOptions.Velocity[axis]) + controlOptions.Velocity[axis] = 0 + }) + return controlOptions + } + async function startMoveOnvif(options,callback){ + const controlOptions = getOnvifControlOptions(options); + let activeMonitor = s.group[options.ke].activeMonitors[options.id] + let device = activeMonitor.onvifConnection + if( + !device || + !device.current_profile || + !device.current_profile.token + ){ const response = await s.createOnvifDevice({ ke: options.ke, id: options.id, }) - const device = s.group[options.ke].activeMonitors[options.id].onvifConnection + device = activeMonitor.onvifConnection } - options.controlOptions.ProfileToken = device.current_profile.token - s.runOnvifMethod({ - auth: { - ke: options.ke, - id: options.id, - action: 'continuousMove', - service: 'ptz', - }, - options: options.controlOptions, - },callback) + function returnResponse(){ + return new Promise((resolve,reject) => { + controlOptions.ProfileToken = device.current_profile.token + s.runOnvifMethod({ + auth: { + ke: options.ke, + id: options.id, + action: 'continuousMove', + service: 'ptz', + }, + options: controlOptions, + },resolve) + }) + } + return await returnResponse(); } - const stopMove = function(options,callback){ - const device = s.group[options.ke].activeMonitors[options.id].onvifConnection - try{ + function stopMoveOnvif(options){ + return new Promise((resolve,reject) => { + const device = s.group[options.ke].activeMonitors[options.id].onvifConnection + try{ + s.runOnvifMethod({ + auth: { + ke: options.ke, + id: options.id, + action: 'stop', + service: 'ptz', + }, + options: { + 'PanTilt': true, + 'Zoom': true, + ProfileToken: device.current_profile.token + }, + },resolve) + }catch(err){ + resolve({ok: false}) + } + }) + } + function relativeMoveOnvif(options){ + return new Promise((resolve,reject) => { + const controlOptions = getOnvifControlOptions(options); + controlOptions.Speed = {'x': 1, 'y': 1, 'z': 1} + controlOptions.Translation = Object.assign(controlOptions.Velocity,{}) + delete(controlOptions.Velocity) + moveLock[options.ke + options.id] = true s.runOnvifMethod({ auth: { ke: options.ke, id: options.id, - action: 'stop', + action: 'relativeMove', service: 'ptz', }, - options: { - 'PanTilt': true, - 'Zoom': true, - ProfileToken: device.current_profile.token - }, - },callback) - }catch(err){ - callback({ok: false}) - } - } - const moveOnvifCamera = function(options,callback){ - const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] - const invertedVerticalAxis = monitorConfig.details.control_invert_y === '1' - const turnSpeed = parseFloat(monitorConfig.details.control_turn_speed) || 0.1 - const controlUrlStopTimeout = parseInt(monitorConfig.details.control_url_stop_timeout) || 1000 - switch(options.direction){ - case'center': - moveLock[options.ke + options.id] = true - moveToPresetPosition({ - ke: options.ke, - id: options.id, - },(endData) => { - moveLock[options.ke + options.id] = false - callback({type:'Moving to Home Preset', response: endData}) - }) - break; - case'stopMove': - callback({type:'Control Trigger Ended'}) - stopMove({ - ke: options.ke, - id: options.id, - },(response) => { - moveLock[options.ke + options.id] = false - }) - break; - default: - try{ - var controlOptions = { - Velocity : {} - } - if(options.axis){ - options.axis.forEach((axis) => { - controlOptions.Velocity[axis.direction] = axis.amount < 0 ? -turnSpeed : axis.amount > 0 ? turnSpeed : 0 - }) + options: controlOptions, + },(response) => { + if(response.ok){ + resolve({type: 'Control Triggered'}) }else{ - var onvifDirections = { - "left": [-turnSpeed,'x'], - "right": [turnSpeed,'x'], - "down": [invertedVerticalAxis ? turnSpeed : -turnSpeed,'y'], - "up": [invertedVerticalAxis ? -turnSpeed : turnSpeed,'y'], - "zoom_in": [turnSpeed,'z'], - "zoom_out": [-turnSpeed,'z'] - } - var direction = onvifDirections[options.direction] - controlOptions.Velocity[direction[1]] = direction[0] + resolve({type: 'Control Triggered', error: response.error}) } - (['x','y','z']).forEach(function(axis){ - if(!controlOptions.Velocity[axis]) - controlOptions.Velocity[axis] = 0 - }) - if(monitorConfig.details.control_stop === '1'){ + moveLock[options.ke + options.id] = false + }) + }) + } + function moveOnvifCamera(options,doMove){ + return new Promise((resolve,reject) => { + const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] + const controlUrlStopTimeout = parseInt(monitorConfig.details.control_url_stop_timeout) || 1000 + options.direction = doMove ? options.direction : 'stopMove'; + switch(options.direction){ + case'center': moveLock[options.ke + options.id] = true - startMove({ + moveToPresetPosition({ ke: options.ke, id: options.id, - controlOptions: controlOptions - },(response) => { - if(response.ok){ - if(controlUrlStopTimeout != '0'){ - setTimeout(function(){ - stopMove({ - ke: options.ke, - id: options.id, - },(response) => { - if(!response.ok){ - s.systemLog(response) - } - moveLock[options.ke + options.id] = false - }) - callback({type: 'Control Triggered'}) - },controlUrlStopTimeout) - } - }else{ - s.debugLog(response) - } + },(endData) => { + moveLock[options.ke + options.id] = false + resolve({ type: lang['Moving to Home Preset'], response: endData }) }) - }else{ - controlOptions.Speed = {'x': 1, 'y': 1, 'z': 1} - controlOptions.Translation = Object.assign(controlOptions.Velocity,{}) - delete(controlOptions.Velocity) - moveLock[options.ke + options.id] = true - s.runOnvifMethod({ - auth: { - ke: options.ke, - id: options.id, - action: 'relativeMove', - service: 'ptz', - }, - options: controlOptions, - },(response) => { - if(response.ok){ - callback({type: 'Control Triggered'}) - }else{ - callback({type: 'Control Triggered', error: response.error}) - } + break; + case'stopMove': + resolve({ type: lang['Control Trigger Ended'] }) + stopMoveOnvif({ + ke: options.ke, + id: options.id, + }).then((response) => { moveLock[options.ke + options.id] = false }) - } - }catch(err){ - console.log(err) - console.log(new Error()) + break; + default: + try{ + moveLock[options.ke + options.id] = true + startMoveOnvif(options).then((moveResponse) => { + if(!moveResponse.ok){ + s.debugLog('ONVIF Move Error',moveResponse) + } + resolve(moveResponse) + }) + }catch(err){ + console.log(err) + console.log(new Error()) + } + break; } - break; + }) + } + async function startMove(options){ + const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] + const controlUrlMethod = monitorConfig.details.control_url_method || 'GET' + if(controlUrlMethod === 'ONVIF'){ + return await moveOnvifCamera(options,true); + }else{ + return await moveGeneric(options,true); } } - const ptzControl = async function(options,callback){ + async function stopMove(options){ + const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] + const controlUrlMethod = monitorConfig.details.control_url_method || 'GET' + if(controlUrlMethod === 'ONVIF'){ + return await moveOnvifCamera(options,false); + }else{ + return await moveGeneric(options,false); + } + } + async function ptzControl(options,callback){ if(!s.group[options.ke] || !s.group[options.ke].activeMonitors[options.id]){return} const monitorConfig = s.group[options.ke].rawMonitorConfigurations[options.id] const controlUrlMethod = monitorConfig.details.control_url_method || 'GET' - const controlBaseUrl = monitorConfig.details.control_base_url || s.buildMonitorUrl(monitorConfig, true) + const controlUrlStopTimeout = options.moveTimeout || parseInt(monitorConfig.details.control_url_stop_timeout) || 1000 + const stopCommandEnabled = monitorConfig.details.control_stop === '1' || monitorConfig.details.control_stop === '2'; if(monitorConfig.details.control !== "1"){ - s.userLog(monitorConfig,{type:lang['Control Error'],msg:lang.ControlErrorText1}); - return + s.userLog(monitorConfig,{ + type: lang['Control Error'], + msg: lang.ControlErrorText1 + }); + return { + ok: false, + msg: lang.ControlErrorText1 + } } - if(monitorConfig.details.control_url_stop_timeout === '0' && monitorConfig.details.control_stop === '1' && s.group[options.ke].activeMonitors[options.id].ptzMoving === true){ - options.direction = 'stopMove' - s.group[options.ke].activeMonitors[options.id].ptzMoving = false - }else{ - s.group[options.ke].activeMonitors[options.id].ptzMoving = true + let response = { + direction: options.direction, } if(controlUrlMethod === 'ONVIF'){ - try{ - //create onvif connection - if( - !s.group[options.ke].activeMonitors[options.id].onvifConnection || - !s.group[options.ke].activeMonitors[options.id].onvifConnection.current_profile || - !s.group[options.ke].activeMonitors[options.id].onvifConnection.current_profile.token - ){ - const response = await s.createOnvifDevice({ - ke: options.ke, - id: options.id, - }) - if(response.ok){ - moveOnvifCamera({ - ke: options.ke, - id: options.id, - direction: options.direction, - axis: options.axis, - },(msg) => { - msg.msg = options.direction - callback(msg) - }) - }else{ - s.userLog(options,{type:lang['Control Error'],msg:response.error}) - } + if(options.direction === 'center'){ + response.moveResponse = await moveOnvifCamera(options,true) + }else if(stopCommandEnabled){ + response.moveResponse = await moveOnvifCamera(options,true) + if(options.direction !== 'stopMove' && options.direction !== 'center'){ + await asyncSetTimeout(controlUrlStopTimeout) + response.stopMoveResponse = await moveOnvifCamera(options,false) + response.ok = response.moveResponse.ok && response.stopMoveResponse.ok; }else{ - moveOnvifCamera({ - ke: options.ke, - id: options.id, - direction: options.direction, - axis: options.axis, - },(msg) => { - if(!msg.msg)msg.msg = {direction: options.direction} - callback(msg) - }) + response.ok = response.moveResponse.ok; } - }catch(err){ - s.debugLog(err) - callback({ - type: lang['Control Error'], - msg: { - msg: lang.ControlErrorText2, - error: err, - direction: options.direction - } - }) + }else{ + response = await relativeMoveOnvif(options); } }else{ - const controlUrlStopTimeout = parseInt(monitorConfig.details.control_url_stop_timeout) || 1000 - var stopCamera = function(){ - let stopURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}_stop`] - let controlOptions = s.cameraControlOptionsFromUrl(stopURL,monitorConfig) - let requestOptions = { - url : controlBaseUrl + controlOptions.path, - method : controlOptions.method - } - if(controlOptions.username && controlOptions.password){ - requestOptions.auth = { - user: controlOptions.username, - pass: controlOptions.password - } - } - if(controlOptions.postData){ - requestOptions.form = controlOptions.postData - } - if(monitorConfig.details.control_digest_auth === '1'){ - requestOptions.uri = sliceUrlAuth(requestOptions.url); - delete requestOptions.url; - requestOptions.auth.sendImmediately = false; - } - request(requestOptions,function(err,data){ - const msg = { - ok: true, - type:'Control Trigger Ended' - } - if(err){ - msg.ok = false - msg.type = 'Control Error' - msg.msg = err - } - moveLock[options.ke + options.id] = false - callback(msg) - s.userLog(monitorConfig,msg); - }) - } if(options.direction === 'stopMove'){ - stopCamera() + response = await moveGeneric(options,false) }else{ - let controlURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}`] - let controlOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig) - let requestOptions = { - url: controlBaseUrl + controlOptions.path, - method: controlOptions.method + // left, right, up, down, center + response.moveResponse = await moveGeneric(options,true) + if(stopCommandEnabled){ + await asyncSetTimeout(controlUrlStopTimeout) + response.stopMoveResponse = await moveGeneric(options,false) + response.ok = response.moveResponse.ok && response.stopMoveResponse.ok; + }else{ + response.ok = response.moveResponse.ok; } - if(controlOptions.username && controlOptions.password){ - requestOptions.auth = { - user: controlOptions.username, - pass: controlOptions.password - } - } - if(controlOptions.postData){ - requestOptions.form = controlOptions.postData - } - if(monitorConfig.details.control_digest_auth === '1'){ - requestOptions.uri = sliceUrlAuth(requestOptions.url); - delete requestOptions.url; - requestOptions.auth.sendImmediately = false; - } - moveLock[options.ke + options.id] = true - request(requestOptions,function(err,data){ - if(err){ - callback({ok:false,type:'Control Error',msg:err}) - return - } - if(monitorConfig.details.control_stop == '1' && options.direction !== 'center' ){ - s.userLog(monitorConfig,{type:'Control Triggered Started'}); - if(controlUrlStopTimeout > 0){ - setTimeout(function(){ - stopCamera() - },controlUrlStopTimeout) - } - }else{ - moveLock[options.ke + options.id] = false - callback({ok:true,type:'Control Triggered'}) - } - }) } } + if(callback)callback(response); + return response; } const getPresetPositions = (options,callback) => { const profileToken = options.ProfileToken || "__CURRENT_TOKEN" @@ -313,7 +312,6 @@ module.exports = function(s,config,lang){ const setPresetForCurrentPosition = (options,callback) => { const nonStandardOnvif = s.group[options.ke].rawMonitorConfigurations[options.id].details.onvif_non_standard === '1' const profileToken = options.ProfileToken || "__CURRENT_TOKEN" - console.log(options.PresetToken) s.runOnvifMethod({ auth: { ke: options.ke, @@ -380,7 +378,6 @@ module.exports = function(s,config,lang){ const imageCenterY = imgHeight / 2 const matrices = event.details.matrices || [] const largestMatrix = getLargestMatrix(matrices.filter(matrix => trackingTarget.indexOf(matrix.tag) > -1)) - // console.log(matrices.find(matrix => matrix.tag === 'person')) if(!largestMatrix)return; const monitorConfig = s.group[event.ke].rawMonitorConfigurations[event.id] const invertedVerticalAxis = monitorConfig.details.control_invert_y === '1' @@ -389,6 +386,10 @@ module.exports = function(s,config,lang){ const matrixCenterY = largestMatrix.y + (largestMatrix.height / 2) const rawDistanceX = (matrixCenterX - imageCenterX) const rawDistanceY = (matrixCenterY - imageCenterY) + const percentX = parseFloat((rawDistanceX / imgWidth).toFixed(2)); + const percentY = parseFloat((rawDistanceY / imgHeight).toFixed(2)); + const turnSpeedX = parseFloat(monitorConfig.details.control_turn_speed) || 0.1 + const turnSpeedY = parseFloat(monitorConfig.details.control_turn_speed) || 0.1 const distanceX = imgWidth / rawDistanceX const distanceY = imgHeight / rawDistanceY const axisX = rawDistanceX > thresholdX || rawDistanceX < -thresholdX ? distanceX : 0 @@ -396,11 +397,11 @@ module.exports = function(s,config,lang){ if(axisX !== 0 || axisY !== 0){ ptzControl({ axis: [ - {direction: 'x', amount: axisX === 0 ? 0 : axisX > 0 ? turnSpeed : -turnSpeed}, - {direction: 'y', amount: axisY === 0 ? 0 : axisY > 0 ? invertedVerticalAxis ? -turnSpeed : turnSpeed : invertedVerticalAxis ? turnSpeed : -turnSpeed}, + {direction: 'x', amount: axisX === 0 ? 0 : axisX > 0 ? turnSpeedX : -turnSpeedX}, + {direction: 'y', amount: axisY === 0 ? 0 : axisY > 0 ? invertedVerticalAxis ? -turnSpeedY : turnSpeedY : invertedVerticalAxis ? turnSpeedY : -turnSpeedY}, {direction: 'z', amount: 0}, ], - // axis: [{direction: 'x', amount: 1.0}], + moveTimeout: 500, id: event.id, ke: event.ke },(msg) => { @@ -413,12 +414,12 @@ module.exports = function(s,config,lang){ } } return { - ptzControl: ptzControl, - startMove: startMove, - stopMove: stopMove, - getPresetPositions: getPresetPositions, - setPresetForCurrentPosition: setPresetForCurrentPosition, - moveToPresetPosition: moveToPresetPosition, - moveCameraPtzToMatrix: moveCameraPtzToMatrix + startMove, + stopMove, + ptzControl, + getPresetPositions, + setPresetForCurrentPosition, + moveToPresetPosition, + moveCameraPtzToMatrix, } } diff --git a/libs/control/zwave.js b/libs/control/zwave.js new file mode 100644 index 00000000..9a84b5c5 --- /dev/null +++ b/libs/control/zwave.js @@ -0,0 +1,134 @@ +const zWaveAPI = require('shinobi-zwave') +module.exports = async (s,config,lang,app,io) => { + const addCredentialsToHostLink = (url,username,password) => { + if(url.indexOf('@') > -1){ + return url + }else if(username){ + const urlParts = url.split('://') + return [urlParts[0],'://',`${username}:${password || ''}@`,urlParts[1]].join('') + }else{ + return url + } + } + function loadApplicationForGroup(user){ + const userDetails = s.parseJSON(user.details); + s.debugLog('Z-Wave','Loading API',user.ke) + if( + !s.group[user.ke].zwave && + userDetails.zwave === '1' && + userDetails.zwave_host + ){ + const zWaveHost = addCredentialsToHostLink( + userDetails.zwave_host || config.zWaveHost, + userDetails.zwave_user, + userDetails.zwave_pass + ).replace(/\/$/, ''); + s.group[user.ke].zwave = zWaveAPI(zWaveHost,config.debugLogZwave || false) + s.debugLog('Z-Wave','Loaded API',zWaveHost) + } + } + function unloadApplicationForGroup(user){ + s.group[user.ke].zwave = null + } + /** + * API : Z-Wave HTTP Handler + */ + function httpApiHandler(req,res){ + s.auth(req.params,async (user) => { + const endData = {ok: true} + const actionFunction = req.params.action + const arguments = s.getPostData(req) || [] + try{ + const groupKey = req.params.ke + endData.response = await s.group[groupKey].zwave[actionFunction](...arguments) + }catch(err){ + endData.ok = false + endData.err = err + s.debugLog(err) + } + s.closeJsonResponse(res,endData) + },res,req); + } + app.get(config.webPaths.apiPrefix+':auth/zwave/:ke/:action',httpApiHandler) + app.post(config.webPaths.apiPrefix+':auth/zwave/:ke/:action',httpApiHandler) + app.get(config.webPaths.apiPrefix+':auth/zwaveRaw/:ke',(req,res) => { + s.auth(req.params,async (user) => { + const groupKey = req.params.ke + const pathString = s.getPostData(req,'path') + if(!pathString){ + res.end(lang['Not Found']) + return + } + s.group[groupKey].zwave.httpRequest(pathString).pipe(res) + },res,req); + }) + s.definitions["Account Settings"].blocks["Z-Wave"] = { + "evaluation": "$user.details.use_zwave !== '0'", + "name": lang['Z-Wave'], + "id":"accSectionZwave", + "isSection": true, + "color": "blue", + "info": [ + { + "name": "detail=zwave", + "selector":"u_zwave_bot", + "field": lang.Enabled, + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + hidden: true, + "name": "detail=zwave_host", + "placeholder": "https://localhost:8083", + "field": lang.Host, + "form-group-class":"u_zwave_bot_input u_zwave_bot_1", + }, + { + hidden: true, + "name": "detail=zwave_user", + "placeholder": lang["Username"], + "field": lang["Username"], + "form-group-class":"u_zwave_bot_input u_zwave_bot_1", + }, + { + hidden: true, + "name": "detail=zwave_pass", + "placeholder": lang["Password"], + "fieldType": "password", + "field": lang["Password"], + "form-group-class":"u_zwave_bot_input u_zwave_bot_1", + } + ] + } + s.definitions["Z-Wave Manager"] = { + "name": lang["Z-Wave Manager"], + blocks: { + "Container1": { + "evaluation": "$user.details.use_zwave !== '0'", + noHeader: true, + noDefaultSectionClasses: true, + "color": "green", + "section-pre-class": "col-md-8 search-parent", + "info": [ + { + "id": "zwaveDevices", + "fieldType": "div", + }, + ] + } + } + } + s.loadGroupAppExtender(loadApplicationForGroup) + s.unloadGroupAppExtender(unloadApplicationForGroup) +} diff --git a/libs/cron.js b/libs/cron.js new file mode 100644 index 00000000..b6b60b4a --- /dev/null +++ b/libs/cron.js @@ -0,0 +1,63 @@ +const { Worker } = require('worker_threads'); +const moment = require('moment'); +module.exports = (s,config,lang) => { + const { + legacyFilterEvents + } = require('./events/utils.js')(s,config,lang) + if(config.doCronAsWorker===undefined)config.doCronAsWorker = true; + const startWorker = () => { + const pathToWorkerScript = __dirname + `/cron/worker.js` + const workerProcess = new Worker(pathToWorkerScript) + workerProcess.on('message',function(data){ + if(data.time === 'moment()')data.time = moment(); + switch(data.f){ + case'debugLog': + s.debugLog(...data.data) + break; + case'systemLog': + s.systemLog(...data.data) + break; + case'filters': + legacyFilterEvents(data.ff,data) + break; + case's.tx': + s.tx(data.data,data.to) + break; + case's.deleteVideo': + s.deleteVideo(data.file) + break; + case's.deleteFileBinEntry': + s.deleteFileBinEntry(data.file) + break; + case's.setDiskUsedForGroup': + function doOnMain(){ + s.setDiskUsedForGroup(data.ke,data.size,data.target || undefined) + } + if(data.videoRow){ + let storageIndex = s.getVideoStorageIndex(data.videoRow); + if(storageIndex){ + s.setDiskUsedForGroupAddStorage(data.ke,{ + size: data.size, + storageIndex: storageIndex + }) + }else{ + doOnMain() + } + }else{ + doOnMain() + } + break; + default: + s.systemLog('CRON.js MESSAGE : ',data) + break; + } + }) + setTimeout(() => { + workerProcess.postMessage({ + f: 'init', + }) + },2000) + return workerProcess + } + if(config.doCronAsWorker === true)startWorker() +} diff --git a/libs/cron/worker.js b/libs/cron/worker.js new file mode 100644 index 00000000..e7a08a58 --- /dev/null +++ b/libs/cron/worker.js @@ -0,0 +1,612 @@ +const fs = require('fs'); +const path = require('path'); +const moment = require('moment'); +const exec = require('child_process').exec; +const spawn = require('child_process').spawn; +const { parentPort, isMainThread } = require('worker_threads'); +const config = require(process.cwd() + '/conf.json') +process.on('uncaughtException', function (err) { + errorLog('uncaughtException',err); +}); +if(isMainThread){ + console.log(`Shinobi now runs cron.js as child process.`) + console.error(`Shinobi now runs cron.js as child process.`) + setInterval(() => { + // console.log(`Please turn off cron.js process.`) + },1000 * 60 * 60 * 24 * 7) + return; +} +function setDefaultConfigOptions(){ + if(config.cron===undefined)config.cron={}; + if(config.cron.deleteOld===undefined)config.cron.deleteOld=true; + if(config.cron.deleteOrphans===undefined)config.cron.deleteOrphans=false; + if(config.cron.deleteNoVideo===undefined)config.cron.deleteNoVideo=true; + if(config.cron.deleteNoVideoRecursion===undefined)config.cron.deleteNoVideoRecursion=false; + if(config.cron.deleteOverMax===undefined)config.cron.deleteOverMax=true; + if(config.cron.deleteLogs===undefined)config.cron.deleteLogs=true; + if(config.cron.deleteTimelpaseFrames===undefined)config.cron.deleteTimelpaseFrames=true; + if(config.cron.deleteEvents===undefined)config.cron.deleteEvents=true; + if(config.cron.deleteFileBins===undefined)config.cron.deleteFileBins=true; + if(config.cron.interval===undefined)config.cron.interval=1; + if(config.databaseType===undefined){config.databaseType='mysql'} + if(config.databaseLogs===undefined){config.databaseLogs=false} + if(config.debugLog===undefined){config.debugLog=false} + + if(!config.ip||config.ip===''||config.ip.indexOf('0.0.0.0')>-1)config.ip='localhost'; + if(!config.videosDir)config.videosDir = process.cwd() + '/videos/'; + if(!config.binDir){config.binDir = process.cwd() + '/fileBin/'} +} +parentPort.on('message',(data) => { + switch(data.f){ + case'init': + setDefaultConfigOptions() + beginProcessing() + break; + case'start':case'restart': + setIntervalForCron() + break; + case'stop': + clearCronInterval() + break; + } +}) +function debugLog(...args){ + if(config.debugLog === true){ + console.log(...([`CRON.js DEBUG LOG ${new Date()}`].concat(args))) + } +} +function normalLog(...args){ + console.log(...([`CRON.js LOG ${new Date()}`].concat(args))) +} +function errorLog(...args){ + console.error(...([`CRON.js ERROR LOG ${new Date()}`].concat(args))) +} +const s = { + debugLog, +} +function beginProcessing(){ + normalLog(`Worker Processing!`) + const { + checkCorrectPathEnding, + generateRandomId, + formattedTime, + localToUtc, + } = require('../basic/utils.js')(process.cwd()) + const { + sqlDate, + knexQuery, + knexQueryPromise, + initiateDatabaseEngine + } = require('../sql/utils.js')(s,config) + var theCronInterval = null + const overlapLocks = {} + const alreadyDeletedRowsWithNoVideosOnStart = {} + const videoDirectory = checkCorrectPathEnding(config.videosDir) + const fileBinDirectory = checkCorrectPathEnding(config.binDir) + const postMessage = (data) => { + parentPort.postMessage(data) + } + const sendToWebSocket = (x,y) => { + //emulate master socket emitter + postMessage({f:'s.tx',data:x,to:y}) + } + const deleteVideo = (x) => { + postMessage({f:'s.deleteVideo',file:x}) + } + const deleteFileBinEntry = (x) => { + postMessage({f:'s.deleteFileBinEntry',file:x}) + } + const setDiskUsedForGroup = (groupKey,size,target,videoRow) => { + postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target, videoRow: videoRow}) + } + const getVideoDirectory = function(e){ + if(e.mid&&!e.id){e.id=e.mid}; + if(e.details&&(e.details instanceof Object)===false){ + try{e.details=JSON.parse(e.details)}catch(err){} + } + if(e.details.dir&&e.details.dir!==''){ + return checkCorrectPathEnding(e.details.dir)+e.ke+'/'+e.id+'/' + }else{ + return videoDirectory + e.ke + '/' + e.id + '/' + } + } + const getTimelapseFrameDirectory = function(e){ + if(e.mid&&!e.id){e.id=e.mid} + if(e.details&&(e.details instanceof Object)===false){ + try{e.details=JSON.parse(e.details)}catch(err){} + } + if(e.details&&e.details.dir&&e.details.dir!==''){ + return checkCorrectPathEnding(e.details.dir)+e.ke+'/'+e.id+'_timelapse/' + }else{ + return videoDirectory + e.ke + '/' + e.id + '_timelapse/' + } + } + const getFileBinDirectory = function(e){ + if(e.mid && !e.id){e.id = e.mid} + return fileBinDirectory + e.ke + '/' + e.id + '/' + } + //filters set by the user in their dashboard + //deleting old videos is part of the filter - config.cron.deleteOld + const checkFilterRules = function(v){ + return new Promise((resolve,reject) => { + //filters + v.d.filters = v.d.filters ? v.d.filters : {} + debugLog('Checking Basic Filters...') + var keys = Object.keys(v.d.filters) + if(keys.length>0){ + keys.forEach(function(m,current){ + // b = filter + var b = v.d.filters[m]; + debugLog(b) + if(b.enabled==="1"){ + const whereQuery = [ + ['ke','=',v.ke], + ['status','!=',"0"], + ['archive','!=',`1`], + ] + b.where.forEach(function(condition){ + if(condition.p1 === 'ke'){condition.p3 = v.ke} + whereQuery.push([condition.p1,condition.p2 || '=',condition.p3]) + }) + knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: whereQuery, + orderBy: [b.sort_by,b.sort_by_direction.toLowerCase()], + limit: b.limit + },(err,r) => { + if(r && r[0]){ + if(r.length > 0 || config.debugLog === true){ + postMessage({f:'filterMatch',msg:r.length+' SQL rows match "'+m+'"',ke:v.ke,time:'moment()'}) + } + b.cx={ + f:'filters', + name:b.name, + videos:r, + time:'moment()', + ke:v.ke, + id:b.id + }; + if(b.archive==="1"){ + postMessage({f:'filters',ff:'archive',videos:r,time:'moment()',ke:v.ke,id:b.id}); + }else if(b.delete==="1"){ + postMessage({f:'filters',ff:'delete',videos:r,time:'moment()',ke:v.ke,id:b.id}); + } + if(b.email==="1"){ + b.cx.ff='email'; + b.cx.delete=b.delete; + b.cx.mail=v.mail; + b.cx.execute=b.execute; + b.cx.query=b.sql; + postMessage(b.cx); + } + if(b.execute&&b.execute!==""){ + postMessage({f:'filters',ff:'execute',execute:b.execute,time:'moment()'}); + } + } + }) + + } + if(current===keys.length-1){ + //last filter + resolve() + } + }) + }else{ + //no filters + resolve() + } + }) + } + const deleteVideosByDays = async (v,days,addedQueries) => { + const groupKey = v.ke; + const whereQuery = [ + ['ke','=',v.ke], + ['archive','!=',`1`], + ['time','<', sqlDate(days+' DAY')], + addedQueries + ] + const selectResponse = await knexQueryPromise({ + action: "select", + columns: "*", + table: "Videos", + where: whereQuery + }) + const videoRows = selectResponse.rows + let affectedRows = 0 + if(videoRows.length > 0){ + let clearSize = 0; + var i; + for (i = 0; i < videoRows.length; i++) { + const row = videoRows[i]; + const dir = getVideoDirectory(row) + const filename = formattedTime(row.time) + '.' + row.ext + try{ + await fs.promises.unlink(dir + filename) + const fileSizeMB = row.size / 1048576; + setDiskUsedForGroup(groupKey,-fileSizeMB,null,row) + sendToWebSocket({ + f: 'video_delete', + filename: filename + '.' + row.ext, + mid: row.mid, + ke: row.ke, + time: row.time, + end: formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') + },'GRP_' + row.ke) + }catch(err){ + normalLog('Video Delete Error',row) + normalLog(err) + } + } + const deleteResponse = await knexQueryPromise({ + action: "delete", + table: "Videos", + where: whereQuery + }) + affectedRows = deleteResponse.rows || 0 + } + return { + ok: true, + affectedRows: affectedRows, + } + } + const deleteOldVideos = async (v) => { + // v = group, admin user + if(config.cron.deleteOld === true){ + const daysOldForDeletion = v.d.days && !isNaN(v.d.days) ? parseFloat(v.d.days) : 5 + const monitorsIgnored = [] + const monitorsResponse = await knexQueryPromise({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',v.ke], + ] + }) + const monitorRows = monitorsResponse.rows + var i; + for (i = 0; i < monitorRows.length; i++) { + const monitor = monitorRows[i] + const monitorId = monitor.mid + const details = JSON.parse(monitor.details); + const monitorsMaxDaysToKeep = !isNaN(details.max_keep_days) ? parseFloat(details.max_keep_days) : null + if(monitorsMaxDaysToKeep){ + const { affectedRows } = await deleteVideosByDays(v,monitorsMaxDaysToKeep,['mid','=',monitorId]) + const hasDeletedRows = affectedRows && affectedRows.length > 0; + if(hasDeletedRows || config.debugLog === true){ + postMessage({ + f: 'deleteOldVideosByMonitorId', + msg: `${affectedRows} SQL rows older than ${monitorsMaxDaysToKeep} days deleted`, + ke: v.ke, + mid: monitorId, + time: 'moment()', + }) + } + monitorsIgnored.push(['mid','!=',monitorId]) + } + } + const { affectedRows } = await deleteVideosByDays(v,daysOldForDeletion,monitorsIgnored) + const hasDeletedRows = affectedRows && affectedRows.length > 0; + if(hasDeletedRows || config.debugLog === true){ + postMessage({ + f: 'deleteOldVideos', + msg: `${affectedRows} SQL rows older than ${daysOldForDeletion} days deleted`, + ke: v.ke, + time: 'moment()', + }) + } + } + } + //database rows with no videos in the filesystem + const deleteRowsWithNoVideo = function(v){ + return new Promise((resolve,reject) => { + if( + config.cron.deleteNoVideo===true&&( + config.cron.deleteNoVideoRecursion===true|| + (config.cron.deleteNoVideoRecursion===false&&!alreadyDeletedRowsWithNoVideosOnStart[v.ke]) + ) + ){ + alreadyDeletedRowsWithNoVideosOnStart[v.ke]=true; + knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: [ + ['ke','=',v.ke], + ['status','!=','0'], + ['archive','!=',`1`], + ['time','<', sqlDate('10 MINUTE')], + ] + },(err,evs) => { + if(evs && evs[0]){ + const videosToDelete = []; + evs.forEach(function(ev){ + var filename + var details + try{ + details = JSON.parse(ev.details) + }catch(err){ + if(details instanceof Object){ + details = ev.details + }else{ + details = {} + } + } + var dir = getVideoDirectory(ev) + filename = formattedTime(ev.time)+'.'+ev.ext + fileExists = fs.existsSync(dir+filename) + if(fileExists !== true){ + deleteVideo(ev) + sendToWebSocket({f:'video_delete',filename:filename+'.'+ev.ext,mid:ev.mid,ke:ev.ke,time:ev.time,end: formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')},'GRP_'+ev.ke); + } + }); + if(videosToDelete.length > 0 || config.debugLog === true){ + postMessage({f:'deleteNoVideo',msg:videosToDelete.length+' SQL rows with no file deleted',ke:v.ke,time:'moment()'}) + } + } + setTimeout(function(){ + resolve() + },3000) + }) + }else{ + resolve() + } + }) + } + //info about what the application is doing + const deleteOldLogs = function(v){ + return new Promise((resolve,reject) => { + const daysOldForDeletion = v.d.log_days && !isNaN(v.d.log_days) ? parseFloat(v.d.log_days) : 10 + if(config.cron.deleteLogs === true && daysOldForDeletion !== 0){ + knexQuery({ + action: "delete", + table: "Logs", + where: [ + ['ke','=',v.ke], + ['time','<', sqlDate(daysOldForDeletion + ' DAY')], + ] + },(err,rrr) => { + resolve() + if(err)return errorLog(err); + if(rrr && rrr > 0 || config.debugLog === true){ + postMessage({f:'deleteLogs',msg: rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:'moment()'}) + } + }) + }else{ + resolve() + } + }) + } + //still images saved + const deleteOldTimelapseFrames = async function(v){ + const daysOldForDeletion = v.d.timelapseFrames_days && !isNaN(v.d.timelapseFrames_days) ? parseFloat(v.d.timelapseFrames_days) : 60 + if(config.cron.deleteTimelpaseFrames === true && daysOldForDeletion !== 0){ + const groupKey = v.ke; + const whereQuery = [ + ['ke','=',v.ke], + ['archive','!=',`1`], + ['time','<', sqlDate(daysOldForDeletion+' DAY')], + ] + const selectResponse = await knexQueryPromise({ + action: "select", + columns: "*", + table: "Timelapse Frames", + where: whereQuery + }) + const videoRows = selectResponse.rows + let affectedRows = 0 + if(videoRows.length > 0){ + const foldersDeletedFrom = []; + let clearSize = 0; + var i; + for (i = 0; i < videoRows.length; i++) { + const row = videoRows[i]; + const dir = getTimelapseFrameDirectory(row) + const filename = row.filename + const theDate = filename.split('T')[0] + const enclosingFolder = `${dir}/${theDate}/` + try{ + const fileSizeMB = row.size / 1048576; + setDiskUsedForGroup(groupKey,-fileSizeMB,null,row) + sendToWebSocket({ + f: 'timelapse_frame_delete', + filename: filename, + mid: row.mid, + ke: groupKey, + time: row.time, + details: row.details, + },'GRP_' + groupKey) + await fs.promises.unlink(`${enclosingFolder}${filename}`) + if(foldersDeletedFrom.indexOf(enclosingFolder) === -1)foldersDeletedFrom.push(enclosingFolder); + }catch(err){ + normalLog('Timelapse Frame Delete Error',row) + normalLog(err) + } + } + for (i = 0; i < foldersDeletedFrom.length; i++) { + const folderPath = foldersDeletedFrom[i]; + const folderIsEmpty = (await fs.promises.readdir(folderPath)).filter(file => file.indexOf('.jpg') > -1).length === 0; + if(folderIsEmpty){ + await fs.promises.rm(folderPath, { recursive: true, force: true }) + } + } + const deleteResponse = await knexQueryPromise({ + action: "delete", + table: "Timelapse Frames", + where: whereQuery + }) + affectedRows = deleteResponse.rows || 0 + } + return { + ok: true, + affectedRows: affectedRows, + } + } + return { + ok: false + } + } + //events - motion, object, etc. detections + const deleteOldEvents = function(v){ + return new Promise((resolve,reject) => { + const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10 + if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){ + knexQuery({ + action: "delete", + table: "Events", + where: [ + ['ke','=',v.ke], + ['archive','!=',`1`], + ['time','<', sqlDate(daysOldForDeletion + ' DAY')], + ] + },(err,rrr) => { + resolve() + if(err)return errorLog(err); + if(rrr && rrr > 0 || config.debugLog === true){ + postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:'moment()'}) + } + }) + }else{ + resolve() + } + }) + } + //event counts + const deleteOldEventCounts = function(v){ + return new Promise((resolve,reject) => { + const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10 + if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){ + knexQuery({ + action: "delete", + table: "Events Counts", + where: [ + ['ke','=',v.ke], + ['time','<', sqlDate(daysOldForDeletion + ' DAY')], + ] + },(err,rrr) => { + resolve() + if(err && err.code !== 'ER_NO_SUCH_TABLE')return errorLog(err); + if(rrr && rrr > 0 || config.debugLog === true){ + postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:'moment()'}) + } + }) + }else{ + resolve() + } + }) + } + //check for temporary files (special archive) + const deleteOldFileBins = function(v){ + return new Promise((resolve,reject) => { + const daysOldForDeletion = v.d.fileBin_days && !isNaN(v.d.fileBin_days) ? parseFloat(v.d.fileBin_days) : 10 + if(config.cron.deleteFileBins === true && daysOldForDeletion !== 0){ + var fileBinQuery = " FROM Files WHERE ke=? AND `time` < ?"; + knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: [ + ['ke','=',v.ke], + ['archive','!=',`1`], + ['time','<', sqlDate(daysOldForDeletion + ' DAY')], + ] + },(err,files) => { + if(files && files[0]){ + //delete the files + files.forEach(function(file){ + deleteFileBinEntry(file) + }) + if(config.debugLog === true){ + postMessage({ + f: 'deleteFileBins', + msg: files.length + ' files older than ' + daysOldForDeletion + ' days deleted', + ke: v.ke, + time: 'moment()' + }) + } + } + resolve() + }) + }else{ + resolve() + } + }) + } + //user processing function + const processUser = async (v) => { + if(!v){ + //no user object given, end of group list + return + } + debugLog(`Group Key : ${v.ke}`) + debugLog(`Owner : ${v.mail}`) + if(!overlapLocks[v.ke]){ + debugLog(`Checking...`) + overlapLocks[v.ke] = true + v.d = JSON.parse(v.details); + try{ + await deleteOldVideos(v) + debugLog('--- deleteOldVideos Complete') + await deleteOldTimelapseFrames(v) + debugLog('--- deleteOldTimelapseFrames Complete') + await deleteOldLogs(v) + debugLog('--- deleteOldLogs Complete') + await deleteOldFileBins(v) + debugLog('--- deleteOldFileBins Complete') + await deleteOldEvents(v) + debugLog('--- deleteOldEvents Complete') + await deleteOldEventCounts(v) + debugLog('--- deleteOldEventCounts Complete') + await checkFilterRules(v) + debugLog('--- checkFilterRules Complete') + await deleteRowsWithNoVideo(v) + debugLog('--- deleteRowsWithNoVideo Complete') + }catch(err){ + normalLog(`Failed to Complete User : ${v.mail}`) + normalLog(err) + } + //done user, unlock current, and do next + overlapLocks[v.ke] = false; + debugLog(`Complete Checking... ${v.mail}`) + }else{ + debugLog(`Locked, Skipped...`) + } + } + //recursive function + const setIntervalForCron = function(){ + clearCronInterval() + // theCronInterval = setInterval(doCronJobs,1000 * 10) + theCronInterval = setInterval(doCronJobs,parseFloat(config.cron.interval)*60000*60) + } + const clearCronInterval = function(){ + clearInterval(theCronInterval) + } + const doCronJobs = function(){ + postMessage({ + f: 'start', + time: 'moment()' + }) + knexQuery({ + action: "select", + columns: "ke,uid,details,mail", + table: "Users", + where: [ + ['details','NOT LIKE','%"sub"%'], + ] + }, async (err,rows) => { + if(err){ + errorLog(err) + } + if(rows.length > 0){ + var i; + for (i = 0; i < rows.length; i++) { + await processUser(rows[i]) + } + } + }) + } + initiateDatabaseEngine() + setIntervalForCron() + doCronJobs() +} diff --git a/libs/customAutoLoad.js b/libs/customAutoLoad.js index 5075c22c..97026eeb 100644 --- a/libs/customAutoLoad.js +++ b/libs/customAutoLoad.js @@ -1,10 +1,10 @@ const fs = require('fs-extra'); const express = require('express') -const request = require('request') const unzipper = require('unzipper') const fetch = require("node-fetch") const spawn = require('child_process').spawn module.exports = async (s,config,lang,app,io) => { + const { fetchDownloadAndWrite } = require('./basic/utils.js')(process.cwd(),config) s.debugLog(`+++++++++++CustomAutoLoad Modules++++++++++++`) const runningInstallProcesses = {} const modulesBasePath = __dirname + '/customAutoLoad/' @@ -18,7 +18,7 @@ module.exports = async (s,config,lang,app,io) => { } const getModule = (moduleName) => { s.debugLog(`+++++++++++++++++++++++`) - s.debugLog(`Loading : ${moduleName}`) + s.debugLog(`Getting Module : ${moduleName}`) const modulePath = modulesBasePath + moduleName const stats = fs.lstatSync(modulePath) const isDirectory = stats.isDirectory() @@ -65,10 +65,9 @@ module.exports = async (s,config,lang,app,io) => { fs.mkdirSync(downloadPath) return new Promise(async (resolve, reject) => { fs.mkdir(downloadPath, () => { - request(downloadUrl).pipe(fs.createWriteStream(downloadPath + '.zip')) - .on('finish',() => { - zip = fs.createReadStream(downloadPath + '.zip') - .pipe(unzipper.Parse()) + fetchDownloadAndWrite(downloadUrl,downloadPath + '.zip', 1) + .then((readStream) => { + readStream.pipe(unzipper.Parse()) .on('entry', async (file) => { if(file.type === 'Directory'){ try{ @@ -176,7 +175,9 @@ module.exports = async (s,config,lang,app,io) => { } } const loadModule = (shinobiModule) => { + s.debugLog(`+++++++++++++++++++++++`) const moduleName = shinobiModule.name + s.debugLog(`Loading Module : ${moduleName}`) s.customAutoLoadModules[moduleName] = {} var customModulePath = modulesBasePath + '/' + moduleName s.debugLog(customModulePath) @@ -208,8 +209,13 @@ module.exports = async (s,config,lang,app,io) => { app.use('/libs',express.static(webFolder + '/libs')) } app.use(s.checkCorrectPathEnding(config.webPaths.home)+'libs',express.static(webFolder + '/libs')) - app.use(s.checkCorrectPathEnding(config.webPaths.admin)+'libs',express.static(webFolder + '/libs')) app.use(s.checkCorrectPathEnding(config.webPaths.super)+'libs',express.static(webFolder + '/libs')) + }else if(name === 'assets'){ + if(config.webPaths.home !== '/'){ + app.use('/assets',express.static(webFolder + '/assets')) + } + app.use(s.checkCorrectPathEnding(config.webPaths.home)+'assets',express.static(webFolder + '/assets')) + app.use(s.checkCorrectPathEnding(config.webPaths.super)+'assets',express.static(webFolder + '/assets')) } var libFolder = webFolder + name + '/' fs.readdir(libFolder,function(err,webFolderContents){ @@ -225,23 +231,34 @@ module.exports = async (s,config,lang,app,io) => { var fullPath = thirdLevelName + '/' + filename var blockPrefix = '' switch(true){ - case filename.contains('super.'): + case filename.indexOf('super.') > -1: blockPrefix = 'super' break; - case filename.contains('admin.'): - blockPrefix = 'admin' - break; } - switch(libName){ - case'js': - s.customAutoLoadTree[blockPrefix + 'LibsJs'].push(filename) - break; - case'css': - s.customAutoLoadTree[blockPrefix + 'LibsCss'].push(filename) - break; - case'blocks': - s.customAutoLoadTree[blockPrefix + 'PageBlocks'].push(fullPath) - break; + if(name === 'libs'){ + switch(libName){ + case'js': + s.customAutoLoadTree[blockPrefix + 'LibsJs'].push(filename) + break; + case'css': + s.customAutoLoadTree[blockPrefix + 'LibsCss'].push(filename) + break; + case'blocks': + s.customAutoLoadTree[blockPrefix + 'PageBlocks'].push(fullPath) + break; + } + }else if(name === 'assets'){ + switch(libName){ + case'js': + s.customAutoLoadTree[blockPrefix + 'AssetsJs'].push(filename) + break; + case'css': + s.customAutoLoadTree[blockPrefix + 'AssetsCss'].push(filename) + break; + case'blocks': + s.customAutoLoadTree[blockPrefix + 'PageBlocks'].push(fullPath) + break; + } } }) }) @@ -278,22 +295,24 @@ module.exports = async (s,config,lang,app,io) => { }) break; case'definitions': - var definitionsFolder = s.checkCorrectPathEnding(customModulePath) + 'definitions/' - fs.readdir(definitionsFolder,function(err,files){ - if(err)return console.log(err); - files.forEach(function(filename){ - var fileData = require(definitionsFolder + filename) - var rule = filename.replace('.json','').replace('.js','') - if(config.language === rule){ - s.definitions = s.mergeDeep(s.definitions,fileData) - } - if(s.loadedDefinitons[rule]){ - s.loadedDefinitons[rule] = s.mergeDeep(s.loadedDefinitons[rule],fileData) - }else{ - s.loadedDefinitons[rule] = s.mergeDeep(s.copySystemDefaultDefinitions(),fileData) - } - }) - }) + console.error('This Method has been deprecated. Could not load : ', customModulePath + 'defintions/') + console.error('Make your module\'s index.js file make the changes directly.') + // var definitionsFolder = s.checkCorrectPathEnding(customModulePath) + 'definitions/' + // fs.readdir(definitionsFolder,function(err,files){ + // if(err)return console.log(err); + // files.forEach(function(filename){ + // var fileData = require(definitionsFolder + filename) + // var rule = filename.replace('.json','').replace('.js','') + // if(config.language === rule){ + // s.definitions = s.mergeDeep(s.definitions,fileData) + // } + // if(s.loadedDefinitons[rule]){ + // s.loadedDefinitons[rule] = s.mergeDeep(s.loadedDefinitons[rule],fileData) + // }else{ + // s.loadedDefinitons[rule] = s.mergeDeep(s.copySystemDefaultDefinitions(),fileData) + // } + // }) + // }) break; } }) @@ -327,13 +346,14 @@ module.exports = async (s,config,lang,app,io) => { PageBlocks: [], LibsJs: [], LibsCss: [], - adminPageBlocks: [], - adminLibsJs: [], - adminLibsCss: [], + AssetsJs: [], + AssetsCss: [], superPageBlocks: [], superLibsJs: [], superRawJs: [], - superLibsCss: [] + superLibsCss: [], + superAssetsJs: [], + superAssetsCss: [] } fs.readdir(modulesBasePath,function(err,folderContents){ if(!err && folderContents.length > 0){ @@ -342,6 +362,8 @@ module.exports = async (s,config,lang,app,io) => { return; } loadModule(shinobiModule) + s.reloadLanguages() + s.reloadDefinitions() }) }else{ fs.mkdir(modulesBasePath,() => {}) diff --git a/libs/dataPort.js b/libs/dataPort.js new file mode 100644 index 00000000..a20c5143 --- /dev/null +++ b/libs/dataPort.js @@ -0,0 +1,62 @@ +const { createWebSocketServer } = require('./basic/websocketTools.js') +module.exports = function(s,config,lang,app,io){ + const { + triggerEvent, + } = require('./events/utils.js')(s,config,lang) + s.dataPortTokens = {} + const theWebSocket = createWebSocketServer() + function setClientKillTimerIfNotAuthenticatedInTime(client){ + client.killTimer = setTimeout(function(){ + client.terminate() + },10000) + } + function clearKillTimer(client){ + clearTimeout(client.killTimer) + } + theWebSocket.on('connection',(client) => { + // client.send(someDataToSendAsStringOrBinary) + setClientKillTimerIfNotAuthenticatedInTime(client) + function onAuthenticate(data){ + clearKillTimer(client) + if(data in s.dataPortTokens){ + client.removeListener('message', onAuthenticate); + client.on('message', onAuthenticatedData) + delete(s.dataPortTokens[data]); + }else{ + client.terminate() + } + } + function onAuthenticatedData(jsonData){ + const data = JSON.parse(jsonData); + switch(data.f){ + case'trigger': + triggerEvent(data) + break; + case's.tx': + s.tx(data.data,data.to) + break; + case'debugLog': + s.debugLog(data.data) + break; + default: + console.log(data) + break; + } + s.onDataPortMessageExtensions.forEach(function(extender){ + extender(data) + }) + } + client.on('message', onAuthenticate) + client.on('close', () => { + clearTimeout(client.killTimer) + }) + client.on('disconnect', () => { + clearTimeout(client.killTimer) + }) + }) + s.onHttpRequestUpgrade('/dataPort',(request, socket, head) => { + theWebSocket.handleUpgrade(request, socket, head, function done(ws) { + theWebSocket.emit('connection', ws, request) + }) + }) +} diff --git a/libs/database/utils.js b/libs/database/utils.js index 296e4407..8b5f91cd 100644 --- a/libs/database/utils.js +++ b/libs/database/utils.js @@ -87,11 +87,11 @@ module.exports = function(s,config){ const knexError = (dbQuery,options,err) => { console.error('knexError----------------------------------- START') if(config.debugLogVerbose && config.debugLog === true){ - s.debugLog('s.knexQuery QUERY',JSON.stringify(options,null,3)) s.debugLog('STACK TRACE, NOT AN ',new Error()) } console.error(err) - console.error(dbQuery.toString()) + // CANNOT USE `dbQuery.toString()` because it breaks the query + console.error(options) console.error('knexError----------------------------------- END') } const knexQuery = (options,callback) => { @@ -140,7 +140,7 @@ module.exports = function(s,config){ if(options.groupBy){ dbQuery.groupBy(options.groupBy) } - if(options.limit){ + if(options.limit && options.limit !== '0'){ if(`${options.limit}`.indexOf(',') === -1){ dbQuery.limit(options.limit) }else{ @@ -149,7 +149,8 @@ module.exports = function(s,config){ } } if(config.debugLog === true){ - console.log(dbQuery.toString()) + // CANNOT USE `dbQuery.toString()` because it breaks the query + console.log(options) } if(callback || options.update || options.insert || options.action === 'delete'){ dbQuery.asCallback(function(err,r) { @@ -177,6 +178,7 @@ module.exports = function(s,config){ ] const monitorRestrictions = options.monitorRestrictions var frameLimit = options.limit + const noLimit = options.noLimit === '1' const endIsStartTo = options.endIsStartTo const chosenDate = options.date const startDate = options.startDate ? stringToSqlTime(options.startDate) : null @@ -211,12 +213,13 @@ module.exports = function(s,config){ whereQuery.push(monitorRestrictions) } if(options.archived){ - whereQuery.push(['details','LIKE',`%"archived":"1"%`]) + whereQuery.push(['archive','=',`1`]) } if(options.filename){ whereQuery.push(['filename','=',options.filename]) frameLimit = "1"; } + if(noLimit)frameLimit = '0'; options.orderBy = options.orderBy ? options.orderBy : ['time','desc'] if(options.count)options.groupBy = options.groupBy ? options.groupBy : options.orderBy[0] knexQuery({ @@ -328,7 +331,10 @@ module.exports = function(s,config){ var endTimeOperator = options.endTimeOperator var startTime = options.startTime var limitString = `${options.limit}` - const monitorRestrictions = s.getMonitorRestrictions(options.user.details,monitorId) + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) getDatabaseRows({ monitorRestrictions: monitorRestrictions, table: theTableSelected, @@ -337,7 +343,7 @@ module.exports = function(s,config){ endDate: endTime, startOperator: startTimeOperator, endOperator: endTimeOperator, - limit: options.limit, + limit: options.noLimit === '1' ? '0' : options.limit, archived: archived, rowType: rowName, endIsStartTo: endIsStartTo diff --git a/libs/definitions.js b/libs/definitions.js index a58a6e24..c29802e6 100644 --- a/libs/definitions.js +++ b/libs/definitions.js @@ -1,25 +1,19 @@ var fs = require('fs') var express = require('express') +const { + mergeDeep +} = require('./common.js') +const frameworkBase = require(`../definitions/base.js`) module.exports = function(s,config,lang,app,io){ - s.location.definitions = s.mainDirectory+'/definitions' - try{ - var definitions = require(s.location.definitions+'/'+config.language+'.js')(s,config,lang) - }catch(er){ - console.error(er) - console.log('There was an error loading your definition file.') - try{ - var definitions = require(s.location.definitions+'/en_CA.js')(s,config,lang) - }catch(er){ - console.error(er) - console.log('There was an error loading your definition file.') - var definitions = require(s.location.definitions+'/en_CA.json'); - } + function getFramework(languageFile){ + return frameworkBase(s,config,languageFile) } + const defaultFramework = getFramework(lang) //load defintions dynamically - s.definitions = definitions + s.definitions = defaultFramework s.copySystemDefaultDefinitions = function(){ //en_CA - return Object.assign(s.definitions,{}) + return Object.assign({},defaultFramework) } s.loadedDefinitons={} s.loadedDefinitons[config.language] = s.copySystemDefaultDefinitions() @@ -28,10 +22,15 @@ module.exports = function(s,config,lang,app,io){ var file = s.loadedDefinitons[rule] if(!file){ try{ - s.loadedDefinitons[rule] = require(s.location.definitions+'/'+rule+'.js')(s,config,lang) - s.loadedDefinitons[rule] = Object.assign(s.copySystemDefaultDefinitions(),s.loadedDefinitons[rule]) + // console.log(getFramework(lang)) + s.loadedDefinitons[rule] = Object.assign( + {}, + s.copySystemDefaultDefinitions(), + getFramework(s.getLanguageFile(rule)) + ); file = s.loadedDefinitons[rule] }catch(err){ + console.error(err) file = s.copySystemDefaultDefinitions() } } @@ -40,5 +39,9 @@ module.exports = function(s,config,lang,app,io){ } return file } - return definitions + s.reloadDefinitions = function(){ + s.loadedDefinitons = {}; + s.loadedDefinitons[config.language] = s.copySystemDefaultDefinitions() + } + return defaultFramework } diff --git a/libs/dropInEvents.js b/libs/dropInEvents.js index 98860d56..f952fd33 100644 --- a/libs/dropInEvents.js +++ b/libs/dropInEvents.js @@ -188,14 +188,21 @@ module.exports = function(s,config,lang,app,io){ createDropInEventsDirectory() if(!config.ftpServerPort)config.ftpServerPort = 21 if(!config.ftpServerUrl)config.ftpServerUrl = `ftp://0.0.0.0:${config.ftpServerPort}` + if(!config.ftpServerPasvUrl)config.ftpServerPasvUrl = config.ftpServerUrl.replace(/.*:\/\//, '').replace(/:.*/, ''); + if(!config.ftpServerPasvMinPort)config.ftpServerPasvMinPort = 10050; + if(!config.ftpServerPasvMaxPort)config.ftpServerPasvMaxPort = 10100; config.ftpServerUrl = config.ftpServerUrl.replace('{{PORT}}',config.ftpServerPort) const FtpSrv = require('ftp-srv') + const ftpServer = new FtpSrv({ url: config.ftpServerUrl, // pasv_url must be set to enable PASV; ftp-srv uses its known IP if given 127.0.0.1, // and smart clients will ignore the IP anyway. Some Dahua IP cams require PASV mode. // ftp-srv just wants an IP only (no protocol or port) - pasv_url: config.ftpServerUrl.replace(/.*:\/\//, '').replace(/:.*/, ''), + pasv_url: config.ftpServerPasvUrl, + pasv_min: config.ftpServerPasvMinPort, + pasv_max: config.ftpServerPasvMaxPort, + greeting: "Shinobi FTP dropInEvent Server says hello!", log: require('bunyan').createLogger({ name: 'ftp-srv', level: 100 @@ -331,4 +338,5 @@ module.exports = function(s,config,lang,app,io){ s.systemLog(`SMTP Server running on port ${config.smtpServerPort}...`) }) } + require('./dropInEvents/mqtt.js')(s,config,lang,app,io) } diff --git a/libs/dropInEvents/mqtt.js b/libs/dropInEvents/mqtt.js new file mode 100644 index 00000000..4c77a402 --- /dev/null +++ b/libs/dropInEvents/mqtt.js @@ -0,0 +1,230 @@ +module.exports = (s,config,lang,app,io) => { + if(config.mqttClient === true){ + console.log('Loading MQTT Inbound Connectivity...') + const mqtt = require('mqtt') + const { + triggerEvent, + } = require('../events/utils.js')(s,config,lang) + function sendPlainEvent(options){ + const groupKey = options.ke + const monitorId = options.mid || options.id + const subKey = options.subKey + const endpoint = options.host + triggerEvent({ + id: monitorId, + ke: groupKey, + details: { + confidence: 100, + name: 'mqtt', + plug: endpoint, + reason: subKey + }, + },config.mqttEventForceSaveEvent) + } + function sendFrigateEvent(data,options){ + const groupKey = options.ke + const monitorId = options.mid || options.id + const subKey = options.subKey + const endpoint = options.host + const frigateMatrix = data.after || data.before + const confidenceScore = frigateMatrix.top_score * 100 + const activeZones = frigateMatrix.entered_zones.join(', ') + const shinobiMatrix = { + x: frigateMatrix.box[0], + y: frigateMatrix.box[1], + width: frigateMatrix.box[2], + height: frigateMatrix.box[3], + tag: frigateMatrix.label, + confidence: confidenceScore, + } + triggerEvent({ + id: monitorId, + ke: groupKey, + details: { + confidence: confidenceScore, + name: 'mqtt-'+endpoint, + plug: subKey, + reason: activeZones, + matrices: [shinobiMatrix] + }, + },config.mqttEventForceSaveEvent) + } + function createMqttSubscription(options){ + const mqttEndpoint = options.host + const subKey = options.subKey + const groupKey = options.ke + const onData = options.onData || function(){} + s.debugLog('Connecting.... mqtt://' + mqttEndpoint) + const client = mqtt.connect('mqtt://' + mqttEndpoint) + client.on('connect', function () { + s.debugLog('Connected! mqtt://' + mqttEndpoint) + client.subscribe(subKey, function (err) { + if (err) { + s.debugLog(err) + s.userLog({ + ke: groupKey, + mid: '$USER' + },{ + type: lang['MQTT Error'], + msg: err + }) + }else{ + client.on('message', function (topic, message) { + const data = s.parseJSON(message.toString()) + onData(data) + }) + } + }) + }) + return client + } + // const onEventTrigger = async () => {} + // const onMonitorUnexpectedExit = (monitorConfig) => {} + const loadMqttListBotForUser = function(user){ + const groupKey = user.ke + const userDetails = s.parseJSON(user.details); + if(userDetails.mqttclient === '1'){ + const mqttClientList = userDetails.mqttclient_list || [] + if(!s.group[groupKey].mqttSubscriptions)s.group[groupKey].mqttSubscriptions = {}; + const mqttSubs = s.group[groupKey].mqttSubscriptions + mqttClientList.forEach(function(row,n){ + try{ + const mqttSubId = `${row.host} ${row.subKey}` + const messageConversionTypes = row.type || [] + const monitorsToTrigger = row.monitors || [] + const triggerAllMonitors = monitorsToTrigger.indexOf('$all') > -1 + const doActions = [] + const onData = (data) => { + doActions.forEach(function(theAction){ + theAction(data) + }) + s.debugLog('MQTT Data',row,data) + } + if(mqttSubs[mqttSubId]){ + mqttSubs[mqttSubId].end() + delete(mqttSubs[mqttSubId]) + } + messageConversionTypes.forEach(function(type){ + switch(type){ + case'plain': + doActions.push(function(data){ + // data is unused for plain event. + let listOfMonitors = monitorsToTrigger + if(triggerAllMonitors){ + const activeMonitors = Object.keys(s.group[groupKey].activeMonitors) + listOfMonitors = activeMonitors + } + listOfMonitors.forEach(function(monitorId){ + sendPlainEvent({ + host: row.host, + subKey: row.subKey, + ke: groupKey, + mid: monitorId + }) + }) + }) + break; + case'frigate': + // https://docs.frigate.video/integrations/mqtt/#frigateevents + doActions.push(function(data){ + // this handler requires using frigate/events + // only "new" events will be captured. + if(data.type === 'new'){ + let listOfMonitors = monitorsToTrigger + if(triggerAllMonitors){ + const activeMonitors = Object.keys(s.group[groupKey].activeMonitors) + listOfMonitors = activeMonitors + } + listOfMonitors.forEach(function(monitorId){ + sendFrigateEvent(data,{ + host: row.host, + subKey: row.subKey, + ke: groupKey, + mid: monitorId + }) + }) + } + }) + break; + } + }) + mqttSubs[mqttSubId] = createMqttSubscription({ + host: row.host, + subKey: row.subKey, + ke: groupKey, + onData: onData, + }) + }catch(err){ + s.debugLog(err) + // s.systemLog(err) + } + }) + } + } + const unloadMqttListBotForUser = function(user){ + const groupKey = user.ke + const mqttSubs = s.group[groupKey].mqttSubscriptions || {} + Object.keys(mqttSubs).forEach(function(mqttSubId){ + try{ + mqttSubs[mqttSubId].end() + }catch(err){ + s.debugLog(err) + // s.userLog({ + // ke: groupKey, + // mid: '$USER' + // },{ + // type: lang['MQTT Error'], + // msg: err + // }) + } + delete(mqttSubs[mqttSubId]) + }) + } + const onBeforeAccountSave = function(data){ + data.d.mqttclient_list = [] + } + s.loadGroupAppExtender(loadMqttListBotForUser) + s.unloadGroupAppExtender(unloadMqttListBotForUser) + s.beforeAccountSave(onBeforeAccountSave) + // s.onEventTrigger(onEventTrigger) + // s.onMonitorUnexpectedExit(onMonitorUnexpectedExit) + s.definitions["Account Settings"].blocks["MQTT Inbound"] = { + "evaluation": "$user.details.use_mqttclient !== '0'", + "name": lang['MQTT Inbound'], + "color": "green", + "info": [ + { + "name": "detail=mqttclient", + "selector":"u_mqttclient", + "field": lang.Enabled, + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "fieldType": "btn", + "class": `btn-success mqtt-add-row`, + "btnContent": `   ${lang['Add']}`, + }, + { + "id": "mqttclient_list", + "fieldType": "div", + }, + { + "fieldType": "script", + "src": "assets/js/bs5.mqtt.js", + } + ] + } + } +} diff --git a/libs/events.js b/libs/events.js index fae6f351..9bfca61a 100644 --- a/libs/events.js +++ b/libs/events.js @@ -1,3 +1,3 @@ module.exports = function(s,config,lang){ - // all contents moved to libs/events/utils.js + require('./events/onvif.js')(s,config,lang) } diff --git a/libs/events/onvif.js b/libs/events/onvif.js new file mode 100644 index 00000000..6f177fe8 --- /dev/null +++ b/libs/events/onvif.js @@ -0,0 +1,109 @@ +module.exports = function(s,config,lang){ + const { + triggerEvent, + } = require('./utils.js')(s,config,lang) + const onvifEvents = require("node-onvif-events"); + const onvifEventIds = [] + const onvifEventControllers = {} + const startMotion = async (onvifId,monitorConfig) => { + const groupKey = monitorConfig.ke + const monitorId = monitorConfig.mid + const onvifIdKey = `${monitorConfig.mid}${monitorConfig.ke}` + const controlBaseUrl = monitorConfig.details.control_base_url || s.buildMonitorUrl(monitorConfig, true) + const controlURLOptions = s.cameraControlOptionsFromUrl(controlBaseUrl,monitorConfig) + const onvifPort = parseInt(monitorConfig.details.onvif_port) || 8000 + let options = { + id: onvifId, + hostname: controlURLOptions.host, + username: controlURLOptions.username, + password: controlURLOptions.password, + port: onvifPort, + }; + const detector = onvifEventControllers[onvifIdKey] || await onvifEvents.MotionDetector.create(options.id, options); + function onvifEventLog(type,data){ + s.userLog({ + ke: groupKey, + mid: monitorId + },{ + type: type, + msg: data + }) + } + onvifEventLog(`ONVIF Event Detection Listening!`) + try { + detector.listen((motion) => { + if (motion) { + // onvifEventLog(`ONVIF Event Detected!`) + triggerEvent({ + f: 'trigger', + id: monitorId, + ke: groupKey, + details:{ + plug: 'onvifEvent', + name: 'onvifEvent', + reason: 'motion', + confidence: 100, + // reason: 'object', + // matrices: [matrix], + // imgHeight: img.height, + // imgWidth: img.width, + } + }) + // } else { + // onvifEventLog(`ONVIF Event Stopped`) + } + }); + } catch(e) { + onvifEventLog(`ONVIF Event Error`,e) + } + return detector + } + function initializeOnvifEvents(monitorConfig){ + const monitorMode = monitorConfig.mode + const groupKey = monitorConfig.ke + const monitorId = monitorConfig.mid + const hasOnvifEventsEnabled = monitorConfig.details.is_onvif === '1' && monitorConfig.details.onvif_events === '1'; + if(hasOnvifEventsEnabled){ + const onvifIdKey = `${monitorConfig.mid}${monitorConfig.ke}` + let onvifId = onvifEventIds.indexOf(onvifIdKey) + if(onvifEventIds.indexOf(onvifIdKey) === -1){ + onvifId = onvifEventIds.length; + onvifEventIds.push(onvifIdKey); + } + try{ + onvifEventControllers[onvifIdKey].close() + s.debugLog('ONVIF Event Module Warning : This could cause a memory leak?') + }catch(err){ + s.debugLog('ONVIF Event Module Error', err); + } + delete(onvifEventControllers[onvifIdKey]) + if(monitorMode !== 'stop'){ + startMotion(onvifId,monitorConfig).then((detector) => { + onvifEventControllers[onvifIdKey] = detector; + }) + } + } + } + s.onMonitorStart((monitorConfig) => { + initializeOnvifEvents(monitorConfig) + }) + const connectionInfoArray = s.definitions["Monitor Settings"].blocks["Detector"].info + connectionInfoArray.splice(2, 0, { + "name": "detail=onvif_events", + "field": lang['ONVIF Events'], + "default": "0", + "form-group-class": "h_onvif_input h_onvif_1", + "form-group-class-pre-layer": "h_det_input h_det_1", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }); +} diff --git a/libs/events/utils.js b/libs/events/utils.js index 9a3a0ab3..34be5838 100644 --- a/libs/events/utils.js +++ b/libs/events/utils.js @@ -3,7 +3,6 @@ const moment = require('moment'); const execSync = require('child_process').execSync; const exec = require('child_process').exec; const spawn = require('child_process').spawn; -const request = require('request'); const imageSaveEventLock = {}; // Matrix In Region Libs > const SAT = require('sat') @@ -12,6 +11,9 @@ const P = SAT.Polygon; const B = SAT.Box; // Matrix In Region Libs /> module.exports = (s,config,lang,app,io) => { + // Event Filters > + const acceptableOperators = ['indexOf','!indexOf','===','!==','>=','>','<','<='] + // Event Filters /> const { splitForFFPMEG } = require('../ffmpeg/utils.js')(s,config,lang) @@ -19,8 +21,13 @@ module.exports = (s,config,lang,app,io) => { moveCameraPtzToMatrix } = require('../control/ptz.js')(s,config,lang) const { - cutVideoLength + cutVideoLength, + reEncodeVideoAndBinOriginalAddToQueue } = require('../video/utils.js')(s,config,lang) + const { + isEven, + fetchTimeout, + } = require('../basic/utils.js')(process.cwd(),config) async function saveImageFromEvent(options,frameBuffer){ const monitorId = options.mid || options.id const groupKey = options.ke @@ -64,6 +71,9 @@ module.exports = (s,config,lang,app,io) => { var newString = string + '' var d = Object.assign(eventData,addOps) var detailString = s.stringJSON(d.details) + var tag = detailString.matrices + && detailString.matrices[0] + && detailString.matrices[0].tag; newString = newString .replace(/{{TIME}}/g,d.currentTimestamp) .replace(/{{REGION_NAME}}/g,d.details.name) @@ -71,7 +81,10 @@ module.exports = (s,config,lang,app,io) => { .replace(/{{MONITOR_ID}}/g,d.id) .replace(/{{MONITOR_NAME}}/g,s.group[d.ke].rawMonitorConfigurations[d.id].name) .replace(/{{GROUP_KEY}}/g,d.ke) - .replace(/{{DETAILS}}/g,detailString) + .replace(/{{DETAILS}}/g,detailString); + if(tag){ + newString = newString.replace(/{{TAG}}/g,tag) + } if(d.details.confidence){ newString = newString .replace(/{{CONFIDENCE}}/g,d.details.confidence) @@ -146,7 +159,6 @@ module.exports = (s,config,lang,app,io) => { } const addToEventCounter = (eventData) => { const eventsCounted = s.group[eventData.ke].activeMonitors[eventData.id].detector_motion_count - s.debugLog(`addToEventCounter`,eventData,eventsCounted.length) eventsCounted.push(eventData) } const clearEventCounter = (groupKey,monitorId) => { @@ -187,8 +199,20 @@ module.exports = (s,config,lang,app,io) => { Object.keys(filters).forEach(function(key){ var conditionChain = {} var dFilter = filters[key] + if(dFilter.enabled === '0')return; + var numberOfOpenAndCloseBrackets = 0 dFilter.where.forEach(function(condition,place){ - conditionChain[place] = {ok:false,next:condition.p4,matrixCount:0} + const hasOpenBracket = condition.openBracket === '1'; + const hasCloseBracket = condition.closeBracket === '1'; + conditionChain[place] = { + ok: false, + next: condition.p4, + matrixCount: 0, + openBracket: hasOpenBracket, + closeBracket: hasCloseBracket, + } + if(hasOpenBracket)++numberOfOpenAndCloseBrackets; + if(hasCloseBracket)++numberOfOpenAndCloseBrackets; if(d.details.matrices)conditionChain[place].matrixCount = d.details.matrices.length var modifyFilters = function(toCheck,matrixPosition){ var param = toCheck[condition.p1] @@ -210,7 +234,12 @@ module.exports = (s,config,lang,app,io) => { pass() } break; - default: + case'===': + case'!==': + case'>=': + case'>': + case'<': + case'<=': if(eval('param '+condition.p2+' "'+condition.p3.replace(/"/g,'\\"')+'"')){ pass() } @@ -243,7 +272,7 @@ module.exports = (s,config,lang,app,io) => { var atSecond = parseInt(doAtTime[2]) - 1 || timeNow.getSeconds() var nowAddedInSeconds = atHourNow * 60 * 60 + atMinuteNow * 60 + atSecondNow var conditionAddedInSeconds = atHour * 60 * 60 + atMinute * 60 + atSecond - if(eval('nowAddedInSeconds '+condition.p2+' conditionAddedInSeconds')){ + if(acceptableOperators.indexOf(condition.p2) > -1 && eval('nowAddedInSeconds '+condition.p2+' conditionAddedInSeconds')){ conditionChain[place].ok = true } } @@ -254,20 +283,29 @@ module.exports = (s,config,lang,app,io) => { } }) var conditionArray = Object.values(conditionChain) - var validationString = '' + var validationString = [] + var allowBrackets = false; + if (numberOfOpenAndCloseBrackets === 0 || isEven(numberOfOpenAndCloseBrackets)){ + allowBrackets = true; + }else{ + s.userLog(d,{type:lang["Event Filter Error"],msg:lang.eventFilterErrorBrackets}) + } conditionArray.forEach(function(condition,number){ - validationString += condition.ok+' ' + validationString.push(`${allowBrackets && condition.openBracket ? '(' : ''}${condition.ok}${allowBrackets && condition.closeBracket ? ')' : ''}`); if(conditionArray.length-1 !== number){ - validationString += condition.next+' ' + validationString.push(condition.next) } }) - if(eval(validationString)){ + if(eval(validationString.join(' '))){ if(dFilter.actions.halt !== '1'){ delete(dFilter.actions.halt) Object.keys(dFilter.actions).forEach(function(key){ var value = dFilter.actions[key] filter[key] = parseValue(key,value) }) + if(dFilter.actions.record === '1'){ + filter.forceRecord = true + } }else{ filter.halt = true } @@ -362,7 +400,7 @@ module.exports = (s,config,lang,app,io) => { matrices: eventDetails.matrices || [], },d.frame) } - if(forceSave || (filter.save && monitorDetails.detector_save === '1')){ + if(forceSave || (filter.save || monitorDetails.detector_save === '1')){ s.knexQuery({ action: "insert", table: "Events", @@ -370,7 +408,7 @@ module.exports = (s,config,lang,app,io) => { ke: d.ke, mid: d.id, details: detailString, - time: eventTime, + time: s.formattedTime(eventTime), } }) } @@ -384,12 +422,12 @@ module.exports = (s,config,lang,app,io) => { detector_timeout = parseFloat(monitorDetails.detector_timeout) } if( - filter.record && + (filter.forceRecord || (filter.record && monitorDetails.detector_trigger === '1')) && monitorConfig.mode === 'start' && - monitorDetails.detector_trigger === '1' && (monitorDetails.detector_record_method === 'sip' || monitorDetails.detector_record_method === 'hot') ){ - createEventBasedRecording(d,moment(eventTime).subtract(5,'seconds').format('YYYY-MM-DDTHH-mm-ss')) + const secondBefore = (parseInt(monitorDetails.detector_buffer_seconds_before) || 5) + 1 + createEventBasedRecording(d,moment(eventTime).subtract(secondBefore,'seconds').format('YYYY-MM-DDTHH-mm-ss')) } d.currentTime = eventTime d.currentTimestamp = s.timeObject(d.currentTime).format() @@ -401,14 +439,17 @@ module.exports = (s,config,lang,app,io) => { var detector_webhook_url = addEventDetailsToString(d,monitorDetails.detector_webhook_url) var webhookMethod = monitorDetails.detector_webhook_method if(!webhookMethod || webhookMethod === '')webhookMethod = 'GET' - request(detector_webhook_url,{method: webhookMethod,encoding:null},function(err,data){ - if(err){ - s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}}) - } + fetchTimeout(detector_webhook_url,10000,{ + method: webhookMethod + }).catch((err) => { + s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}}) }) } - if(filter.command && monitorDetails.detector_command_enable === '1' && !s.group[d.ke].activeMonitors[d.id].detector_command){ + if( + filter.command || + (monitorDetails.detector_command_enable === '1' && !s.group[d.ke].activeMonitors[d.id].detector_command) + ){ s.group[d.ke].activeMonitors[d.id].detector_command = s.createTimeout('detector_command',s.group[d.ke].activeMonitors[d.id],monitorDetails.detector_command_timeout,10) var detector_command = addEventDetailsToString(d,monitorDetails.detector_command) if(detector_command === '')return @@ -431,23 +472,27 @@ module.exports = (s,config,lang,app,io) => { if(activeMonitor && activeMonitor.eventBasedRecording && activeMonitor.eventBasedRecording.process){ const eventBasedRecording = activeMonitor.eventBasedRecording const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId] - const videoLength = monitorConfig.details.detector_send_video_length + const videoLength = parseInt(monitorConfig.details.detector_send_video_length) || 10 const recordingDirectory = s.getVideoDirectory(monitorConfig) const fileTime = eventBasedRecording.lastFileTime const filename = `${fileTime}.mp4` response.filename = `${filename}` response.filePath = `${recordingDirectory}${filename}` - eventBasedRecording.process.on('close',function(){ + eventBasedRecording.process.on('exit',function(){ setTimeout(async () => { if(!isNaN(videoLength)){ const cutResponse = await cutVideoLength({ ke: groupKey, mid: monitorId, filePath: response.filePath, - cutLength: parseInt(videoLength), + cutLength: videoLength, }) - response.filename = cutResponse.filename - response.filePath = cutResponse.filePath + if(cutResponse.ok){ + response.filename = cutResponse.filename + response.filePath = cutResponse.filePath + }else{ + s.debugLog('cutResponse',cutResponse) + } } resolve(response) },1000) @@ -489,14 +534,26 @@ module.exports = (s,config,lang,app,io) => { var ffmpegError = '' var error var filename = fileTime + '.mp4' + let outputMap = `-map 0:0 ` + const analyzeDuration = parseInt(monitorDetails.event_record_aduration) || 1000 + const probeSize = parseInt(monitorDetails.event_record_probesize) || 32 s.userLog(d,{ type: logTitleText, msg: lang["Started"] }) //-t 00:'+s.timeObject(new Date(detector_timeout * 1000 * 60)).format('mm:ss')+' + if( + monitorDetails.detector_buffer_acodec && + monitorDetails.detector_buffer_acodec !== 'no' && + monitorDetails.detector_buffer_acodec !== 'auto' + ){ + outputMap += `-map 0:1 ` + } + const ffmpegCommand = `-loglevel warning -live_start_index -99999 -analyzeduration ${analyzeDuration} -probesize ${probeSize} -re -i "${s.dir.streams+d.ke+'/'+d.id}/detectorStream.m3u8" ${outputMap}-movflags faststart+frag_keyframe+empty_moov -fflags +igndts -c:v copy -c:a aac -strict -2 -strftime 1 -y "${s.getVideoDirectory(monitorConfig) + filename}"` + s.debugLog(ffmpegCommand) activeMonitor.eventBasedRecording.process = spawn( config.ffmpegDir, - splitForFFPMEG(('-loglevel warning -analyzeduration 1000000 -probesize 1000000 -re -i "'+s.dir.streams+d.ke+'/'+d.id+'/detectorStream.m3u8" -movflags faststart+frag_keyframe+empty_moov -fflags +igndts -c:v copy -strftime 1 "'+s.getVideoDirectory(monitorConfig) + filename + '"')) + splitForFFPMEG(ffmpegCommand) ) activeMonitor.eventBasedRecording.process.stderr.on('data',function(data){ s.userLog(d,{ @@ -515,7 +572,24 @@ module.exports = (s,config,lang,app,io) => { } s.insertCompletedVideo(monitorConfig,{ file : filename, - }) + },function(err,response){ + const autoCompressionEnabled = monitorDetails.auto_compress_videos === '1'; + if(autoCompressionEnabled){ + reEncodeVideoAndBinOriginalAddToQueue({ + video: response.insertQuery, + targetVideoCodec: 'vp9', + targetAudioCodec: 'libopus', + targetQuality: '-q:v 1 -q:a 1', + targetExtension: 'webm', + doSlowly: false, + automated: true, + }).then((encodeResponse) => { + s.debugLog('Complete Automatic Compression',encodeResponse) + }).catch((err) => { + console.log(err) + }) + } + }); s.userLog(d,{ type: logTitleText, msg: lang["Detector Recording Complete"] @@ -594,12 +668,13 @@ module.exports = (s,config,lang,app,io) => { halt : false, addToMotionCounter : true, useLock : true, - save : true, - webhook : true, - command : true, + save : false, + webhook : false, + command : false, record : true, + forceRecord : false, indifference : false, - countObjects : true + countObjects : false } if(!s.group[d.ke] || !s.group[d.ke].activeMonitors[d.id]){ return s.systemLog(lang['No Monitor Found, Ignoring Request']) @@ -608,14 +683,14 @@ module.exports = (s,config,lang,app,io) => { if(!monitorConfig){ return s.systemLog(lang['No Monitor Found, Ignoring Request']) } + const activeMonitor = s.group[d.ke].activeMonitors[d.id] const monitorDetails = monitorConfig.details s.onEventTriggerBeforeFilterExtensions.forEach(function(extender){ extender(d,filter) }) const eventDetails = d.details - const passedEventFilters = checkEventFilters(d,monitorDetails,filter) - if(!passedEventFilters)return - const detailString = JSON.stringify(eventDetails) + const passedEventFilters = checkEventFilters(d,activeMonitor.details,filter) + if(!passedEventFilters)return; const eventTime = new Date() if( filter.addToMotionCounter && @@ -638,8 +713,7 @@ module.exports = (s,config,lang,app,io) => { addToEventCounter(d) } if( - filter.countObjects && - monitorDetails.detector_obj_count === '1' && + (filter.countObjects || monitorDetails.detector_obj_count === '1') && monitorDetails.detector_obj_count_in_region !== '1' ){ didCountingAlready = true diff --git a/libs/extenders.js b/libs/extenders.js index 89a887be..be305680 100644 --- a/libs/extenders.js +++ b/libs/extenders.js @@ -1,186 +1,66 @@ module.exports = function(s,config){ - ////// USER ////// - s.onSocketAuthenticationExtensions = [] - s.onSocketAuthentication = function(callback){ - s.onSocketAuthenticationExtensions.push(callback) - } - // - s.onUserLogExtensions = [] - s.onUserLog = function(callback){ - s.onUserLogExtensions.push(callback) - } - // - s.loadGroupExtensions = [] - s.loadGroupExtender = function(callback){ - s.loadGroupExtensions.push(callback) - } - // - s.loadGroupAppExtensions = [] - s.loadGroupAppExtender = function(callback){ - s.loadGroupAppExtensions.push(callback) - } - // - s.unloadGroupAppExtensions = [] - s.unloadGroupAppExtender = function(callback){ - s.unloadGroupAppExtensions.push(callback) - } - // - s.cloudDisksLoaded = [] - s.cloudDisksLoader = function(storageType){ - s.cloudDisksLoaded.push(storageType) - } - // - s.onAccountSaveExtensions = [] - s.onAccountSave = function(callback){ - s.onAccountSaveExtensions.push(callback) - } - // - s.beforeAccountSaveExtensions = [] - s.beforeAccountSave = function(callback){ - s.beforeAccountSaveExtensions.push(callback) - } - // - s.onTwoFactorAuthCodeNotificationExtensions = [] - s.onTwoFactorAuthCodeNotification = function(callback){ - s.onTwoFactorAuthCodeNotificationExtensions.push(callback) - } - // - s.onStalePurgeLockExtensions = [] - s.onStalePurgeLock = function(callback){ - s.onStalePurgeLockExtensions.push(callback) - } - // s.cloudDiskUseStartupExtensions = {} s.cloudDiskUseOnGetVideoDataExtensions = {} - + function createExtension(nameOfExtension,nameOfExtensionContainer,objective){ + nameOfExtensionContainer = nameOfExtensionContainer || `${nameOfExtension}Extensions` + if(objective){ + s[nameOfExtensionContainer] = [] + s[nameOfExtension] = function(nameOfCallback,callback){ + s[nameOfExtensionContainer][nameOfCallback] = callback + } + }else{ + s[nameOfExtensionContainer] = [] + s[nameOfExtension] = function(callback){ + s[nameOfExtensionContainer].push(callback) + } + } + } + ////// USER ////// + createExtension(`onSocketAuthentication`) + createExtension(`onUserLog`) + createExtension(`loadGroupExtender`,`loadGroupExtensions`) + createExtension(`loadGroupAppExtender`,`loadGroupAppExtensions`) + createExtension(`unloadGroupAppExtender`,`unloadGroupAppExtensions`) + createExtension(`cloudDisksLoader`,`cloudDisksLoaded`) + createExtension(`onAccountSave`) + createExtension(`beforeAccountSave`) + createExtension(`onTwoFactorAuthCodeNotification`) + createExtension(`onStalePurgeLock`) ////// EVENTS ////// - s.onEventTriggerExtensions = [] - s.onEventTrigger = function(callback){ - s.onEventTriggerExtensions.push(callback) - } - s.onEventTriggerBeforeFilterExtensions = [] - s.onEventTriggerBeforeFilter = function(callback){ - s.onEventTriggerBeforeFilterExtensions.push(callback) - } - s.onFilterEventExtensions = [] - s.onFilterEvent = function(callback){ - s.onFilterEventExtensions.push(callback) - } - + createExtension(`onEventTrigger`) + createExtension(`onEventTriggerBeforeFilter`) + createExtension(`onFilterEvent`) ////// MONITOR ////// - s.onMonitorInitExtensions = [] - s.onMonitorInit = function(callback){ - s.onMonitorInitExtensions.push(callback) - } - // - s.onMonitorStartExtensions = [] - s.onMonitorStart = function(callback){ - s.onMonitorStartExtensions.push(callback) - } - // - s.onMonitorStopExtensions = [] - s.onMonitorStop = function(callback){ - s.onMonitorStopExtensions.push(callback) - } - // - s.onMonitorSaveExtensions = [] - s.onMonitorSave = function(callback){ - s.onMonitorSaveExtensions.push(callback) - } - // - s.onMonitorUnexpectedExitExtensions = [] - s.onMonitorUnexpectedExit = function(callback){ - s.onMonitorUnexpectedExitExtensions.push(callback) - } - // - s.onDetectorNoTriggerTimeoutExtensions = [] - s.onDetectorNoTriggerTimeout = function(callback){ - s.onDetectorNoTriggerTimeoutExtensions.push(callback) - } - // - s.onFfmpegCameraStringCreationExtensions = [] - s.onFfmpegCameraStringCreation = function(callback){ - s.onFfmpegCameraStringCreationExtensions.push(callback) - } - // - s.onMonitorPingFailedExtensions = [] - s.onMonitorPingFailed = function(callback){ - s.onMonitorPingFailedExtensions.push(callback) - } - // - s.onMonitorDiedExtensions = [] - s.onMonitorDied = function(callback){ - s.onMonitorDiedExtensions.push(callback) - } - + createExtension(`onMonitorInit`) + createExtension(`onMonitorStart`) + createExtension(`onMonitorStop`) + createExtension(`onMonitorSave`) + createExtension(`onMonitorUnexpectedExit`) + createExtension(`onDetectorNoTriggerTimeout`) + createExtension(`onFfmpegCameraStringCreation`) + createExtension(`onFfmpegBuildMainStream`) + createExtension(`onFfmpegBuildStreamChannel`) + createExtension(`onMonitorPingFailed`) + createExtension(`onMonitorDied`) + createExtension(`onMonitorCreateStreamPipe`) ///////// SYSTEM //////// - s.onProcessReadyExtensions = [] - s.onProcessReady = function(callback){ - s.onProcessReadyExtensions.push(callback) - } - // - s.onProcessExitExtensions = [] - s.onProcessExit = function(callback){ - s.onProcessExitExtensions.push(callback) - } - // - s.onBeforeDatabaseLoadExtensions = [] - s.onBeforeDatabaseLoad = function(callback){ - s.onBeforeDatabaseLoadExtensions.push(callback) - } - // - s.onFFmpegLoadedExtensions = [] - s.onFFmpegLoaded = function(callback){ - s.onFFmpegLoadedExtensions.push(callback) - } - // - s.beforeMonitorsLoadedOnStartupExtensions = [] - s.beforeMonitorsLoadedOnStartup = function(callback){ - s.beforeMonitorsLoadedOnStartupExtensions.push(callback) - } - // - s.onWebSocketConnectionExtensions = [] - s.onWebSocketConnection = function(callback){ - s.onWebSocketConnectionExtensions.push(callback) - } - // - s.onWebSocketDisconnectionExtensions = [] - s.onWebSocketDisconnection = function(callback){ - s.onWebSocketDisconnectionExtensions.push(callback) - } - // - s.onWebsocketMessageSendExtensions = [] - s.onWebsocketMessageSend = function(callback){ - s.onWebsocketMessageSendExtensions.push(callback) - } - // - s.onGetCpuUsageExtensions = [] - s.onGetCpuUsage = function(callback){ - s.onGetCpuUsageExtensions.push(callback) - } - // - s.onGetRamUsageExtensions = [] - s.onGetRamUsage = function(callback){ - s.onGetRamUsageExtensions.push(callback) - } - // - s.onSubscriptionCheckExtensions = [] - s.onSubscriptionCheck = function(callback){ - s.onSubscriptionCheckExtensions.push(callback) - } - // + createExtension(`onProcessReady`) + createExtension(`onProcessExit`) + createExtension(`onBeforeDatabaseLoad`) + createExtension(`onFFmpegLoaded`) + createExtension(`beforeMonitorsLoadedOnStartup`) + createExtension(`onWebSocketConnection`) + createExtension(`onWebSocketDisconnection`) + createExtension(`onWebsocketMessageSend`) + createExtension(`onOtherWebSocketMessages`) + createExtension(`onGetCpuUsage`) + createExtension(`onGetRamUsage`) + createExtension(`onSubscriptionCheck`) + createExtension(`onDataPortMessage`) + createExtension(`onHttpRequestUpgrade`,null,true) /////// VIDEOS //////// - s.insertCompletedVideoExtensions = [] - s.insertCompletedVideoExtender = function(callback){ - s.insertCompletedVideoExtensions.push(callback) - } - s.onBeforeInsertCompletedVideoExtensions = [] - s.onBeforeInsertCompletedVideo = function(callback){ - s.onBeforeInsertCompletedVideoExtensions.push(callback) - } + createExtension(`insertCompletedVideoExtender`,`insertCompletedVideoExtensions`) + createExtension(`onBeforeInsertCompletedVideo`) /////// TIMELAPSE //////// - s.onInsertTimelapseFrameExtensions = [] - s.onInsertTimelapseFrame = function(callback){ - s.onInsertTimelapseFrameExtensions.push(callback) - } + createExtension(`onInsertTimelapseFrame`) } diff --git a/libs/ffmpeg.js b/libs/ffmpeg.js index dd29f08c..4a4502fa 100644 --- a/libs/ffmpeg.js +++ b/libs/ffmpeg.js @@ -26,9 +26,15 @@ module.exports = async (s,config,lang,onFinish) => { s.ffmpeg = function(e){ try{ + const activeMonitor = s.group[e.ke].activeMonitors[e.mid]; + const dataPortToken = s.gid(10); + s.dataPortTokens[dataPortToken] = { + type: 'cameraThread', + ke: e.ke, + mid: e.mid, + } const ffmpegCommand = [`-progress pipe:5`]; - ([ - buildMainInput(e), + const allOutputs = [ buildMainStream(e), buildJpegApiOutput(e), buildMainRecording(e), @@ -36,45 +42,53 @@ module.exports = async (s,config,lang,onFinish) => { buildMainDetector(e), buildEventRecordingOutput(e), buildTimelapseOutput(e), - ]).forEach(function(commandStringPart){ - ffmpegCommand.push(commandStringPart) - }) - s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){ - extender(e,ffmpegCommand) - }) - const stdioPipes = createPipeArray(e) - const ffmpegCommandString = ffmpegCommand.join(' ') - //hold ffmpeg command for log stream - s.group[e.ke].activeMonitors[e.mid].ffmpeg = sanitizedFfmpegCommand(e,ffmpegCommandString) - //clean the string of spatial impurities and split for spawn() - const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString) - try{ - fs.unlinkSync(e.sdir + 'cmd.txt') - }catch(err){ - - } - fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({ - cmd: ffmpegCommandParsed, - pipes: stdioPipes.length, - rawMonitorConfig: s.group[e.ke].rawMonitorConfigurations[e.id], - globalInfo: { - config: config, - isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected - } - },null,3),'utf8') - var cameraCommandParams = [ - config.monitorDaemonPath ? config.monitorDaemonPath : __dirname + '/cameraThread/singleCamera.js', - config.ffmpegDir, - e.sdir + 'cmd.txt' - ] - const cameraProcess = spawn('node',cameraCommandParams,{detached: true,stdio: stdioPipes}) - if(config.debugLog === true){ - cameraProcess.stderr.on('data',(data) => { - console.log(`${e.ke} ${e.mid}`) - console.log(data.toString()) + ]; + if(allOutputs.filter(output => !!output).length > 0){ + ([ + buildMainInput(e), + ]).concat(allOutputs).forEach(function(commandStringPart){ + ffmpegCommand.push(commandStringPart) }) + s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){ + extender(e,ffmpegCommand) + }) + const stdioPipes = createPipeArray(e) + const ffmpegCommandString = ffmpegCommand.join(' ') + //hold ffmpeg command for log stream + activeMonitor.ffmpeg = sanitizedFfmpegCommand(e,ffmpegCommandString) + //clean the string of spatial impurities and split for spawn() + const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString) + try{ + fs.unlinkSync(e.sdir + 'cmd.txt') + }catch(err){ + + } + fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({ + dataPortToken: dataPortToken, + cmd: ffmpegCommandParsed, + pipes: stdioPipes.length, + rawMonitorConfig: s.group[e.ke].rawMonitorConfigurations[e.id], + globalInfo: { + config: config, + isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected + } + },null,3),'utf8') + var cameraCommandParams = [ + config.monitorDaemonPath ? config.monitorDaemonPath : __dirname + '/cameraThread/singleCamera.js', + config.ffmpegDir, + e.sdir + 'cmd.txt' + ] + const cameraProcess = spawn('node',cameraCommandParams,{detached: true,stdio: stdioPipes}) + if(config.debugLog === true && config.debugLogMonitors === true){ + cameraProcess.stderr.on('data',(data) => { + console.log(`${e.ke} ${e.mid}`) + console.log(data.toString()) + }) + } + return cameraProcess + }else{ + return null } - return cameraProcess }catch(err){ s.systemLog(err) return null diff --git a/libs/ffmpeg/builders.js b/libs/ffmpeg/builders.js index b2711e79..8583de25 100644 --- a/libs/ffmpeg/builders.js +++ b/libs/ffmpeg/builders.js @@ -9,6 +9,8 @@ module.exports = (s,config,lang) => { const { validateDimensions, } = require('./utils.js')(s,config,lang) + if(!config.outputsWithAudio)config.outputsWithAudio = ['hls','flv','mp4','rtmp']; + if(!config.outputsNotCapableOfPresets)config.outputsNotCapableOfPresets = []; const hasCudaEnabled = (monitor) => { return monitor.details.accelerator === '1' && monitor.details.hwaccel === 'cuvid' && monitor.details.hwaccel_vcodec === ('h264_cuvid' || 'hevc_cuvid' || 'mjpeg_cuvid' || 'mpeg4_cuvid') } @@ -183,7 +185,7 @@ module.exports = (s,config,lang) => { const createStreamChannel = function(e,number,channel){ //`e` is the monitor object //`x` is an object used to contain temporary values. - const channelStreamDirectory = !isNaN(parseInt(number)) ? `${e.sdir}channel${number}/` : e.sdir + const channelStreamDirectory = !isNaN(parseInt(number)) ? `${e.sdir || s.getStreamsDirectory(e)}channel${number}/` : e.sdir if(channelStreamDirectory !== e.sdir && !fs.existsSync(channelStreamDirectory)){ try{ fs.mkdirSync(channelStreamDirectory) @@ -203,7 +205,7 @@ module.exports = (s,config,lang) => { const streamType = channel.stream_type ? channel.stream_type : 'hls' const videoFps = !isNaN(parseFloat(channel.stream_fps)) && channel.stream_fps !== '0' ? parseFloat(channel.stream_fps) : streamType === 'rtmp' ? '30' : null const inputMap = buildInputMap(e,e.details.input_map_choices[`stream_channel-${channelNumber}`]) - const outputCanHaveAudio = (streamType === 'hls' || streamType === 'mp4' || streamType === 'flv' || streamType === 'h265' || streamType === 'rtmp') + const outputCanHaveAudio = config.outputsWithAudio.indexOf(streamType) > -1; const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64' const outputIsPresetCapable = outputCanHaveAudio const { videoWidth, videoHeight } = validateDimensions(channel.stream_scale_x,channel.stream_scale_y) @@ -246,7 +248,7 @@ module.exports = (s,config,lang) => { streamFilters.push(channel.stream_vf) } if(outputIsPresetCapable){ - const streamPreset = streamType !== 'h265' && channel.preset_stream ? channel.preset_stream : null + const streamPreset = config.outputsNotCapableOfPresets.indexOf(streamType) === -1 && channel.preset_stream ? channel.preset_stream : null if(streamPreset){ streamFlags.push(`-preset ${streamPreset}`) } @@ -287,18 +289,18 @@ module.exports = (s,config,lang) => { streamFlags.push(`-g 1`) } } - streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${channelStreamDirectory}s.m3u8"`) + streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist+discont_start "${channelStreamDirectory}s.m3u8"`) break; case'mjpeg': streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:${number}`) break; - case'h265': - streamFlags.push(`-movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1 -f hevc pipe:${number}`) - break; case'b64':case'':case undefined:case null://base64 streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:${number}`) break; } + s.onFfmpegBuildStreamChannelExtensions.forEach(function(extender){ + extender(streamType,streamFlags,number,e) + }); return ' ' + streamFlags.join(' ') } const buildMainInput = function(e){ @@ -375,7 +377,7 @@ module.exports = (s,config,lang) => { //x = temporary values const streamFlags = [] const streamType = e.details.stream_type ? e.details.stream_type : 'hls' - if(streamType !== 'jpeg'){ + if(streamType !== 'jpeg' && streamType !== 'useSubstream'){ const isCudaEnabled = hasCudaEnabled(e) const streamFilters = [] const videoCodecisCopy = e.details.stream_vcodec === 'copy' @@ -384,7 +386,7 @@ module.exports = (s,config,lang) => { const videoQuality = e.details.stream_quality ? e.details.stream_quality : '1' const videoFps = !isNaN(parseFloat(e.details.stream_fps)) && e.details.stream_fps !== '0' ? parseFloat(e.details.stream_fps) : null const inputMap = buildInputMap(e,e.details.input_map_choices.stream) - const outputCanHaveAudio = (streamType === 'hls' || streamType === 'mp4' || streamType === 'flv' || streamType === 'h265') + const outputCanHaveAudio = config.outputsWithAudio.indexOf(streamType) > -1; const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64' const outputIsPresetCapable = outputCanHaveAudio const { videoWidth, videoHeight } = validateDimensions(e.details.stream_scale_x,e.details.stream_scale_y) @@ -429,7 +431,7 @@ module.exports = (s,config,lang) => { streamFilters.push(e.details.stream_vf) } if(outputIsPresetCapable){ - const streamPreset = streamType !== 'h265' && e.details.preset_stream ? e.details.preset_stream : null + const streamPreset = config.outputsNotCapableOfPresets.indexOf(streamType) === -1 && e.details.preset_stream ? e.details.preset_stream : null if(streamPreset){ streamFlags.push(`-preset ${streamPreset}`) } @@ -460,18 +462,18 @@ module.exports = (s,config,lang) => { streamFlags.push(`-g 1`) } } - streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${e.sdir}s.m3u8"`) + streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist+discont_start "${e.sdir}s.m3u8"`) break; case'mjpeg': streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:1`) break; - case'h265': - streamFlags.push(`-movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1 -f hevc pipe:1`) - break; case'b64':case'':case undefined:case null://base64 streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:1`) break; } + s.onFfmpegBuildMainStreamExtensions.forEach(function(extender){ + extender(streamType,streamFlags,e) + }); if(e.details.custom_output){ streamFlags.push(e.details.custom_output) } @@ -652,20 +654,20 @@ module.exports = (s,config,lang) => { if(objectDetectorOutputIsEnabled){ addObjectDetectorInputMap() addObjectDetectValues() - detectorFlags.push('-an -f singlejpeg pipe:4') + detectorFlags.push('-an -f mjpeg pipe:4') } }else if(sendFramesToObjectDetector){ addObjectDetectorInputMap() addObjectDetectValues() - detectorFlags.push('-an -f singlejpeg pipe:4') + detectorFlags.push('-an -f mjpeg pipe:4') }else{ addInputMap() - detectorFlags.push('-an -f singlejpeg pipe:4') + detectorFlags.push('-an -f mjpeg pipe:4') } }else if(sendFramesToObjectDetector){ addObjectDetectorInputMap() addObjectDetectValues() - detectorFlags.push('-an -f singlejpeg pipe:4') + detectorFlags.push('-an -f mjpeg pipe:4') } return detectorFlags.join(' ') } @@ -677,13 +679,17 @@ module.exports = (s,config,lang) => { const isCudaEnabled = hasCudaEnabled(e) const outputFilters = [] var videoCodec = e.details.detector_buffer_vcodec + var liveStartIndex = e.details.detector_buffer_live_start_index || '-3' var audioCodec = e.details.detector_buffer_acodec ? e.details.detector_buffer_acodec : 'no' const videoCodecisCopy = videoCodec === 'copy' const videoFps = !isNaN(parseFloat(e.details.stream_fps)) && e.details.stream_fps !== '0' ? parseFloat(e.details.stream_fps) : null const inputMap = buildInputMap(e,e.details.input_map_choices.detector_sip_buffer) const { videoWidth, videoHeight } = validateDimensions(e.details.event_record_scale_x,e.details.event_record_scale_y) const hlsTime = !isNaN(parseInt(e.details.detector_buffer_hls_time)) ? `${parseInt(e.details.detector_buffer_hls_time)}` : '2' - const hlsListSize = !isNaN(parseInt(e.details.detector_buffer_hls_list_size)) ? `${parseInt(e.details.detector_buffer_hls_list_size)}` : '4' + // const hlsListSize = !isNaN(parseInt(e.details.detector_buffer_hls_list_size)) ? `${parseInt(e.details.detector_buffer_hls_list_size)}` : '4' + const secondsBefore = parseInt(e.details.detector_buffer_seconds_before) || 5 + let hlsListSize = parseInt(secondsBefore * 0.6) + hlsListSize = hlsListSize < 3 ? 3 : hlsListSize; if(inputMap)outputFlags.push(inputMap) if(e.details.cust_sip_record)outputFlags.push(e.details.cust_sip_record) if(videoCodec === 'auto'){ @@ -737,7 +743,7 @@ module.exports = (s,config,lang) => { outputFlags.push(`-g 1`) } } - outputFlags.push(`-f hls -live_start_index -3 -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${e.sdir}detectorStream.m3u8"`) + outputFlags.push(`-f hls -live_start_index -3 -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist+discont_start "${e.sdir}detectorStream.m3u8"`) } return outputFlags.join(' ') } @@ -757,11 +763,63 @@ module.exports = (s,config,lang) => { if(videoFilters.length > 0){ videoFlags.push(`-vf "${videoFilters.join(',')}"`) } - videoFlags.push(`-f singlejpeg -an -q:v 1 pipe:7`) + videoFlags.push(`-f mjpeg -an -q:v 1 pipe:7`) return videoFlags.join(' ') } return `` } + const getDefaultSubstreamFields = function(monitor){ + const subStreamFields = s.parseJSON(monitor.details.substream || {input:{},output:{}}) + const inputAndConnectionFields = Object.assign({ + "type":"h264", + "fulladdress":"", + "sfps":"", + "aduration":"", + "probesize":"", + "stream_loop":"0", + "rtsp_transport":"", + "accelerator":"0", + "hwaccel":"", + "hwaccel_vcodec":"auto", + "hwaccel_device":"", + "cust_input":"" + },subStreamFields.input); + const outputFields = Object.assign({ + "stream_type":"hls", + "rtmp_server_url":"", + "rtmp_stream_key":"", + "stream_mjpeg_clients":"", + "stream_vcodec":"copy", + "stream_acodec":"no", + "stream_fps":"", + "hls_time":"", + "preset_stream":"", + "hls_list_size":"", + "stream_quality":"", + "stream_v_br":"", + "stream_a_br":"", + "stream_scale_x":"", + "stream_scale_y":"", + "rotate_stream":"no", + "svf":"", + "cust_stream":"" + },subStreamFields.output); + return { + inputAndConnectionFields, + outputFields, + } + } + const buildSubstreamString = function(channelNumber,monitor){ + let ffmpegParts = [] + const { + inputAndConnectionFields, + outputFields, + } = getDefaultSubstreamFields(monitor) + ffmpegParts.push(`-loglevel ${monitor.details.loglevel || 'warning'}`) + ffmpegParts.push(createInputMap(monitor,channelNumber,inputAndConnectionFields)) + ffmpegParts.push(createStreamChannel(monitor,channelNumber,outputFields)) + return ffmpegParts.join(' ') + } return { createStreamChannel: createStreamChannel, buildMainInput: buildMainInput, @@ -772,5 +830,7 @@ module.exports = (s,config,lang) => { buildMainDetector: buildMainDetector, buildEventRecordingOutput: buildEventRecordingOutput, buildTimelapseOutput: buildTimelapseOutput, + getDefaultSubstreamFields: getDefaultSubstreamFields, + buildSubstreamString: buildSubstreamString, } } diff --git a/libs/ffmpeg/subStream.js b/libs/ffmpeg/subStream.js new file mode 100644 index 00000000..87740f34 --- /dev/null +++ b/libs/ffmpeg/subStream.js @@ -0,0 +1,16 @@ +function createStringForSubstreamProcess(options){ + let options = { + "type":"h264", + "fulladdress":"", + "sfps":"", + "aduration":"", + "probesize":"", + "stream_loop":"0", + "rtsp_transport":"", + "accelerator":"0", + "hwaccel":"auto", + "hwaccel_vcodec":"auto", + "hwaccel_device":"", + "cust_input":"" + } +} diff --git a/libs/ffmpeg/utils.js b/libs/ffmpeg/utils.js index 432092a3..115576b3 100644 --- a/libs/ffmpeg/utils.js +++ b/libs/ffmpeg/utils.js @@ -14,48 +14,63 @@ module.exports = (s,config,lang) => { var endData = {ok: false, result: {}} if(!url){ endData.error = 'Missing URL' - callback(endData) - return + if(callback)callback(endData) + return { + result: { + format: { + duration: 1 + } + } + } } if(!forceOverlap && activeProbes[auth]){ endData.error = 'Account is already probing' - callback(endData) - return - } - activeProbes[auth] = 1 - var stderr = '' - var stdout = '' - const probeCommand = splitForFFPMEG(`${customInput ? customInput + ' ' : ''}-analyzeduration 10000 -probesize 10000 -v quiet -print_format json -show_format -show_streams -i "${url}"`) - var processTimeout = null - var ffprobeLocation = config.ffmpegDir.split('/') - ffprobeLocation[ffprobeLocation.length - 1] = 'ffprobe' - ffprobeLocation = ffprobeLocation.join('/') - const probeProcess = spawn(ffprobeLocation, probeCommand) - const finishReponse = () => { - delete(activeProbes[auth]) - if(!stdout){ - endData.error = stderr - }else{ - endData.ok = true - endData.result = s.parseJSON(stdout) + if(callback)callback(endData) + return { + result: { + format: { + duration: 1 + } + } } - endData.probe = probeCommand - callback(endData) } - probeProcess.stderr.on('data',function(data){ - stderr += data.toString() + return new Promise((resolve) => { + activeProbes[auth] = 1 + var stderr = '' + var stdout = '' + const probeCommand = splitForFFPMEG(`${customInput ? customInput + ' ' : ''}-analyzeduration 10000 -probesize 10000 -v quiet -print_format json -show_format -show_streams -i "${url}"`) + var processTimeout = null + var ffprobeLocation = config.ffmpegDir.split('/') + ffprobeLocation[ffprobeLocation.length - 1] = 'ffprobe' + ffprobeLocation = ffprobeLocation.join('/') + const probeProcess = spawn(ffprobeLocation, probeCommand) + const finishReponse = () => { + delete(activeProbes[auth]) + if(!stdout){ + endData.error = stderr + }else{ + endData.ok = true + endData.result = s.parseJSON(stdout) + } + endData.probe = probeCommand + if(callback)callback(endData) + resolve(endData) + } + probeProcess.stderr.on('data',function(data){ + stderr += data.toString() + }) + probeProcess.stdout.on('data',function(data){ + stdout += data.toString() + }) + probeProcess.on('close',function(){ + clearTimeout(processTimeout) + finishReponse() + }) + processTimeout = setTimeout(() => { + treekill(probeProcess.pid) + finishReponse() + },4000) }) - probeProcess.stdout.on('data',function(data){ - stdout += data.toString() - }) - probeProcess.on('close',function(){ - clearTimeout(processTimeout) - finishReponse() - }) - processTimeout = setTimeout(() => { - treekill(probeProcess.pid) - finishReponse() - },4000) } const probeMonitor = (monitor,timeoutAmount,forceOverlap) => { return new Promise((resolve,reject) => { @@ -130,7 +145,11 @@ module.exports = (s,config,lang) => { }else{ const details = s.parseJSON(configPartial.details) Object.keys(details).forEach((key) => { - activeMonitor.details[key] = details[key] + try{ + activeMonitor.details[key] = details[key] + }catch(err){ + console.log(err) + } }) } }) @@ -155,11 +174,11 @@ module.exports = (s,config,lang) => { } return sanitizedCmd } - const createPipeArray = function(e){ + const createPipeArray = function(e, amountToAdd){ const stdioPipes = []; - var times = config.pipeAddition; - if(e.details.stream_channels){ - times+=e.details.stream_channels.length + var times = amountToAdd ? amountToAdd + config.pipeAddition : config.pipeAddition; + if(e.details && e.details.stream_channels){ + times += e.details.stream_channels.length } for(var i=0; i < times; i++){ stdioPipes.push('pipe') diff --git a/libs/fileBin.js b/libs/fileBin.js index 455fc6d1..acac006e 100644 --- a/libs/fileBin.js +++ b/libs/fileBin.js @@ -163,11 +163,34 @@ module.exports = function(s,config,lang,app,io){ }) }) } + function archiveFileBinEntry(file,unarchive){ + return new Promise((resolve) => { + s.knexQuery({ + action: "update", + table: 'Files', + update: { + archive: unarchive ? '0' : 1 + }, + where: { + ke: file.ke, + mid: file.mid, + name: file.name, + } + },function(err){ + resolve({ + ok: !err, + err: err, + archived: !unarchive + }) + }) + }) + } s.getFileBinDirectory = getFileBinDirectory s.getFileBinEntry = getFileBinEntry s.insertFileBinEntry = insertFileBinEntry s.updateFileBinEntry = updateFileBinEntry s.deleteFileBinEntry = deleteFileBinEntry + s.archiveFileBinEntry = archiveFileBinEntry /** * API : Get fileBin file rows */ @@ -187,6 +210,7 @@ module.exports = function(s,config,lang,app,io){ startTimeOperator: req.query.startOperator, endTimeOperator: req.query.endOperator, limit: req.query.limit, + archived: req.query.archived, endIsStartTo: true, noFormat: true, noCount: true, @@ -196,7 +220,7 @@ module.exports = function(s,config,lang,app,io){ },(response) => { response.forEach(function(v){ v.details = s.parseJSON(v.details) - v.href = '/'+req.params.auth+'/fileBin/'+req.params.ke+'/'+req.params.id+'/'+v.details.year+'/'+v.details.month+'/'+v.details.day+'/'+v.name; + v.href = '/'+req.params.auth+'/fileBin/'+req.params.ke+'/'+req.params.id+'/'+v.name; }) s.closeJsonResponse(res,{ ok: true, @@ -208,55 +232,124 @@ module.exports = function(s,config,lang,app,io){ /** * API : Get fileBin file */ - app.get([ - config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:file', - config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:year/:month/:day/:file', - ], async (req,res) => { + app.get(config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:file', async (req,res) => { s.auth(req.params,function(user){ var failed = function(){ res.end(user.lang['File Not Found']) } - if (!s.group[req.params.ke].fileBin[req.params.id+'/'+req.params.file]){ - const groupKey = req.params.ke - const monitorId = req.params.id - const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) - if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ - s.closeJsonResponse(res,{ - ok: false, - msg: lang['Not Permitted'] - }) - return - } - s.knexQuery({ - action: "select", - columns: "*", - table: "Files", - where: [ - ['ke','=',groupKey], - ['mid','=',req.params.id], - ['name','=',req.params.file], - monitorRestrictions - ] - },(err,r) => { - if(r && r[0]){ - r = r[0] - r.details = JSON.parse(r.details) - const filePath = s.dir.fileBin + req.params.ke + '/' + req.params.id + (r.details.year && r.details.month && r.details.day ? '/' + r.details.year + '/' + r.details.month + '/' + r.details.day : '') + '/' + req.params.file; - fs.stat(filePath,function(err,stats){ - if(!err){ + const groupKey = req.params.ke + const monitorId = req.params.id + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_video_view`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return + } + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: [ + ['ke','=',groupKey], + ['mid','=',req.params.id], + ['name','=',req.params.file], + monitorRestrictions + ] + },(err,r) => { + if(r && r[0]){ + r = r[0] + r.details = JSON.parse(r.details) + const filename = req.params.file + const filePath = s.dir.fileBin + req.params.ke + '/' + req.params.id + (r.details.year && r.details.month && r.details.day ? '/' + r.details.year + '/' + r.details.month + '/' + r.details.day : '') + '/' + filename; + fs.stat(filePath,function(err,stats){ + if(!err){ + if(filename.endsWith('.mp4')){ + s.streamMp4FileOverHttp(filePath,req,res,!!req.query.pureStream) + }else{ res.on('finish',function(){res.end()}) fs.createReadStream(filePath).pipe(res) - }else{ - failed() } - }) - }else{ - failed() - } - }) - }else{ - res.end(user.lang['Please Wait for Completion']) - } + }else{ + failed() + } + }) + }else{ + failed() + } + }) },res,req); }); + /** + * API : Modify fileBin File + */ + app.get(config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:file/:mode', function (req,res){ + let response = { ok: false }; + res.setHeader('Content-Type', 'application/json'); + s.auth(req.params,function(user){ + const monitorId = req.params.id + const groupKey = req.params.ke + const filename = req.params.file + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_video_delete`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return + } + s.knexQuery({ + action: "select", + columns: "*", + table: 'Files', + where: [ + ['ke','=',groupKey], + ['mid','=',monitorId], + ['name','=',filename] + ], + limit: 1 + },async (err,r) => { + if(r && r[0]){ + const file = r[0]; + var details = s.parseJSON(r.details) || {} + switch(req.params.mode){ + case'archive': + response.ok = true + const unarchive = s.getPostData(req,'unarchive') == '1'; + const archiveResponse = await archiveFileBinEntry(file,unarchive) + response.ok = archiveResponse.ok + response.archived = archiveResponse.archived + break; + case'delete': + response.ok = true; + await s.deleteFileBinEntry(file) + break; + default: + response.msg = user.lang.modifyVideoText1; + break; + } + }else{ + response.msg = user.lang['No such file']; + } + s.closeJsonResponse(res,response); + }) + },res,req); + }) } diff --git a/libs/health.js b/libs/health.js index 31d2755e..a0ad9eb7 100644 --- a/libs/health.js +++ b/libs/health.js @@ -55,6 +55,9 @@ module.exports = function(s,config,lang,io){ exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ if(s.isWin===true){ d=d.replace(/(\r\n|\n|\r)/gm,"").replace(/%/g,"") + }else if(s.platform == 'darwin') { + // on macos the cpu% is per core, so a 10 core machine can go up to 1000% + d=parseFloat(d.trim()) / s.coreCount } resolve(d) s.onGetCpuUsageExtensions.forEach(function(extender){ @@ -100,18 +103,31 @@ module.exports = function(s,config,lang,io){ k.cmd = "LANG=C free | grep Mem | awk '{print $7/$2 * 100.0}'"; break; } + let percent = 0 + let used = 0 if(k.cmd){ - exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true){ - d=(parseInt(d.split('=')[1])/(s.totalmem/1000))*100 - } - resolve(d) - s.onGetRamUsageExtensions.forEach(function(extender){ - extender(d) - }) - }) + exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true){ + const freeMb = parseInt(d.split('=')[1].trim()) / 1024 + const totalMemInMb = s.totalmem/1024/1024 + used = totalMemInMb - freeMb + percent=(used/totalMemInMb)*100 + } else { + percent = d.trim() + } + resolve({ + used, + percent + }) + s.onGetRamUsageExtensions.forEach(function(extender){ + extender(percent) + }) + }) }else{ - resolve(0) + resolve({ + used, + percent + }) } }) } diff --git a/libs/health/utils.js b/libs/health/utils.js index 98eecdfc..07f36c99 100644 --- a/libs/health/utils.js +++ b/libs/health/utils.js @@ -16,14 +16,11 @@ const currentCPUInfo = { total: 0, active: 0 } -const lastCPUInfo = { +let lastCPUInfo = { total: 0, active: 0 } exports.getCpuUsageOnLinux = () => { - lastCPUInfo.active = currentCPUInfo.active; - lastCPUInfo.idle = currentCPUInfo.idle; - lastCPUInfo.total = currentCPUInfo.total; return new Promise((resolve,reject) => { const getUsage = function(callback){ fs.readFile("/proc/stat" ,'utf8', function(err, data){ @@ -36,6 +33,7 @@ exports.getCpuUsageOnLinux = () => { } currentCPUInfo.active = currentCPUInfo.total - currentCPUInfo.idle currentCPUInfo.percentUsed = calculateCPUPercentage(lastCPUInfo, currentCPUInfo); + lastCPUInfo = Object.assign({},currentCPUInfo) callback(currentCPUInfo.percentUsed) }) } diff --git a/libs/language.js b/libs/language.js index ced395a0..512b4617 100644 --- a/libs/language.js +++ b/libs/language.js @@ -3,17 +3,23 @@ module.exports = function(s,config){ if(!config.language){ config.language='en_CA' } - try{ - var lang = require(s.location.languages+'/'+config.language+'.json'); - }catch(er){ - console.error(er) - console.log('There was an error loading your language file.') - var lang = require(s.location.languages+'/en_CA.json'); + var lang = {}; + function getLanguageData(choice){ + let gotLang = {} + try{ + eval(`gotLang = ${fs.readFileSync(s.location.languages+'/'+choice+'.json','utf8')}`) + }catch(er){ + console.error(er) + console.log('There was an error loading your language file.') + eval(`gotLang = ${fs.readFileSync(s.location.languages+'/en_CA.json','utf8')}`) + } + return gotLang } + lang = getLanguageData(config.language) //load languages dynamically s.copySystemDefaultLanguage = function(){ //en_CA - return Object.assign(lang,{}) + return Object.assign({},getLanguageData(config.language)) } s.listOfPossibleLanguages = [] fs.readdirSync(s.mainDirectory + '/languages').forEach(function(filename){ @@ -28,12 +34,15 @@ module.exports = function(s,config){ s.getLanguageFile = function(rule){ if(rule && rule !== ''){ var file = s.loadedLanguages[file] + s.debugLog(file) if(!file){ try{ - s.loadedLanguages[rule] = require(s.location.languages+'/'+rule+'.json') - s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),s.loadedLanguages[rule]) + let newLang = {} + eval(`newLang = ${fs.readFileSync(s.location.languages+'/'+rule+'.json','utf8')}`) + s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),newLang) file = s.loadedLanguages[rule] }catch(err){ + console.error(err) file = s.copySystemDefaultLanguage() } } @@ -42,5 +51,9 @@ module.exports = function(s,config){ } return file } + s.reloadLanguages = function(){ + s.loadedLanguages = {}; + s.loadedLanguages[config.language] = s.copySystemDefaultLanguage() + } return lang } diff --git a/libs/monitor.js b/libs/monitor.js index 0d57dbb4..d7009374 100644 --- a/libs/monitor.js +++ b/libs/monitor.js @@ -5,16 +5,22 @@ const exec = require('child_process').exec; const Mp4Frag = require('mp4frag'); const onvif = require("shinobi-onvif"); const treekill = require('tree-kill'); -const request = require('request'); const connectionTester = require('connection-tester') const SoundDetection = require('shinobi-sound-detection') -const async = require("async"); const URL = require('url') const { Worker } = require('worker_threads'); const { copyObject, createQueue, queryStringToObject, createQueryStringFromObject } = require('./common.js') module.exports = function(s,config,lang){ + const { fetchTimeout } = require('./basic/utils.js')(process.cwd(),config) + const isMasterNode = ( + ( + config.childNodes.enabled === true && + config.childNodes.mode === 'master' + ) || + config.childNodes.enabled === false + ); const { probeMonitor, getStreamInfoFromProbe, @@ -27,6 +33,11 @@ module.exports = function(s,config,lang){ processKill, cameraDestroy, monitorConfigurationMigrator, + attachStreamChannelHandlers, + setActiveViewer, + getActiveViewerCount, + destroySubstreamProcess, + attachMainProcessHandlers, } = require('./monitor/utils.js')(s,config,lang) const { addEventDetailsToString, @@ -37,15 +48,21 @@ module.exports = function(s,config,lang){ setPresetForCurrentPosition } = require('./control/ptz.js')(s,config,lang) const { - scanForOrphanedVideos + scanForOrphanedVideos, + reEncodeVideoAndBinOriginalAddToQueue, } = require('./video/utils.js')(s,config,lang) + const { + selectNodeForOperation, + bindMonitorToChildNode + } = require('./childNode/utils.js')(s,config,lang) const startMonitorInQueue = createQueue(1, 3) s.initiateMonitorObject = function(e){ if(!s.group[e.ke]){s.group[e.ke]={}}; if(!s.group[e.ke].activeMonitors){s.group[e.ke].activeMonitors={}} if(!s.group[e.ke].activeMonitors[e.mid]){s.group[e.ke].activeMonitors[e.mid]={}} const activeMonitor = s.group[e.ke].activeMonitors[e.mid] - + activeMonitor.ke = e.ke + activeMonitor.mid = e.mid if(!activeMonitor.streamIn){activeMonitor.streamIn={}}; if(!activeMonitor.emitterChannel){activeMonitor.emitterChannel={}}; if(!activeMonitor.mp4frag){activeMonitor.mp4frag={}}; @@ -53,7 +70,7 @@ module.exports = function(s,config,lang){ if(!activeMonitor.contentWriter){activeMonitor.contentWriter={}}; if(!activeMonitor.childNodeStreamWriters){activeMonitor.childNodeStreamWriters={}}; if(!activeMonitor.eventBasedRecording){activeMonitor.eventBasedRecording={}}; - if(!activeMonitor.watch){activeMonitor.watch={}}; + if(!activeMonitor.watch){activeMonitor.watch = []}; if(!activeMonitor.fixingVideos){activeMonitor.fixingVideos={}}; // if(!activeMonitor.viewerConnection){activeMonitor.viewerConnection={}}; // if(!activeMonitor.viewerConnectionCount){activeMonitor.viewerConnectionCount=0}; @@ -138,7 +155,7 @@ module.exports = function(s,config,lang){ return x.ar; } s.getStreamsDirectory = (monitor) => { - return s.dir.streams + monitor.ke + '/' + monitor.mid + '/' + return s.dir.streams + monitor.ke + '/' + (monitor.mid || monitor.id) + '/' } s.getRawSnapshotFromMonitor = function(monitor,options){ return new Promise((resolve,reject) => { @@ -185,7 +202,7 @@ module.exports = function(s,config,lang){ var snapBuffer = [] var temporaryImageFile = streamDir + s.gid(5) + '.jpg' var iconImageFile = streamDir + 'icon.jpg' - var ffmpegCmd = splitForFFPMEG(`-loglevel warning -re -probesize 100000 -analyzeduration 100000 ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -vf "fps=1" -vframes 1 "${temporaryImageFile}"`) + var ffmpegCmd = splitForFFPMEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`) checkExists(streamDir, function(success) { if (success === false) { fs.mkdirSync(streamDir, {recursive: true}, (err) => {s.debugLog(err)}) @@ -500,20 +517,7 @@ module.exports = function(s,config,lang){ s.checkDetails(e) if(e.ke && config.doSnapshot === true){ if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){ - if(s.group[e.ke].activeMonitors[e.mid].onvifConnection){ - const screenShot = await s.getSnapshotFromOnvif({ - username: onvifUsername, - password: onvifPassword, - uri: cameraResponse.uri, - }); - s.tx({ - f: 'monitor_snapshot', - snapshot: screenShot.toString('base64'), - snapshot_format: 'b64', - mid: e.mid, - ke: e.ke - },'GRP_'+e.ke) - }else{ + async function getRaw(){ var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/' const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options) if(screenShot){ @@ -527,7 +531,27 @@ module.exports = function(s,config,lang){ }else{ s.debugLog('Damaged Snapshot Data') s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - } + } + } + if(s.group[e.ke].activeMonitors[e.mid].onvifConnection){ + try{ + const screenShot = await s.getSnapshotFromOnvif({ + ke: e.ke, + mid: e.mid, + }); + s.tx({ + f: 'monitor_snapshot', + snapshot: screenShot.toString('base64'), + snapshot_format: 'b64', + mid: e.mid, + ke: e.ke + },'GRP_'+e.ke) + }catch(err){ + s.debugLog(err) + await getRaw() + } + }else{ + await getRaw() } }else{ s.tx({f:'monitor_snapshot',snapshot:'Disabled',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) @@ -587,7 +611,7 @@ module.exports = function(s,config,lang){ } const createTimelapseDirectory = function(e,callback){ var directory = s.getTimelapseFrameDirectory(e) - fs.mkdir(directory,function(err){ + fs.mkdir(directory,{ recursive: true },function(err){ s.handleFolderError(err) callback(err,directory) }) @@ -752,55 +776,8 @@ module.exports = function(s,config,lang){ code: e.wantedStatusCode }); //on unexpected exit restart - s.group[e.ke].activeMonitors[e.id].spawn_exit = function(){ - if(s.group[e.ke].activeMonitors[e.id].isStarted === true){ - if(e.details.loglevel!=='quiet'){ - s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang.unexpectedExitText,cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}}); - } - fatalError(e,'Process Unexpected Exit'); - scanForOrphanedVideos(e,{ - forceCheck: true, - checkMax: 2 - }) - s.onMonitorUnexpectedExitExtensions.forEach(function(extender){ - extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) - }) - } - } - s.group[e.ke].activeMonitors[e.id].spawn.on('end',s.group[e.ke].activeMonitors[e.id].spawn_exit) - s.group[e.ke].activeMonitors[e.id].spawn.on('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit) - s.group[e.ke].activeMonitors[e.id].spawn.on('error',function(er){ - s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error') - }) - s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}}) - if(s.isWin === false){ - var strippedHost = s.stripAuthFromHost(e) - var sendProcessCpuUsage = function(){ - s.getMonitorCpuUsage(e,function(percent){ - s.group[e.ke].activeMonitors[e.id].currentCpuUsage = percent - s.tx({ - f: 'camera_cpu_usage', - ke: e.ke, - id: e.id, - percent: percent - },'MON_STREAM_'+e.ke+e.id) - }) - } - clearInterval(s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage) - s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage = setInterval(function(){ - if(e.details.skip_ping !== '1'){ - connectionTester.test(strippedHost,e.port,2000,function(err,response){ - if(response.success){ - sendProcessCpuUsage() - }else{ - launchMonitorProcesses(e) - } - }) - }else{ - sendProcessCpuUsage() - } - },1000 * 60) - } + if(s.group[e.ke].activeMonitors[e.id].spawn)attachMainProcessHandlers(e,fatalError) + return s.group[e.ke].activeMonitors[e.id].spawn } const createEventCounter = function(monitor){ if(monitor.details.detector_obj_count === '1'){ @@ -923,42 +900,16 @@ module.exports = function(s,config,lang){ //frames from motion detect if(e.details.detector_pam === '1'){ // s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].pipe(s.group[e.ke].activeMonitors[e.id].p2p).pipe(s.group[e.ke].activeMonitors[e.id].pamDiff) - s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].on('data',function(buf){ - let theJson - try{ - buf.toString().split('}{').forEach((object,n)=>{ - theJson = object - if(object.substr(object.length - 1) !== '}')theJson += '}' - if(object.substr(0,1) !== '{')theJson = '{' + theJson - try{ - var data = JSON.parse(theJson) - }catch(err){ - var data = JSON.parse(theJson + '}') - } - switch(data.f){ - case'trigger': - triggerEvent(data) - break; - case's.tx': - s.tx(data.data,data.to) - break; - } - }) - }catch(err){ - console.log(theJson) - console.log('There was an error parsing a detector event') - console.log(err) - } - }) + // spawn.stdio[3] is deprecated and now motion events are handled by dataPort if(e.details.detector_use_detect_object === '1' && e.details.detector_use_motion === '1' ){ s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){ onDetectorJpegOutputSecondary(e,data) }) }else{ - s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){ + s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){ onDetectorJpegOutputAlone(e,data) }) - } + } }else if(e.details.detector_use_detect_object === '1' && e.details.detector_send_frames !== '1'){ s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){ onDetectorJpegOutputSecondary(e,data) @@ -970,8 +921,9 @@ module.exports = function(s,config,lang){ } } //frames to stream - var frameToStreamPrimary - switch(e.details.stream_type){ + var frameToStreamPrimary; + const streamType = e.details.stream_type; + switch(streamType){ case'mp4': delete(s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN']) if(!s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'])s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'] = new Mp4Frag() @@ -996,12 +948,6 @@ module.exports = function(s,config,lang){ s.group[e.ke].activeMonitors[e.id].emitter.emit('data',d) } break; - case'h265': - frameToStreamPrimary = function(d){ - resetStreamCheck(e) - s.group[e.ke].activeMonitors[e.id].emitter.emit('data',d) - } - break; case'b64':case undefined:case null:case'': var buffer frameToStreamPrimary = function(d){ @@ -1018,50 +964,28 @@ module.exports = function(s,config,lang){ } break; } + s.onMonitorCreateStreamPipeExtensions.forEach(function(extender){ + if(!frameToStreamPrimary)frameToStreamPrimary = extender(streamType,e,resetStreamCheck) + }); if(frameToStreamPrimary){ s.group[e.ke].activeMonitors[e.id].spawn.stdout.on('data',frameToStreamPrimary) } if(e.details.stream_channels && e.details.stream_channels !== ''){ - var createStreamEmitter = function(channel,number){ - var pipeNumber = number+config.pipeAddition; - if(!s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber]){ - s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber] = new events.EventEmitter().setMaxListeners(0); - } - var frameToStreamAdded - switch(channel.stream_type){ - case'mp4': - delete(s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber]) - if(!s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber])s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber] = new Mp4Frag(); - s.group[e.ke].activeMonitors[e.id].spawn.stdio[pipeNumber].pipe(s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber],{ end: false }) - break; - case'mjpeg': - frameToStreamAdded = function(d){ - s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d) - } - break; - case'flv': - frameToStreamAdded = function(d){ - if(!s.group[e.ke].activeMonitors[e.id].firstStreamChunk[pipeNumber])s.group[e.ke].activeMonitors[e.id].firstStreamChunk[pipeNumber] = d; - frameToStreamAdded = function(d){ - s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d) - } - frameToStreamAdded(d) - } - break; - case'h264': - frameToStreamAdded = function(d){ - s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d) - } - break; - } - if(frameToStreamAdded){ - s.group[e.ke].activeMonitors[e.id].spawn.stdio[pipeNumber].on('data',frameToStreamAdded) - } - } - e.details.stream_channels.forEach(createStreamEmitter) + e.details.stream_channels.forEach((fields,number) => { + attachStreamChannelHandlers({ + ke: e.ke, + mid: e.id, + fields: fields, + number: number, + ffmpegProcess: s.group[e.ke].activeMonitors[e.id].spawn, + }) + }) } } const catchNewSegmentNames = function(e){ + const monitorConfig = s.group[e.ke].rawMonitorConfigurations[e.id] + const monitorDetails = monitorConfig.details + const autoCompressionEnabled = monitorDetails.auto_compress_videos === '1' var checkLog = function(d,x){return d.indexOf(x)>-1} s.group[e.ke].activeMonitors[e.id].spawn.stdio[8].on('data',function(d){ d=d.toString(); @@ -1070,7 +994,7 @@ module.exports = function(s,config,lang){ s.insertCompletedVideo(e,{ file: filename, events: s.group[e.ke].activeMonitors[e.id].detector_motion_count - },function(err){ + },function(err,response){ s.userLog(e,{type:lang['Video Finished'],msg:{filename:d}}) if( e.details.detector === '1' && @@ -1088,6 +1012,21 @@ module.exports = function(s,config,lang){ ke : e.ke, id : e.id }) + }else if(autoCompressionEnabled){ + s.debugLog('Queue Automatic Compression',response.insertQuery) + reEncodeVideoAndBinOriginalAddToQueue({ + video: response.insertQuery, + targetVideoCodec: 'vp9', + targetAudioCodec: 'libopus', + targetQuality: '-q:v 1 -q:a 1', + targetExtension: 'webm', + doSlowly: false, + automated: true, + }).then((encodeResponse) => { + s.debugLog('Complete Automatic Compression',encodeResponse) + }).catch((err) => { + console.log(err) + }) } s.group[e.ke].activeMonitors[e.id].detector_motion_count = [] }) @@ -1158,11 +1097,11 @@ module.exports = function(s,config,lang){ s.group[e.ke].activeMonitors[monitorId].detector_notrigger_webhook = s.createTimeout('detector_notrigger_webhook',s.group[e.ke].activeMonitors[monitorId],currentConfig.detector_notrigger_webhook_timeout,10) var detector_notrigger_webhook_url = addEventDetailsToString(e,currentConfig.detector_notrigger_webhook_url) var webhookMethod = currentConfig.detector_notrigger_webhook_method - if(!webhookMethod || webhookMethod === '')webhookMethod = 'GET' - request(detector_notrigger_webhook_url,{method: webhookMethod,encoding:null},function(err,data){ - if(err){ - s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}}) - } + if(!webhookMethod || webhookMethod === '')webhookMethod = 'GET'; + fetchTimeout(detector_notrigger_webhook_url,10000,{ + method: webhookMethod + }).catch((err) => { + s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}}) }) } if(currentConfig.detector_notrigger_command_enable === '1' && !s.group[e.ke].activeMonitors[monitorId].detector_notrigger_command){ @@ -1278,27 +1217,29 @@ module.exports = function(s,config,lang){ if(pingResponse.success === true){ activeMonitor.isRecording = true try{ - createCameraFfmpegProcess(e) - createCameraStreamHandlers(e) + var mainProcess = createCameraFfmpegProcess(e) createEventCounter(e) - if(e.type === 'dashcam' || e.type === 'socket'){ - setTimeout(function(){ - activeMonitor.allowStdinWrite = true - s.txToDashcamUsers({ - f : 'enable_stream', - ke : e.ke, - mid : e.id - },e.ke) - },30000) - } - if( - e.functionMode === 'record' || - e.type === 'mjpeg' || - e.type === 'h264' || - e.type === 'local' - ){ - catchNewSegmentNames(e) - cameraFilterFfmpegLog(e) + if(mainProcess){ + createCameraStreamHandlers(e) + if(e.type === 'dashcam' || e.type === 'socket'){ + setTimeout(function(){ + activeMonitor.allowStdinWrite = true + s.txToDashcamUsers({ + f : 'enable_stream', + ke : e.ke, + mid : e.id + },e.ke) + },30000) + } + if( + e.functionMode === 'record' || + e.type === 'mjpeg' || + e.type === 'h264' || + e.type === 'local' + ){ + catchNewSegmentNames(e) + cameraFilterFfmpegLog(e) + } } clearTimeout(activeMonitor.onMonitorStartTimer) activeMonitor.onMonitorStartTimer = setTimeout(() => { @@ -1349,6 +1290,8 @@ module.exports = function(s,config,lang){ //data, options d : s.group[e.ke].rawMonitorConfigurations[e.id] },activeMonitor.childNodeId) + clearTimeout(activeMonitor.recordingChecker); + clearTimeout(activeMonitor.streamChecker); } if( e.type !== 'socket' && @@ -1364,37 +1307,21 @@ module.exports = function(s,config,lang){ } try{ if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){ - var copiedMonitorObject = s.cleanMonitorObject(s.group[e.ke].rawMonitorConfigurations[e.id]) - var childNodeList = Object.keys(s.childNodes) - if(childNodeList.length > 0){ - e.childNodeFound = false - var selectNode = function(ip){ - e.childNodeFound = true - e.childNodeSelected = ip - } - var nodeWithLowestActiveCamerasCount = 65535 - var nodeWithLowestActiveCameras = null - childNodeList.forEach(function(ip){ - delete(s.childNodes[ip].activeCameras[e.ke+e.id]) - var nodeCameraCount = Object.keys(s.childNodes[ip].activeCameras).length - if(!s.childNodes[ip].dead && nodeCameraCount < nodeWithLowestActiveCamerasCount && s.childNodes[ip].cpu < 75){ - nodeWithLowestActiveCamerasCount = nodeCameraCount - nodeWithLowestActiveCameras = ip - } - }) - if(nodeWithLowestActiveCameras)selectNode(nodeWithLowestActiveCameras) - if(e.childNodeFound === true){ - s.childNodes[e.childNodeSelected].activeCameras[e.ke+e.id] = copiedMonitorObject - activeMonitor.childNode = e.childNodeSelected - activeMonitor.childNodeId = s.childNodes[e.childNodeSelected].cnid; - s.cx({f:'sync',sync:s.group[e.ke].rawMonitorConfigurations[e.id],ke:e.ke,mid:e.id},activeMonitor.childNodeId); + selectNodeForOperation({ + ke: e.ke, + mid: e.id, + }).then((selectedNode) => { + if(selectedNode){ + bindMonitorToChildNode({ + ke: e.ke, + mid: e.id, + childNodeId: selectedNode, + }) doOnChildMachine() }else{ startMonitorInQueue.push(doOnThisMachine,function(){}) } - }else{ - startMonitorInQueue.push(doOnThisMachine,function(){}) - } + }); }else{ startMonitorInQueue.push(doOnThisMachine,function(){}) } @@ -1568,26 +1495,23 @@ module.exports = function(s,config,lang){ s.initiateMonitorObject({ke:e.ke,mid:e.id}) switch(e.functionMode){ case'watch_on'://live streamers - join - if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}} - if(!cn.monitorsCurrentlyWatching[e.id]){cn.monitorsCurrentlyWatching[e.id]={ke:e.ke}} - s.group[e.ke].activeMonitors[e.id].watch[cn.id]={}; - var numberOfViewers = Object.keys(s.group[e.ke].activeMonitors[e.id].watch).length - s.tx({ - viewers: numberOfViewers, - ke: e.ke, - id: e.id - },'MON_'+e.ke+e.id) + if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}} + if(!cn.monitorsCurrentlyWatching[e.id]){cn.monitorsCurrentlyWatching[e.id]={ke:e.ke}} + setActiveViewer(e.ke,e.id,cn.id,true) + s.group[e.ke].activeMonitors[e.id].allowDestroySubstream = false + clearTimeout(s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream) break; case'watch_off'://live streamers - leave if(cn.monitorsCurrentlyWatching){delete(cn.monitorsCurrentlyWatching[e.id])} - var numberOfViewers = 0 - delete(s.group[e.ke].activeMonitors[e.id].watch[cn.id]); - numberOfViewers = Object.keys(s.group[e.ke].activeMonitors[e.id].watch).length - s.tx({ - viewers: numberOfViewers, - ke: e.ke, - id: e.id - },'MON_'+e.ke+e.id) + setActiveViewer(e.ke,e.id,cn.id,false) + clearTimeout(s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream) + s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream = setTimeout(async () => { + let currentCount = getActiveViewerCount(e.ke,e.id) + if(currentCount === 0 && s.group[e.ke].activeMonitors[e.id].subStreamProcess){ + s.group[e.ke].activeMonitors[e.id].allowDestroySubstream = true + await destroySubstreamProcess(s.group[e.ke].activeMonitors[e.id]) + } + },10000) break; case'restart'://restart monitor s.sendMonitorStatus({ @@ -1605,13 +1529,13 @@ module.exports = function(s,config,lang){ if(!s.group[e.ke]||!s.group[e.ke].activeMonitors[e.id]){return} if(config.childNodes.enabled === true && config.childNodes.mode === 'master' && s.group[e.ke].activeMonitors[e.id].childNode && s.childNodes[s.group[e.ke].activeMonitors[e.id].childNode].activeCameras[e.ke+e.id]){ s.group[e.ke].activeMonitors[e.id].isStarted = false + s.cx({f:'sync',sync:s.group[e.ke].rawMonitorConfigurations[e.id],ke:e.ke,mid:e.id},s.group[e.ke].activeMonitors[e.id].childNodeId); s.cx({ //function f : 'cameraStop', //data, options d : s.group[e.ke].rawMonitorConfigurations[e.id] },s.group[e.ke].activeMonitors[e.id].childNodeId) - s.cx({f:'sync',sync:s.group[e.ke].rawMonitorConfigurations[e.id],ke:e.ke,mid:e.id},s.group[e.ke].activeMonitors[e.id].childNodeId); }else{ closeEventBasedRecording(e) if(s.group[e.ke].activeMonitors[e.id].fswatch){s.group[e.ke].activeMonitors[e.id].fswatch.close();delete(s.group[e.ke].activeMonitors[e.id].fswatch)} @@ -1653,15 +1577,17 @@ module.exports = function(s,config,lang){ status: wantedStatus, code: wantedStatusCode, }) - setTimeout(() => { - scanForOrphanedVideos({ - ke: e.ke, - mid: e.id, - },{ - forceCheck: true, - checkMax: 2 - }) - },2000) + if(isMasterNode){ + setTimeout(() => { + scanForOrphanedVideos({ + ke: e.ke, + mid: e.id, + },{ + forceCheck: true, + checkMax: 2 + }) + },2000) + } clearTimeout(s.group[e.ke].activeMonitors[e.id].onMonitorStartTimer) s.onMonitorStopExtensions.forEach(function(extender){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) @@ -1676,6 +1602,15 @@ module.exports = function(s,config,lang){ //stop action, monitor already started or recording return } + if(activeMonitor.masterSaysToStop === true){ + s.sendMonitorStatus({ + id: e.id, + ke: e.ke, + status: lang.Stopped, + code: 5, + }) + return; + } if(config.probeMonitorOnStart === true){ const probeResponse = await probeMonitor(s.group[e.ke].rawMonitorConfigurations[e.id],2000,true) const probeStreams = getStreamInfoFromProbe(probeResponse.result) @@ -1856,6 +1791,7 @@ module.exports = function(s,config,lang){ monitorRestrictions.push(['or','mid','=',v]) } }) + console.log(monitorRestrictions) }catch(er){ } }else if( @@ -1873,20 +1809,104 @@ module.exports = function(s,config,lang){ ){} return monitorRestrictions } - // s.checkViewerConnectionsForMonitor = function(monitorObject){ - // var monitorConfig = s.group[monitorObject.ke].rawMonitorConfigurations[monitorObject.mid] - // if(monitorConfig.mode === 'start'){ - // - // } - // } - // s.addViewerConnectionForMonitor = function(monitorObject,viewerDetails){ - // s.group[monitorObject.ke].activeMonitors[monitorObject.mid].viewerConnection[viewerDetails.viewerId] = viewerDetails - // s.group[monitorObject.ke].activeMonitors[monitorObject.mid].viewerConnectionCount += 1 - // return s.group[monitorObject.ke].activeMonitors[monitorObject.mid].viewerConnectionCount - // } - // s.removeViewerConnectionForMonitor = function(monitorObject,viewerDetails){ - // delete(s.group[monitorObject.ke].activeMonitors[monitorObject.mid].viewerConnection[viewerDetails.viewerId]) - // s.group[monitorObject.ke].activeMonitors[monitorObject.mid].viewerConnectionCount -= 1 - // return s.group[monitorObject.ke].activeMonitors[monitorObject.mid].viewerConnectionCount - // } + s.checkPermission = (user) => { + // provide "user" object given from "s.auth" + const isSubAccount = !!user.details.sub + const response = { + isSubAccount, + hasAllPermissions: isSubAccount && user.details.allmonitors === '1', + isRestricted: isSubAccount && user.details.allmonitors !== '1', + isRestrictedApiKey: false, + apiKeyPermissions: {}, + userPermissions: {}, + } + const permissions = user.permissions + const details = user.details; + [ + 'auth_socket', + 'get_monitors', + 'control_monitors', + 'get_logs', + 'watch_stream', + 'watch_snapshot', + 'watch_videos', + 'delete_videos', + ].forEach((key) => { + const permissionOff = permissions[key] === '0'; + response.apiKeyPermissions[key] = permissions[key] === '1'; + response.apiKeyPermissions[`${key}_disallowed`] = permissionOff; + response.isRestrictedApiKey = response.isRestrictedApiKey || permissionOff; + }); + // Base Level Permissions + // allmonitors : All Monitors and Privileges + // monitor_create : Can Create and Delete Monitors + // user_change : Can Change User Settings + // view_logs : Can View Logs + [ + 'allmonitors', + 'monitor_create', + 'user_change', + 'view_logs', + ].forEach((key) => { + response.userPermissions[key] = details[key] === '1' || !details[key]; + response.userPermissions[`${key}_disallowed`] = details[key] === '0'; + }); + return response + } + s.getMonitorsPermitted = (userDetails,monitorId) => { + const monitorRestrictions = [] + const monitors = {} + function setMonitorPermissions(mid){ + // monitors : Can View Monitor + // monitor_edit : Can Edit Monitor (Delete as well) + // video_view : Can View Videos and Events + // video_delete : Can Delete Videos and Events + [ + 'monitors', + 'monitor_edit', + 'video_view', + 'video_delete', + ].forEach((key) => { + monitors[`${mid}_${key}`] = userDetails[key] && userDetails[key].indexOf(mid) > -1 || false; + }); + return true + } + function addToQuery(mid,n){ + if(n === 0){ + monitorRestrictions.push(['mid','=',mid]) + }else{ + monitorRestrictions.push(['or','mid','=',mid]) + } + }; + if( + !monitorId && + userDetails.sub && + userDetails.monitors && + userDetails.allmonitors !== '1' + ){ + try{ + userDetails.monitors = s.parseJSON(userDetails.monitors) + userDetails.monitors.forEach(function(v,n){ + setMonitorPermissions(v) + addToQuery(v,n) + }) + }catch(err){ + s.debugLog(err) + } + }else if( + monitorId && ( + !userDetails.sub || + userDetails.allmonitors !== '0' || + userDetails.monitors.indexOf(monitorId) >- 1 + ) + ){ + setMonitorPermissions(monitorId) + addToQuery(monitorId,0) + } + return { + monitorPermissions: monitors, + // queryConditions + monitorRestrictions: monitorRestrictions, + } + } } diff --git a/libs/monitor/utils.js b/libs/monitor/utils.js index cfcf8446..4e66e162 100644 --- a/libs/monitor/utils.js +++ b/libs/monitor/utils.js @@ -1,21 +1,39 @@ const fs = require('fs'); const treekill = require('tree-kill'); const spawn = require('child_process').spawn; +const events = require('events'); +const Mp4Frag = require('mp4frag'); +const streamViewerCountTimeouts = {} module.exports = (s,config,lang) => { const { + scanForOrphanedVideos + } = require('../video/utils.js')(s,config,lang) + const { + createPipeArray, splitForFFPMEG, + sanitizedFfmpegCommand, } = require('../ffmpeg/utils.js')(s,config,lang) + const { + buildSubstreamString, + getDefaultSubstreamFields, + } = require('../ffmpeg/builders.js')(s,config,lang) const getUpdateableFields = require('./updatedFields.js') const processKill = (proc) => { const response = {ok: true} return new Promise((resolve,reject) => { + if(!proc){ + resolve(response) + return + } function sendError(err){ response.ok = false response.err = err resolve(response) } try{ - proc.stdin.write("q\r\n") + if(proc && proc.stdin) { + proc.stdin.write("q\r\n"); + } setTimeout(() => { if(proc && proc.kill){ if(s.isWin){ @@ -86,13 +104,17 @@ module.exports = (s,config,lang) => { if(activeMonitor.onChildNodeExit){ activeMonitor.onChildNodeExit() } - activeMonitor.spawn.stdio.forEach(function(stdio){ - try{ - stdio.unpipe() - }catch(err){ - console.log(err) - } - }) + try{ + activeMonitor.spawn.stdio.forEach(function(stdio){ + try{ + stdio.unpipe() + }catch(err){ + console.log(err) + } + }) + }catch(err){ + // s.debugLog(err) + } if(activeMonitor.mp4frag){ var mp4FragChannels = Object.keys(activeMonitor.mp4frag) mp4FragChannels.forEach(function(channel){ @@ -108,6 +130,10 @@ module.exports = (s,config,lang) => { }else{ processKill(proc).then((response) => { s.debugLog(`cameraDestroy`,response) + activeMonitor.allowDestroySubstream = true + destroySubstreamProcess(activeMonitor).then((response) => { + if(response.hadSubStream)s.debugLog(`cameraDestroy`,response.closeResponse) + }) }) } } @@ -136,7 +162,7 @@ module.exports = (s,config,lang) => { }) } const temporaryImageFile = streamDir + s.gid(5) + '.jpg' - const ffmpegCmd = splitForFFPMEG(`-loglevel warning -re -stimeout 30000000 -probesize 100000 -analyzeduration 100000 ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -vf "fps=1" -vframes 1 "${temporaryImageFile}"`) + const ffmpegCmd = splitForFFPMEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`) const snapProcess = spawn('ffmpeg',ffmpegCmd,{detached: true}) snapProcess.stderr.on('data',function(data){ // s.debugLog(data.toString()) @@ -195,11 +221,367 @@ module.exports = (s,config,lang) => { } }) } + const spawnSubstreamProcess = function(e){ + // e = monitorConfig + try{ + const groupKey = e.ke + const monitorId = e.mid + const monitorConfig = Object.assign({},s.group[groupKey].rawMonitorConfigurations[monitorId]) + const monitorDetails = monitorConfig.details + const activeMonitor = s.group[e.ke].activeMonitors[e.mid] + const channelNumber = 1 + (monitorDetails.stream_channels || []).length + const ffmpegCommand = [`-progress pipe:5`]; + const logLevel = monitorDetails.loglevel ? e.details.loglevel : 'warning' + const stdioPipes = createPipeArray({}, 2) + const substreamConfig = monitorConfig.details.substream + substreamConfig.input.type = !substreamConfig.input.fulladdress ? monitorConfig.type : substreamConfig.input.type || monitorConfig.details.rtsp_transport + substreamConfig.input.fulladdress = substreamConfig.input.fulladdress || s.buildMonitorUrl(monitorConfig) + substreamConfig.input.rtsp_transport = substreamConfig.input.rtsp_transport || monitorConfig.details.rtsp_transport + const { + inputAndConnectionFields, + outputFields, + } = getDefaultSubstreamFields(monitorConfig); + ([ + buildSubstreamString(channelNumber + config.pipeAddition,e), + ]).forEach(function(commandStringPart){ + ffmpegCommand.push(commandStringPart) + }); + const ffmpegCommandString = ffmpegCommand.join(' ') + activeMonitor.ffmpegSubstream = sanitizedFfmpegCommand(e,ffmpegCommandString) + const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString) + activeMonitor.subStreamChannel = channelNumber; + s.userLog({ + ke: e.ke, + mid: e.mid, + }, + { + type: lang["Substream Process"], + msg: { + msg: lang["Process Started"], + cmd: ffmpegCommandString, + }, + }); + const subStreamProcess = spawn(config.ffmpegDir,ffmpegCommandParsed,{detached: true,stdio: stdioPipes}) + attachStreamChannelHandlers({ + ke: e.ke, + mid: e.mid, + fields: Object.assign({},inputAndConnectionFields,outputFields), + number: activeMonitor.subStreamChannel, + ffmpegProcess: subStreamProcess, + }) + if(config.debugLog === true){ + subStreamProcess.stderr.on('data',(data) => { + console.log(`${e.ke} ${e.mid}`) + console.log(data.toString()) + }) + } + if(logLevel !== 'quiet'){ + subStreamProcess.stderr.on('data',(data) => { + s.userLog({ + ke: e.ke, + mid: e.mid, + },{ + type: lang["Substream Process"], + msg: data.toString() + }) + }) + } + subStreamProcess.on('close',(data) => { + if(!activeMonitor.allowDestroySubstream){ + subStreamProcess.stderr.on('data',(data) => { + s.userLog({ + ke: e.ke, + mid: e.mid, + }, + { + type: lang["Substream Process"], + msg: lang["Process Crashed for Monitor"], + }) + }) + setTimeout(() => { + spawnSubstreamProcess(e) + },2000) + } + }) + activeMonitor.subStreamProcess = subStreamProcess + s.tx({ + f: 'substream_start', + mid: e.mid, + ke: e.ke, + channel: activeMonitor.subStreamChannel + },'GRP_'+e.ke); + return subStreamProcess + }catch(err){ + s.systemLog(err) + return null + } + } + const destroySubstreamProcess = async function(activeMonitor){ + // e = monitorConfig.details.substream + const response = { + hadSubStream: false, + alreadyClosing: false + } + try{ + if(activeMonitor.subStreamProcessClosing){ + response.alreadyClosing = true + }else if(activeMonitor.subStreamProcess){ + activeMonitor.subStreamProcessClosing = true + activeMonitor.subStreamChannel = null; + const closeResponse = await processKill(activeMonitor.subStreamProcess) + response.hadSubStream = true + response.closeResponse = closeResponse + delete(activeMonitor.subStreamProcess) + s.tx({ + f: 'substream_end', + mid: activeMonitor.mid, + ke: activeMonitor.ke + },'GRP_'+activeMonitor.ke); + activeMonitor.subStreamProcessClosing = false + } + }catch(err){ + s.debugLog('destroySubstreamProcess',err) + } + return response + } + function attachStreamChannelHandlers(options){ + const fields = options.fields + const number = options.number + const ffmpegProcess = options.ffmpegProcess + const activeMonitor = s.group[options.ke].activeMonitors[options.mid] + const pipeNumber = number + config.pipeAddition; + if(!activeMonitor.emitterChannel[pipeNumber]){ + activeMonitor.emitterChannel[pipeNumber] = new events.EventEmitter().setMaxListeners(0); + } + let frameToStreamAdded + switch(fields.stream_type){ + case'mp4': + delete(activeMonitor.mp4frag[pipeNumber]) + if(!activeMonitor.mp4frag[pipeNumber])activeMonitor.mp4frag[pipeNumber] = new Mp4Frag(); + ffmpegProcess.stdio[pipeNumber].pipe(activeMonitor.mp4frag[pipeNumber],{ end: false }) + break; + case'mjpeg': + frameToStreamAdded = function(d){ + activeMonitor.emitterChannel[pipeNumber].emit('data',d) + } + break; + case'flv': + frameToStreamAdded = function(d){ + if(!activeMonitor.firstStreamChunk[pipeNumber])activeMonitor.firstStreamChunk[pipeNumber] = d; + frameToStreamAdded = function(d){ + activeMonitor.emitterChannel[pipeNumber].emit('data',d) + } + frameToStreamAdded(d) + } + break; + case'h264': + frameToStreamAdded = function(d){ + activeMonitor.emitterChannel[pipeNumber].emit('data',d) + } + break; + } + if(frameToStreamAdded){ + ffmpegProcess.stdio[pipeNumber].on('data',frameToStreamAdded) + } + } + function setActiveViewer(groupKey,monitorId,connectionId,isBeingAdded){ + const viewerList = s.group[groupKey].activeMonitors[monitorId].watch; + if(isBeingAdded){ + if(viewerList.indexOf(connectionId) > -1)viewerList.push(connectionId); + }else{ + viewerList.splice(viewerList.indexOf(connectionId), 1) + } + const numberOfViewers = viewerList.length + s.tx({ + f: 'viewer_count', + viewers: numberOfViewers, + ke: groupKey, + id: monitorId + },'MON_' + groupKey + monitorId) + return numberOfViewers; + } + function getActiveViewerCount(groupKey,monitorId){ + const viewerList = s.group[groupKey].activeMonitors[monitorId].watch; + const numberOfViewers = viewerList.length + return numberOfViewers; + } + function setTimedActiveViewerForHttp(req){ + const groupKey = req.params.ke + const connectionId = req.params.auth + const loggedInUser = s.group[groupKey].users[connectionId] + if(!loggedInUser){ + const monitorId = req.params.id + const viewerList = s.group[groupKey].activeMonitors[monitorId].watch + const theViewer = viewerList[connectionId] + if(!theViewer){ + setActiveViewer(groupKey,monitorId,connectionId,true) + } + clearTimeout(streamViewerCountTimeouts[req.originalUrl]) + streamViewerCountTimeouts[req.originalUrl] = setTimeout(() => { + setActiveViewer(groupKey,monitorId,connectionId,false) + },5000) + }else{ + s.debugLog(`User is Logged in, Don't add to viewer count`); + } + } + function attachMainProcessHandlers(e,fatalError){ + s.group[e.ke].activeMonitors[e.id].spawn_exit = function(){ + if(s.group[e.ke].activeMonitors[e.id].isStarted === true){ + if(e.details.loglevel!=='quiet'){ + s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang.unexpectedExitText,cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}}); + } + fatalError(e,'Process Unexpected Exit'); + scanForOrphanedVideos(e,{ + forceCheck: true, + checkMax: 2 + }) + s.onMonitorUnexpectedExitExtensions.forEach(function(extender){ + extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) + }) + } + } + s.group[e.ke].activeMonitors[e.id].spawn.on('end',s.group[e.ke].activeMonitors[e.id].spawn_exit) + s.group[e.ke].activeMonitors[e.id].spawn.on('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit) + s.group[e.ke].activeMonitors[e.id].spawn.on('error',function(er){ + s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error') + }) + s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}}) + // if(s.isWin === false){ + // var strippedHost = s.stripAuthFromHost(e) + // var sendProcessCpuUsage = function(){ + // s.getMonitorCpuUsage(e,function(percent){ + // s.group[e.ke].activeMonitors[e.id].currentCpuUsage = percent + // s.tx({ + // f: 'camera_cpu_usage', + // ke: e.ke, + // id: e.id, + // percent: percent + // },'MON_STREAM_'+e.ke+e.id) + // }) + // } + // clearInterval(s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage) + // s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage = setInterval(function(){ + // if(e.details.skip_ping !== '1'){ + // connectionTester.test(strippedHost,e.port,2000,function(err,response){ + // if(response.success){ + // sendProcessCpuUsage() + // }else{ + // launchMonitorProcesses(e) + // } + // }) + // }else{ + // sendProcessCpuUsage() + // } + // },1000 * 60) + // } + } + async function deleteMonitorData(groupKey,monitorId){ + // deleteVideos + // deleteFileBinFiles + // deleteTimelapseFrames + async function deletePath(thePath){ + try{ + await fs.promises.stat(thePath) + await fs.promises.rm(thePath, {recursive: true}) + }catch(err){ + + } + } + async function deleteFromTable(tableName){ + await s.knexQueryPromise({ + action: "delete", + table: tableName, + where: { + ke: groupKey, + mid: monitorId, + } + }) + } + async function getSizeFromTable(tableName){ + const response = await s.knexQueryPromise({ + action: "select", + columns: "size", + table: tableName, + where: { + ke: groupKey, + mid: monitorId, + } + }) + const rows = response.rows + let size = 0 + for (let i = 0; i < rows.length; i++) { + const row = rows[i] + size += row.size + } + return size + } + async function adjustSpaceCounterForTableWithAddStorage(tableName,storageType){ + // does normal videos and addStorage + const response = await s.knexQueryPromise({ + action: "select", + columns: "ke,mid,details,size", + table: tableName || 'Videos', + where: { + ke: groupKey, + mid: monitorId, + } + }) + const rows = response.rows + for (let i = 0; i < rows.length; i++) { + const video = rows[i] + const storageIndex = s.getVideoStorageIndex(video) + if(storageIndex){ + s.setDiskUsedForGroupAddStorage(video.ke,{ + size: -(video.size / 1048576), + storageIndex: storageIndex + },storageType) + }else{ + s.setDiskUsedForGroup(video.ke,-(video.size / 1048576),storageType) + } + } + } + async function adjustSpaceCounter(tableName,storageType){ + const amount = await getSizeFromTable(tableName) + s.setDiskUsedForGroup(groupKey,-amount,storageType) + } + const videosDir = s.dir.videos + `${groupKey}/${monitorId}` + const binDir = s.dir.fileBin + `${groupKey}/${monitorId}` + + // videos and addStorage + await adjustSpaceCounterForTableWithAddStorage('Timelapse Frames','timelapeFrames') + await adjustSpaceCounterForTableWithAddStorage('Videos') + await deleteFromTable('Videos') + await deletePath(videosDir) + for (let i = 0; i < s.dir.addStorage.length; i++) { + const storage = s.dir.addStorage[i] + const addStorageDir = storage.path + groupKey + '/' + monitorId + await deletePath(addStorageDir) + await deletePath(addStorageDir + '_timelapse') + } + + // timelapse frames + await adjustSpaceCounter('Timelapse Frames','timelapeFrames') + await deleteFromTable('Timelapse Frames') + await deletePath(videosDir + '_timelapse') + + // fileBin + await adjustSpaceCounter('Files','fileBin') + await deleteFromTable('Files') + await deletePath(binDir) + } return { + deleteMonitorData, cameraDestroy: cameraDestroy, createSnapshot: createSnapshot, processKill: processKill, addCredentialsToStreamLink: addCredentialsToStreamLink, monitorConfigurationMigrator: monitorConfigurationMigrator, + spawnSubstreamProcess: spawnSubstreamProcess, + destroySubstreamProcess: destroySubstreamProcess, + attachStreamChannelHandlers: attachStreamChannelHandlers, + setActiveViewer: setActiveViewer, + getActiveViewerCount: getActiveViewerCount, + setTimedActiveViewerForHttp: setTimedActiveViewerForHttp, + attachMainProcessHandlers: attachMainProcessHandlers, } } diff --git a/libs/notification.js b/libs/notification.js index 3db2b604..296f462a 100644 --- a/libs/notification.js +++ b/libs/notification.js @@ -10,8 +10,18 @@ module.exports = function(s,config,lang){ d.screenshotBuffer = screenShot } } - require('./notifications/email.js')(s,config,lang,getSnapshot) + if( + config.mail && + config.mail.auth && + config.mail.auth.user !== 'your_email@gmail.com' && + config.mail.auth.pass !== 'your_password_or_app_specific_password' + ){ + require('./notifications/email.js')(s,config,lang,getSnapshot) + } + require('./notifications/emailByUser.js')(s,config,lang,getSnapshot) require('./notifications/discordBot.js')(s,config,lang,getSnapshot) require('./notifications/telegram.js')(s,config,lang,getSnapshot) require('./notifications/pushover.js')(s,config,lang,getSnapshot) + require('./notifications/webhook.js')(s,config,lang,getSnapshot) + require('./notifications/mqtt.js')(s,config,lang,getSnapshot) } diff --git a/libs/notifications/discordBot.js b/libs/notifications/discordBot.js index 336177c5..0f39e60a 100644 --- a/libs/notifications/discordBot.js +++ b/libs/notifications/discordBot.js @@ -48,14 +48,14 @@ module.exports = function(s,config,lang,getSnapshot){ } } const onEventTriggerBeforeFilterForDiscord = function(d,filter){ - filter.discord = true + filter.discord = false } const onEventTriggerForDiscord = async (d,filter) => { const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id] // d = event object //discord bot - const isEnabled = monitorConfig.details.detector_discordbot === '1' || monitorConfig.details.notify_discord === '1' - if(filter.discord && s.group[d.ke].discordBot && isEnabled && !s.group[d.ke].activeMonitors[d.id].detector_discordbot){ + const isEnabled = filter.discord || monitorConfig.details.detector_discordbot === '1' || monitorConfig.details.notify_discord === '1' + if(s.group[d.ke].discordBot && isEnabled && !s.group[d.ke].activeMonitors[d.id].detector_discordbot){ var detector_discordbot_timeout if(!monitorConfig.details.detector_discordbot_timeout||monitorConfig.details.detector_discordbot_timeout===''){ detector_discordbot_timeout = 1000 * 60 * 10; @@ -66,6 +66,28 @@ module.exports = function(s,config,lang,getSnapshot){ clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_discordbot); s.group[d.ke].activeMonitors[d.id].detector_discordbot = null },detector_discordbot_timeout) + await getSnapshot(d,monitorConfig) + if(d.screenshotBuffer){ + sendMessage({ + author: { + name: s.group[d.ke].rawMonitorConfigurations[d.id].name, + icon_url: config.iconURL + }, + title: lang.Event+' - '+d.screenshotName, + description: lang.EventText1+' '+d.currentTimestamp, + fields: [], + timestamp: d.currentTime, + footer: { + icon_url: config.iconURL, + text: "Shinobi Systems" + } + },[ + { + attachment: d.screenshotBuffer, + name: d.screenshotName+'.jpg' + } + ],d.ke) + } if(monitorConfig.details.detector_discordbot_send_video === '1'){ let videoPath = null let videoName = null @@ -102,28 +124,6 @@ module.exports = function(s,config,lang,getSnapshot){ ],d.ke) } } - await getSnapshot(d,monitorConfig) - if(d.screenshotBuffer){ - sendMessage({ - author: { - name: s.group[d.ke].rawMonitorConfigurations[d.id].name, - icon_url: config.iconURL - }, - title: lang.Event+' - '+d.screenshotName, - description: lang.EventText1+' '+d.currentTimestamp, - fields: [], - timestamp: d.currentTime, - footer: { - icon_url: config.iconURL, - text: "Shinobi Systems" - } - },[ - { - attachment: d.screenshotBuffer, - name: d.screenshotName+'.jpg' - } - ],d.ke) - } } } const onTwoFactorAuthCodeNotificationForDiscord = function(r){ @@ -366,6 +366,25 @@ module.exports = function(s,config,lang,getSnapshot){ } ] } + s.definitions["Event Filters"].blocks["Action for Selected"].info.push({ + "name": "actions=discord", + "field": lang['Discord'], + "fieldType": "select", + "form-group-class": "actions-row", + "default": "", + "example": "1", + "possible": [ + { + "name": lang['Original Choice'], + "value": "", + "selected": true + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }) }catch(err){ console.log(err) console.log('Could not start Discord bot, please run "npm install discord.js" inside the Shinobi folder.') diff --git a/libs/notifications/email.js b/libs/notifications/email.js index 6d9f6c70..172aee73 100644 --- a/libs/notifications/email.js +++ b/libs/notifications/email.js @@ -29,22 +29,22 @@ module.exports = function(s,config,lang,getSnapshot){ ] },(err,r) => { r = r[0] - var mailOptions = { - from: config.mail.from, // sender address - to: checkEmail(r.mail), // list of receivers - subject: lang.NoMotionEmailText1+' '+e.name+' ('+e.id+')', // Subject line - html: ''+lang.NoMotionEmailText2+' ' + (e.details.detector_notrigger_timeout || 10) + ' '+lang.minutes+'.', + var mailOptions = { + from: config.mail.from, // sender address + to: checkEmail(r.mail), // list of receivers + subject: lang.NoMotionEmailText1+' '+e.name+' ('+e.id+')', // Subject line + html: ''+lang.NoMotionEmailText2+' ' + (e.details.detector_notrigger_timeout || 10) + ' '+lang.minutes+'.', + } + mailOptions.html+='
'+lang['Monitor Name']+' : '+e.name+'
' + mailOptions.html+='
'+lang['Monitor ID']+' : '+e.id+'
' + sendMessage(mailOptions, (error, info) => { + if (error) { + s.systemLog('detector:notrigger:sendMail',error) + s.tx({f:'error',ff:'detector_notrigger_mail',id:e.id,ke:e.ke,error:error},'GRP_'+e.ke); + return ; } - mailOptions.html+='
'+lang['Monitor Name']+' : '+e.name+'
' - mailOptions.html+='
'+lang['Monitor ID']+' : '+e.id+'
' - sendMessage(mailOptions, (error, info) => { - if (error) { - s.systemLog('detector:notrigger:sendMail',error) - s.tx({f:'error',ff:'detector_notrigger_mail',id:e.id,ke:e.ke,error:error},'GRP_'+e.ke); - return ; - } - s.tx({f:'detector_notrigger_mail',id:e.id,ke:e.ke,info:info},'GRP_'+e.ke); - }) + s.tx({f:'detector_notrigger_mail',id:e.id,ke:e.ke,info:info},'GRP_'+e.ke); + }) }) } } @@ -94,16 +94,11 @@ module.exports = function(s,config,lang,getSnapshot){ } } const onEventTriggerBeforeFilterForEmail = function(d,filter){ - const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id] - if(monitorConfig.details.detector_mail === '1'){ - filter.mail = true - }else{ - filter.mail = false - } + filter.mail = false } const onEventTriggerForEmail = async (d,filter) => { const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id] - if(filter.mail && config.mail && !s.group[d.ke].activeMonitors[d.id].detector_mail){ + if((filter.mail || monitorConfig.details.detector_mail === '1') && config.mail && !s.group[d.ke].activeMonitors[d.id].detector_mail){ s.knexQuery({ action: "select", columns: "mail", @@ -154,6 +149,13 @@ module.exports = function(s,config,lang,getSnapshot){ } }) } + await getSnapshot(d,monitorConfig) + sendMail([ + { + filename: d.screenshotName + '.jpg', + content: d.screenshotBuffer + } + ]) if(monitorConfig.details.detector_mail_send_video === '1'){ let videoPath = null let videoName = null @@ -193,13 +195,6 @@ module.exports = function(s,config,lang,getSnapshot){ }) } } - await getSnapshot(d,monitorConfig) - sendMail([ - { - filename: d.screenshotName + '.jpg', - content: d.screenshotBuffer - } - ]) }) } } @@ -245,6 +240,112 @@ module.exports = function(s,config,lang,getSnapshot){ s.onFilterEvent(onFilterEventForEmail) s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeoutForEmail) s.onMonitorUnexpectedExit(onMonitorUnexpectedExitForEmail) + s.definitions['Account Settings'].blocks['2-Factor Authentication'].info.push( { + "name": "detail=factor_mail", + "field": `${lang.Email} (${lang['System Level']})`, + "description": "Send 2-Factor Authentication codes to the email address of the account.", + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }); + s.definitions["Event Filters"].blocks["Action for Selected"].info.push( { + "name": "actions=mail", + "field": `${lang['Email on Trigger']} (${lang['System Level']})`, + "fieldType": "select", + "form-group-class": "actions-row", + "default": "", + "example": "1", + "possible": [ + { + "name": lang['Original Choice'], + "value": "", + "selected": true + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }) + s.definitions['Monitor Settings'].blocks['Notifications'].info[0].info.push( + { + "name": "detail=notify_email", + "field": `${lang.Email} (${lang['System Level']})`, + "default": "0", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + ) + s.definitions['Monitor Settings'].blocks['Notifications'].info.push( + { + isFormGroupGroup: true, + name: `${lang.Email} (${lang['System Level']})`, + color: 'blue', + 'section-class': 'h_det_input h_det_1', + info: [ + { + "name": "detail=detector_mail", + "field": lang['Email on Trigger'], + "description": "Recieve an email of an image during a motion event to the master account for the camera group. You must setup SMTP details in conf.json.", + "default": "0", + "selector": "h_det_email", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "name": "detail=detector_mail_timeout", + "field": lang['Allow Next Email'], + "description": "The amount of time until a trigger is allowed to send another email with motion details and another image.", + "default": "10", + }, + { + "name": "detail=detector_notrigger_mail", + "field": lang['No Trigger'], + "description": "If motion has not been detected after the timeout period you will recieve an email.", + "default": "0", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + ], + } + ); }catch(err){ console.log(err) } diff --git a/libs/notifications/emailByUser.js b/libs/notifications/emailByUser.js new file mode 100644 index 00000000..6f78f0c1 --- /dev/null +++ b/libs/notifications/emailByUser.js @@ -0,0 +1,404 @@ +var fs = require('fs'); +const { + template, + checkEmail, +} = require("./emailUtils.js") +module.exports = function (s, config, lang, getSnapshot) { + const { getEventBasedRecordingUponCompletion } = require('../events/utils.js')(s, config, lang); + const nodeMailer = require('nodemailer'); + try { + const sendMessage = async function (sendBody, files, groupKey) { + const transporter = s.group[groupKey].emailClient; + if (!transporter) { + s.userLog( + { ke: groupKey, mid: '$USER' }, + { + type: lang.NotifyErrorText, + msg: { + msg: lang.AppNotEnabledText, + app: lang.Email + }, + } + ); + return; + } + try { + const emailClientOptions = s.group[groupKey].emailClientOptions; + const appOptions = emailClientOptions.transport; + const sendTo = emailClientOptions.sendTo; + sendTo.forEach((reciepientAddress) => { + const sendData = { + from: `"${config.mailFromName || 'shinobi.video'}" <${appOptions.auth.user}>`, + to: reciepientAddress, + subject: sendBody.subject, + html: sendBody.html, + attachments: files || [] + }; + transporter.sendMail(sendData, function (err, result) { + if (err) { + throw err; + } + s.userLog(result); + s.debugLog(result); + }); + }) + console.log(sendBody) + } catch (err) { + s.debugLog(err) + s.userLog( + { ke: groupKey, mid: '$USER' }, + { type: lang.NotifyErrorText, msg: err } + ); + } + }; + + const loadAppForUser = function (user) { + const userDetails = s.parseJSON(user.details); + const optionsHost = userDetails.emailClient_host + const optionsUser = userDetails.emailClient_user + const optionsSendTo = userDetails.emailClient_sendTo || '' + if ( + !s.group[user.ke].emailClient && + userDetails.emailClient === '1' && + optionsHost && + optionsUser && + optionsSendTo + ){ + const optionsPass = userDetails.emailClient_pass || '' + const optionsSecure = userDetails.emailClient_secure === '1' ? true : false + const optionsPort = isNaN(userDetails.emailClient_port) ? (optionsSecure ? 465 : 587) : parseInt(userDetails.emailClient_port) + const clientOptions = { + host: optionsHost, + port: optionsPort, + secure: optionsSecure, + auth: { + user: optionsUser, + pass: optionsPass + } + } + s.group[user.ke].emailClientOptions = { + transport: clientOptions, + sendTo: optionsSendTo.split(',').map((text) => {return text.trim()}), + } + s.group[user.ke].emailClient = nodeMailer.createTransport(clientOptions) + } + }; + + const unloadAppForUser = function (user) { + if ( + s.group[user.ke].emailClient && + s.group[user.ke].emailClient.close + ) { + s.group[user.ke].emailClient.close(); + } + delete s.group[user.ke].emailClient; + delete s.group[user.ke].emailClientOptions; + }; + + const onTwoFactorAuthCodeNotificationForApp = function (r) { + // r = user + if (r.details.factor_emailClient === '1') { + sendMessage({ + subject: r.lang['2-Factor Authentication'], + html: template.createFramework({ + title: r.lang['2-Factor Authentication'], + subtitle: r.lang['Enter this code to proceed'], + body: ''+s.factorAuth[r.ke][r.uid].key+'

'+r.lang.FactorAuthText1, + }), + },[],r.ke); + } + }; + + const onEventTriggerForApp = async (d, filter) => { + const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id]; + // d = event object + if ( + s.group[d.ke].emailClient && + (filter.emailClient || monitorConfig.details.notify_emailClient === '1') && + !s.group[d.ke].activeMonitors[d.id].detector_emailClient + ) { + var detector_emailClient_timeout; + if (!monitorConfig.details.detector_emailClient_timeout){ + detector_emailClient_timeout = 1000 * 60 * 10 + }else{ + detector_emailClient_timeout = parseFloat(monitorConfig.details.detector_emailClient_timeout) * 1000 * 60 + } + s.group[d.ke].activeMonitors[d.id].detector_emailClient = setTimeout(function () { + s.group[d.ke].activeMonitors[d.id].detector_emailClient = null; + }, detector_emailClient_timeout); + + // lock passed + const sendMail = function(files){ + const infoRows = [] + Object.keys(d.details).forEach(function(key){ + var value = d.details[key] + var text = value + if(value instanceof Object){ + text = JSON.stringify(value,null,3) + } + infoRows.push(template.createRow({ + title: key, + text: text + })) + }) + sendMessage({ + subject: lang.Event+' - '+d.screenshotName, + html: template.createFramework({ + title: lang.EventText1 + ' ' + d.currentTimestamp, + subtitle: lang.Event, + body: infoRows.join(''), + }), + },files || [],d.ke) + } + await getSnapshot(d,monitorConfig) + sendMail([ + { + filename: d.screenshotName + '.jpg', + content: d.screenshotBuffer + } + ]) + if(monitorConfig.details.detector_mail_send_video === '1'){ + let videoPath = null + let videoName = null + const eventBasedRecording = await getEventBasedRecordingUponCompletion({ + ke: d.ke, + mid: d.mid + }) + if(eventBasedRecording.filePath){ + videoPath = eventBasedRecording.filePath + videoName = eventBasedRecording.filename + }else{ + const siftedVideoFileFromRam = await s.mergeDetectorBufferChunks(d) + videoPath = siftedVideoFileFromRam.filePath + videoName = siftedVideoFileFromRam.filename + } + if(videoPath){ + fs.readFile(mergedFilepath,function(err,buffer){ + if(buffer){ + sendMail([ + { + filename: videoName, + content: buffer + } + ]) + } + }) + } + } + } + }; + + const onEventTriggerBeforeFilterForApp = function (d, filter) { + filter.emailClient = false; + }; + + const onDetectorNoTriggerTimeoutForApp = function (e) { + //e = monitor object + var currentTime = new Date(); + if (e.details.detector_notrigger_emailClient === '1') { + var html = + '*' + + lang.NoMotionEmailText2 + + ' ' + + (e.details.detector_notrigger_timeout || 10) + + ' ' + + lang.minutes + + '.*\n'; + html += + '**' + lang['Monitor Name'] + '** : ' + e.name + '\n'; + html += '**' + lang['Monitor ID'] + '** : ' + e.id + '\n'; + html += currentTime; + sendMessage({ + subject: lang['"No Motion" Detector'], + html: template.createFramework({ + title: lang['"No Motion" Detector'], + subtitle: 'Shinobi Event', + body: html, + }), + },[],e.ke); + } + }; + + const onMonitorUnexpectedExitForApp = (monitorConfig) => { + if ( + monitorConfig.details.notify_emailClient === '1' && + monitorConfig.details.notify_onUnexpectedExit === '1' + ){ + const ffmpegCommand = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].ffmpeg + const subject = lang['Process Unexpected Exit'] + ' : ' + monitorConfig.name + const currentTime = new Date(); + sendMessage({ + subject: subject, + html: template.createFramework({ + title: subject, + subtitle: lang['Process Crashed for Monitor'], + body: ffmpegCommand, + footerText: currentTime + }), + },[],monitorConfig.ke); + } + }; + + s.loadGroupAppExtender(loadAppForUser); + s.unloadGroupAppExtender(unloadAppForUser); + s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotificationForApp); + s.onEventTrigger(onEventTriggerForApp); + s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilterForApp); + s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeoutForApp); + s.onMonitorUnexpectedExit(onMonitorUnexpectedExitForApp); + s.definitions['Monitor Settings'].blocks['Notifications'].info[0].info.push({ + name: 'detail=notify_emailClient', + field: lang.Email, + description: '', + default: '0', + example: '', + selector: 'h_det_emailClient', + fieldType: 'select', + possible: [ + { + name: lang.No, + value: '0', + }, + { + name: lang.Yes, + value: '1', + }, + ], + }); + s.definitions['Monitor Settings'].blocks['Notifications'].info.push( + { + evaluation: "$user.details.use_emailClient !== '0'", + isFormGroupGroup: true, + name: lang.Email, + color: 'blue', + 'section-class': 'h_det_emailClient_input h_det_emailClient_1', + info: [ + { + name: 'detail=detector_emailClient_timeout', + field: `${lang['Allow Next Alert']} (${lang['on Event']})`, + default: '10', + }, + ], + } + ); + s.definitions['Account Settings'].blocks['2-Factor Authentication'].info.push({ + name: 'detail=factor_emailClient', + field: lang.Email, + default: '1', + example: '', + fieldType: 'select', + possible: [ + { + name: lang.No, + value: '0', + }, + { + name: lang.Yes, + value: '1', + }, + ], + }); + s.definitions['Account Settings'].blocks['Email'] = { + evaluation: "$user.details.use_emailClient !== '0'", + name: lang.Email, + id: lang.Email, + color: 'blue', + info: [ + { + name: 'detail=emailClient', + selector: 'u_emailClient', + field: lang.Enabled, + default: '0', + example: '', + fieldType: 'select', + possible: [ + { + name: lang.No, + value: '0', + }, + { + name: lang.Yes, + value: '1', + }, + ], + }, + { + hidden: true, + field: lang.Host, + name: 'detail=emailClient_host', + example: 'smtp.gmail.com', + 'form-group-class': 'u_emailClient_input u_emailClient_1', + }, + { + hidden: true, + field: lang.Port, + name: 'detail=emailClient_port', + example: '587', + 'form-group-class': 'u_emailClient_input u_emailClient_1', + }, + { + hidden: true, + name: 'detail=emailClient_secure', + 'form-group-class': 'u_emailClient_input u_emailClient_1', + field: lang.Secure, + default: '0', + example: '', + fieldType: 'select', + possible: [ + { + name: lang.No, + value: '0', + }, + { + name: lang.Yes, + value: '1', + }, + ], + }, + { + hidden: true, + field: lang.Email, + name: 'detail=emailClient_user', + example: 'test@gmail.com', + 'form-group-class': 'u_emailClient_input u_emailClient_1', + }, + { + hidden: true, + field: lang.Password, + fieldType: 'password', + name: 'detail=emailClient_pass', + 'form-group-class': 'u_emailClient_input u_emailClient_1', + }, + { + hidden: true, + field: lang['Send to'], + name: 'detail=emailClient_sendTo', + example: 'testrecipient@gmail.com', + 'form-group-class': 'u_emailClient_input u_emailClient_1', + }, + ], + }; + s.definitions["Event Filters"].blocks["Action for Selected"].info.push({ + "name": "actions=emailClient", + "field": lang['Email'], + "fieldType": "select", + "form-group-class": "actions-row", + "default": "", + "example": "1", + "possible": [ + { + "name": lang['Original Choice'], + "value": "", + "selected": true + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }) + } catch (err) { + console.log(err); + console.log('Could not engage Email notifications.'); + } +}; diff --git a/libs/notifications/mqtt.js b/libs/notifications/mqtt.js new file mode 100644 index 00000000..2d801c18 --- /dev/null +++ b/libs/notifications/mqtt.js @@ -0,0 +1,428 @@ +var fs = require("fs") +module.exports = function(s,config,lang,getSnapshot){ + if(config.mqttClient === true){ + console.log('Loading MQTT Outbound Connectivity...') + const mqtt = require('mqtt') + const { + getEventBasedRecordingUponCompletion, + } = require('../events/utils.js')(s,config,lang) + try{ + function createMqttSubscription(options){ + let mqttEndpoint = options.host + const username = options.username || '' + const password = options.password || '' + const subKey = options.subKey + const pubKey = options.pubKey + const groupKey = options.ke + const onData = options.onData || function(){} + function mqttUserLog(type,data){ + s.userLog({ + ke: groupKey, + mid: '$USER' + },{ + type: type, + msg: data + }) + } + if(mqttEndpoint.indexOf('://') === -1){ + mqttEndpoint = `mqtt://${mqttEndpoint}` + } + mqttUserLog('Connecting... ' + mqttEndpoint) + const client = mqtt.connect(mqttEndpoint,{ + clean: true, + username: username, + password: password, + clientId: `shinobi_${Math.random().toString(16).substr(2, 8)}`, + reconnectPeriod: 10000, // 10 seconds + }); + client.on('reconnect', (e) => mqttUserLog(`MQTT Reconnected`)) + client.on('disconnect', (e) => mqttUserLog(`MQTT Disconnected`)) + client.on('offline', (e) => mqttUserLog(`MQTT Offline`)) + client.on('error', (e) => mqttUserLog(`MQTT Error`,e)) + client.on('connect', function () { + mqttUserLog('Connected! ' + mqttEndpoint) + client.subscribe(pubKey, function (err) { + if (err) { + s.debugLog(err) + s.userLog({ + ke: groupKey, + mid: '$USER' + },{ + type: lang['MQTT Error'], + msg: err + }) + }else{ + client.on('message', function (topic, message) { + const data = s.parseJSON(message.toString()) + onData(data) + }) + } + }) + }) + return client + } + function sendToMqttConnections(groupKey,eventName,addedArgs,checkMonitors){ + try{ + (s.group[groupKey].mqttOutbounderKeys || []).forEach(function(key){ + const outBounder = s.group[groupKey].mqttOutbounders[key] + const theAction = outBounder.eventHandlers[eventName] + if(!theAction)return; + if(checkMonitors){ + const monitorsToRead = outBounder.monitorsToRead + const firstArg = addedArgs[0] + const monitorId = firstArg.mid || firstArg.id + if(monitorsToRead.indexOf(monitorId) > -1 || monitorsToRead.indexOf('$all') > -1)theAction(...addedArgs); + }else{ + theAction(...addedArgs) + } + }) + }catch(err){ + s.debugLog(err) + } + } + const sendMessage = async function(options,data){ + const sendBody = s.stringJSON(data) + const groupKey = options.ke + const subId = options.subId + const publishTo = options.to + try{ + s.group[groupKey].mqttOutbounders[subId].client.publish(publishTo,sendBody) + }catch(err){ + s.debugLog('MQTT Error',err) + s.userLog({ke:groupKey,mid:'$USER'},{type:lang['MQTT Error'],msg:err}) + } + } + const onEventTriggerBeforeFilter = function(d,filter){ + filter.mqttout = false + } + const onDetectorNoTriggerTimeout = function(e){ + if(e.details.detector_notrigger_mqttout === '1'){ + const groupKey = e.ke + sendToMqttConnections(groupKey,'onDetectorNoTriggerTimeout',[e],true) + } + } + const onEventTrigger = (d,filter) => { + const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id] + if((filter.mqttout || monitorConfig.details.notify_mqttout === '1') && !s.group[d.ke].activeMonitors[d.id].detector_mqttout){ + var detector_mqttout_timeout + if(!monitorConfig.details.detector_mqttout_timeout||monitorConfig.details.detector_mqttout_timeout===''){ + detector_mqttout_timeout = 1000 * 60 * 10; + }else{ + detector_mqttout_timeout = parseFloat(monitorConfig.details.detector_mqttout_timeout) * 1000 * 60; + } + s.group[d.ke].activeMonitors[d.id].detector_mqttout = setTimeout(function(){ + clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_mqttout); + s.group[d.ke].activeMonitors[d.id].detector_mqttout = null + },detector_mqttout_timeout) + // + const groupKey = d.ke + sendToMqttConnections(groupKey,'onEventTrigger',[d,filter],true) + } + } + const onMonitorSave = (monitorConfig) => { + const groupKey = monitorConfig.ke + sendToMqttConnections(groupKey,'onMonitorSave',[monitorConfig],true) + } + const onMonitorStart = (monitorConfig) => { + const groupKey = monitorConfig.ke + sendToMqttConnections(groupKey,'onMonitorStart',[monitorConfig],true) + } + const onMonitorStop = (monitorConfig) => { + const groupKey = monitorConfig.ke + sendToMqttConnections(groupKey,'onMonitorStop',[monitorConfig],true) + } + const onMonitorDied = (monitorConfig) => { + const groupKey = monitorConfig.ke + sendToMqttConnections(groupKey,'onMonitorDied',[monitorConfig],true) + } + const onAccountSave = (activeGroup,userDetails,user) => { + const groupKey = user.ke + sendToMqttConnections(groupKey,'onAccountSave',[activeGroup,userDetails,user]) + } + const onUserLog = (logEvent) => { + const groupKey = logEvent.ke + if(groupKey.indexOf('$') === -1){ + sendToMqttConnections(groupKey,'onUserLog',[logEvent]) + }else{ + s.debugLog(`Failed sendToMqttConnections onUserLog : ${groupKey}`) + } + } + const onTwoFactorAuthCodeNotification = function(user){ + const groupKey = user.ke + if(user.details.factor_mqttout === '1'){ + sendToMqttConnections(groupKey,'onTwoFactorAuthCodeNotification',[user],true) + } + } + const loadMqttListBotForUser = function(user){ + const groupKey = user.ke + const userDetails = s.parseJSON(user.details); + if(!s.group[groupKey].mqttOutbounders)s.group[groupKey].mqttOutbounders = {}; + const mqttSubs = s.group[groupKey].mqttOutbounders + if(userDetails.mqttout === '1' && Object.keys(mqttSubs).length === 0){ + const mqttClientList = userDetails.mqttout_list || [] + mqttClientList.forEach(function(row,n){ + try{ + const mqttSubId = `${row.host} ${row.pubKey}` + const message = row.type || [] + const eventsToAttachTo = row.msgFor || [] + const monitorsToRead = row.monitors || [] + mqttSubs[mqttSubId] = { + eventHandlers: {} + }; + mqttSubs[mqttSubId].client = createMqttSubscription({ + username: row.username, + password: row.password, + host: row.host, + pubKey: row.pubKey, + ke: groupKey, + }); + const msgOptions = { + ke: groupKey, + subId: mqttSubId, + to: row.pubKey, + } + const titleLegend = { + onMonitorSave: lang['Monitor Edit'], + onMonitorStart: lang['Monitor Start'], + onMonitorStop: lang['Monitor Stop'], + onMonitorDied: lang['Monitor Died'], + onEventTrigger: lang['Event'], + onDetectorNoTriggerTimeout: lang['"No Motion" Detector'], + onAccountSave: lang['Account Save'], + onUserLog: lang['User Log'], + onTwoFactorAuthCodeNotification: lang['2-Factor Authentication'], + } + eventsToAttachTo.forEach(function(eventName){ + let theAction = function(){} + switch(eventName){ + case'onEventTrigger': + theAction = function(d,filter){ + const eventObject = Object.assign({},d) + delete(eventObject.frame); + sendMessage(msgOptions,{ + title: titleLegend[eventName], + name: eventName, + data: eventObject, + time: new Date(), + }) + } + break; + case'onAccountSave': + theAction = function(activeGroup,userDetails,user){ + sendMessage(msgOptions,{ + title: titleLegend[eventName], + name: eventName, + data: { + mail: user.mail, + ke: user.ke, + }, + time: new Date(), + }) + } + break; + case'userLog': + theAction = function(logEvent){ + sendMessage(msgOptions,{ + title: titleLegend[eventName], + name: eventName, + data: logEvent, + time: new Date(), + }) + } + break; + case'onTwoFactorAuthCodeNotification': + theAction = function(user){ + sendMessage(msgOptions,{ + title: titleLegend[eventName], + name: eventName, + data: { + code: s.factorAuth[user.ke][user.uid].key + }, + time: new Date(), + }) + } + break; + case'onMonitorSave': + case'onMonitorStart': + case'onMonitorStop': + case'onMonitorDied': + case'onDetectorNoTriggerTimeout': + theAction = function(monitorConfig){ + //e = monitor object + sendMessage(msgOptions,{ + title: titleLegend[eventName], + name: eventName, + data: { + name: monitorConfig.name, + monitorId: monitorConfig.mid || monitorConfig.id, + }, + time: new Date(), + }) + } + break; + } + mqttSubs[mqttSubId].eventHandlers[eventName] = theAction + }) + mqttSubs[mqttSubId].monitorsToRead = monitorsToRead; + }catch(err){ + s.debugLog(err) + // s.systemLog(err) + } + }) + s.group[groupKey].mqttOutbounderKeys = Object.keys(s.group[groupKey].mqttOutbounders) + }else{ + s.group[groupKey].mqttOutbounderKeys = [] + } + } + const unloadMqttListBotForUser = function(user){ + const groupKey = user.ke + const mqttSubs = s.group[groupKey].mqttOutbounders || {} + Object.keys(mqttSubs).forEach(function(mqttSubId){ + try{ + mqttSubs[mqttSubId].client.end() + }catch(err){ + s.debugLog(err) + // s.userLog({ + // ke: groupKey, + // mid: '$USER' + // },{ + // type: lang['MQTT Error'], + // msg: err + // }) + } + delete(mqttSubs[mqttSubId]) + }) + } + const onBeforeAccountSave = function(data){ + data.d.mqttout_list = [] + } + s.loadGroupAppExtender(loadMqttListBotForUser) + s.unloadGroupAppExtender(unloadMqttListBotForUser) + s.beforeAccountSave(onBeforeAccountSave) + s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotification) + s.onEventTrigger(onEventTrigger) + s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilter) + s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeout) + s.onMonitorSave(onMonitorSave) + s.onMonitorStart(onMonitorStart) + s.onMonitorStop(onMonitorStop) + s.onMonitorDied(onMonitorDied) + s.onUserLog(onUserLog) + s.definitions["Monitor Settings"].blocks["Notifications"].info[0].info.push( + { + "name": "detail=notify_mqttout", + "field": lang['MQTT Outbound'], + "description": "", + "default": "0", + "example": "", + "selector": "h_det_mqttout", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + ) + s.definitions["Monitor Settings"].blocks["Notifications"].info.push({ + "evaluation": "$user.details.use_mqttout !== '0'", + isFormGroupGroup: true, + "name": lang['MQTT Outbound'], + "color": "blue", + "section-class": "h_det_mqttout_input h_det_mqttout_1", + "info": [ + { + "name": "detail=detector_mqttout_timeout", + "field": lang['Allow Next Alert'] + ` (${lang['on Event']})`, + "description": "", + "default": "10", + "example": "", + "possible": "" + }, + ] + }) + s.definitions["Account Settings"].blocks["2-Factor Authentication"].info.push({ + "name": "detail=factor_mqttout", + "field": lang['MQTT Outbound'], + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }) + s.definitions["Account Settings"].blocks["MQTT Outbound"] = { + "evaluation": "$user.details.use_mqttout !== '0'", + "name": lang['MQTT Outbound'], + "color": "blue", + "info": [ + { + "name": "detail=mqttout", + "selector":"u_mqttout", + "field": lang.Enabled, + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + "fieldType": "btn", + "class": `btn-success mqtt-out-add-row`, + "btnContent": `   ${lang['Add']}`, + }, + { + "id": "mqttout_list", + "fieldType": "div", + }, + { + "fieldType": "script", + "src": "assets/js/bs5.mqttOut.js", + } + ] + } + s.definitions["Event Filters"].blocks["Action for Selected"].info.push({ + "name": "actions=mqttout", + "field": lang['MQTT Outbound'], + "fieldType": "select", + "form-group-class": "actions-row", + "default": "", + "example": "1", + "possible": [ + { + "name": lang['Original Choice'], + "value": "", + "selected": true + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }) + }catch(err){ + console.error(err) + console.log('Could not start MQTT Outbound Handling.') + } + } +} diff --git a/libs/notifications/pushover.js b/libs/notifications/pushover.js index c1ba6cc4..e2ac79af 100644 --- a/libs/notifications/pushover.js +++ b/libs/notifications/pushover.js @@ -12,8 +12,8 @@ module.exports = function (s, config, lang, getSnapshot) { s.userLog( { ke: groupKey, mid: '$USER' }, { - type: lang.NotifyErrorText, - msg: lang.DiscordNotEnabledText, + type: lang.PushoverNotifyErrorText, + msg: lang.PushoverNotEnabledText, } ); return; @@ -112,9 +112,8 @@ module.exports = function (s, config, lang, getSnapshot) { s.group[d.ke].rawMonitorConfigurations[d.id]; // d = event object if ( - filter.pushover && s.group[d.ke].pushover && - monitorConfig.details.notify_pushover === '1' && + (filter.pushover || monitorConfig.details.notify_pushover === '1') && !s.group[d.ke].activeMonitors[d.id].detector_pushover ) { var detector_pushover_timeout; @@ -163,7 +162,7 @@ module.exports = function (s, config, lang, getSnapshot) { }; const onEventTriggerBeforeFilterForPushover = function (d, filter) { - filter.pushover = true; + filter.pushover = false; }; const onDetectorNoTriggerTimeoutForPushover = function (e) { @@ -339,6 +338,25 @@ module.exports = function (s, config, lang, getSnapshot) { }, ], }; + s.definitions["Event Filters"].blocks["Action for Selected"].info.push({ + "name": "actions=pushover", + "field": lang['Pushover'], + "fieldType": "select", + "form-group-class": "actions-row", + "default": "", + "example": "1", + "possible": [ + { + "name": lang['Original Choice'], + "value": "", + "selected": true + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }) } catch (err) { console.log(err); console.log( @@ -346,4 +364,4 @@ module.exports = function (s, config, lang, getSnapshot) { ); } } -}; \ No newline at end of file +}; diff --git a/libs/notifications/telegram.js b/libs/notifications/telegram.js index ec029b34..c84629d0 100644 --- a/libs/notifications/telegram.js +++ b/libs/notifications/telegram.js @@ -1,4 +1,11 @@ var fs = require("fs") +// function asyncSetTimeout(timeout){ +// return new Promise((resolve,reject) => { +// setTimeout(() => { +// resolve() +// },timeout || 1000) +// }) +// } module.exports = function(s,config,lang,getSnapshot){ const { getEventBasedRecordingUponCompletion, @@ -30,6 +37,7 @@ module.exports = function(s,config,lang,getSnapshot){ }) } }catch(err){ + s.debugLog('Telegram Error',err) s.userLog({ke:groupKey,mid:'$USER'},{type:lang.NotifyErrorText,msg:err}) } }else{ @@ -43,13 +51,13 @@ module.exports = function(s,config,lang,getSnapshot){ } } const onEventTriggerBeforeFilterForTelegram = function(d,filter){ - filter.telegram = true + filter.telegram = false } const onEventTriggerForTelegram = async (d,filter) => { const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id] // d = event object //telegram bot - if(filter.telegram && s.group[d.ke].telegramBot && monitorConfig.details.notify_telegram === '1' && !s.group[d.ke].activeMonitors[d.id].detector_telegrambot){ + if(s.group[d.ke].telegramBot && (filter.telegram || monitorConfig.details.notify_telegram === '1') && !s.group[d.ke].activeMonitors[d.id].detector_telegrambot){ var detector_telegrambot_timeout if(!monitorConfig.details.detector_telegrambot_timeout||monitorConfig.details.detector_telegrambot_timeout===''){ detector_telegrambot_timeout = 1000 * 60 * 10; @@ -60,7 +68,21 @@ module.exports = function(s,config,lang,getSnapshot){ clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_telegrambot); s.group[d.ke].activeMonitors[d.id].detector_telegrambot = null },detector_telegrambot_timeout) + await getSnapshot(d,monitorConfig) + if(d.screenshotBuffer){ + sendMessage({ + title: lang.Event+' - '+d.screenshotName, + description: lang.EventText1+' '+d.currentTimestamp, + },[ + { + type: 'photo', + attachment: d.screenshotBuffer, + name: d.screenshotName+'.jpg' + } + ],d.ke) + } if(monitorConfig.details.detector_telegrambot_send_video === '1'){ + // await asyncSetTimeout(3000) let videoPath = null let videoName = null const eventBasedRecording = await getEventBasedRecordingUponCompletion({ @@ -87,19 +109,6 @@ module.exports = function(s,config,lang,getSnapshot){ ],d.ke) } } - await getSnapshot(d,monitorConfig) - if(d.screenshotBuffer){ - sendMessage({ - title: lang.Event+' - '+d.screenshotName, - description: lang.EventText1+' '+d.currentTimestamp, - },[ - { - type: 'photo', - attachment: d.screenshotBuffer, - name: d.screenshotName+'.jpg' - } - ],d.ke) - } } } const onTwoFactorAuthCodeNotificationForTelegram = function(r){ @@ -267,7 +276,7 @@ module.exports = function(s,config,lang,getSnapshot){ "default": "", "example": "", "possible": "" - }, + }, { hidden: true, "name": "detail=telegrambot_channel", @@ -281,8 +290,31 @@ module.exports = function(s,config,lang,getSnapshot){ } ] } + s.definitions["Event Filters"].blocks["Action for Selected"].info.push({ + "name": "actions=telegram", + "field": lang['Telegram'], + "fieldType": "select", + "form-group-class": "actions-row", + "default": "", + "example": "1", + "possible": [ + { + "name": lang.Default, + "value": "", + "selected": true + }, + { + "name": lang.No, + "value": "0", + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }) }catch(err){ - console.log(err) + console.error(err) console.log('Could not start Telegram bot, please run "npm install node-telegram-bot-api" inside the Shinobi folder.') } } diff --git a/libs/notifications/webhook.js b/libs/notifications/webhook.js new file mode 100644 index 00000000..9d3d916f --- /dev/null +++ b/libs/notifications/webhook.js @@ -0,0 +1,289 @@ +const fetch = require('node-fetch'); +const FormData = require('form-data'); +module.exports = function(s,config,lang,getSnapshot){ + function replaceQueryStringValues(webhookEndpoint,data){ + let newString = webhookEndpoint + .replace(/{{INNER_EVENT_TITLE}}/g,data.title) + .replace(/{{INNER_EVENT_INFO}}/g,s.stringJSON(data.info)); + return newString; + } + const sendMessage = function(sendBody,files,groupKey){ + let webhookEndpoint = s.group[groupKey].init.global_webhook_url; + if(!webhookEndpoint){ + s.userLog({ + ke: groupKey, + mid: '$USER' + },{ + type: lang.NotifyErrorText, + infoType: 'global_webhook', + msg: lang['Invalid Settings'] + }) + return new Promise((resolve,reject) => { + resolve({ + error: lang['Invalid Settings'], + ok: false, + }) + }) + } + const doPostMethod = s.group[groupKey].init.global_webhook_method === 'post'; + // const includeSnapshot = s.group[groupKey].init.global_webhook_include_image === '1'; + const webhookInfoData = { + info: sendBody, + files: [], + } + if(files){ + const formData = new FormData(); + files.forEach(async (file,n) => { + switch(file.type){ + case'video': + // video cannot be sent this way unless POST + if(doPostMethod){ + const fileName = file.name + formData.append(`file${n + 1}`, file.attachment, { + contentType: 'video/mp4', + name: fileName, + filename: fileName, + }); + webhookInfoData.files.push(fileName) + } + break; + case'photo': + if(doPostMethod){ + const fileName = file.name + formData.append(`file${n + 1}`, file.attachment, { + contentType: 'image/jpeg', + name: fileName, + filename: fileName, + }); + webhookInfoData.files.push(fileName) + }else{ + const base64StringofImage = file.attachment.toString('base64') + webhookInfoData.files.push(base64StringofImage) + } + break; + } + }) + }else{ + delete(webhookInfoData.files) + } + webhookEndpoint = replaceQueryStringValues(webhookEndpoint,{ + title: sendBody.title, + info: webhookInfoData, + }); + return new Promise((resolve,reject) => { + const response = { + ok: true, + } + fetch(webhookEndpoint,doPostMethod ? { + method: 'POST', + body: formData + } : undefined) + .then(res => res.text()) + .then((text) => { + response.response = text; + resolve(response) + }) + .catch((err) => { + response.ok = false; + response.error = err; + s.userLog({ + ke: groupKey, + mid: '$USER' + },{ + type: lang.NotifyErrorText, + infoType: 'global_webhook', + msg: err + }) + resolve(response) + }) + }) + } + const onEventTriggerBeforeFilterForGlobalWebhook = function(d,filter){ + filter.global_webhook = false + } + const onEventTriggerForGlobalWebhook = async (d,filter) => { + let filesSent = 0; + const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id] + // d = event object + if((filter.global_webhook || monitorConfig.details.notify_global_webhook === '1') && !s.group[d.ke].activeMonitors[d.id].detector_global_webhook){ + var detector_global_webhook_timeout + if(!monitorConfig.details.detector_global_webhook_timeout||monitorConfig.details.detector_global_webhook_timeout===''){ + detector_global_webhook_timeout = 1000 * 60 * 10; + }else{ + detector_global_webhook_timeout = parseFloat(monitorConfig.details.detector_global_webhook_timeout) * 1000 * 60; + } + s.group[d.ke].activeMonitors[d.id].detector_global_webhook = setTimeout(function(){ + clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_global_webhook); + s.group[d.ke].activeMonitors[d.id].detector_global_webhook = null + },detector_global_webhook_timeout) + await getSnapshot(d,monitorConfig) + if(d.screenshotBuffer){ + sendMessage({ + title: lang.Event+' - '+d.screenshotName, + description: lang.EventText1+' '+d.currentTimestamp, + },[ + { + type: 'photo', + attachment: d.screenshotBuffer, + name: d.screenshotName+'.jpg' + } + ],d.ke) + ++filesSent; + } + if(filesSent === 0){ + sendMessage({ + title: lang.Event, + description: lang.EventText1+' '+d.currentTimestamp, + eventDetails: d.details + },[],d.ke) + } + } + } + const onTwoFactorAuthCodeNotificationForGlobalWebhook = function(r){ + // r = user + if(r.details.factor_global_webhook === '1'){ + sendMessage({ + title: r.lang['Enter this code to proceed'], + description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+r.lang.FactorAuthText1, + },[],r.ke) + } + } + // const onDetectorNoTriggerTimeoutForGlobalWebhook = function(e){ + // //e = monitor object + // var currentTime = new Date() + // if(e.details.detector_notrigger_global_webhook === '1'){ + // var html = '*'+lang.NoMotionEmailText2+' ' + (e.details.detector_notrigger_timeout || 10) + ' '+lang.minutes+'.*\n' + // html += '**' + lang['Monitor Name'] + '** : '+e.name + '\n' + // html += '**' + lang['Monitor ID'] + '** : '+e.id + '\n' + // html += currentTime + // sendMessage({ + // title: lang['\"No Motion"\ Detector'], + // description: html, + // },[],e.ke) + // } + // } + const onMonitorUnexpectedExitForGlobalWebhook = (monitorConfig) => { + if(monitorConfig.details.notify_global_webhook === '1' && monitorConfig.details.notify_onUnexpectedExit === '1'){ + const ffmpegCommand = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].ffmpeg + const description = lang['Process Crashed for Monitor'] + '\n' + ffmpegCommand + const currentTime = new Date() + sendMessage({ + title: lang['Process Unexpected Exit'] + ' : ' + monitorConfig.name, + description: description, + },[],monitorConfig.ke) + } + } + s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotificationForGlobalWebhook) + s.onEventTrigger(onEventTriggerForGlobalWebhook) + s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilterForGlobalWebhook) + // s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeoutForGlobalWebhook) + s.onMonitorUnexpectedExit(onMonitorUnexpectedExitForGlobalWebhook) + s.definitions["Monitor Settings"].blocks["Notifications"].info[0].info.push( + { + "name": "detail=notify_global_webhook", + "field": lang.Webhook, + "description": "", + "default": "0", + "example": "", + "selector": "h_det_global_webhook", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + ) + s.definitions["Account Settings"].blocks["2-Factor Authentication"].info.push({ + "name": "detail=factor_global_webhook", + "field": lang.Webhook, + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }) + s.definitions["Account Settings"].blocks["Webhook"] = { + "evaluation": "$user.details.use_global_webhook !== '0'", + "name": lang.Webhook, + "color": "blue", + info: [ + { + "name": "detail=global_webhook", + "selector":"u_global_webhook", + "field": lang.Enabled, + "default": "0", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + }, + { + hidden: true, + "name": "detail=global_webhook_url", + "placeholder": "http://your-webhook-point/onEvent/{{INNER_EVENT_TITLE}}?info={{INNER_EVENT_INFO}}", + "field": lang["Webhook URL"], + "form-group-class":"u_global_webhook_input u_global_webhook_1", + }, + { + hidden: true, + "name": "detail=factor_global_webhook", + "field": lang["2-Factor Authentication"], + "form-group-class":"u_global_webhook_input u_global_webhook_1", + "default": "1", + "example": "", + "fieldType": "select", + "possible": [ + { + "name": lang.No, + "value": "0" + }, + { + "name": lang.Yes, + "value": "1" + } + ] + } + ] + } + s.definitions["Event Filters"].blocks["Action for Selected"].info.push({ + "name": "actions=global_webhook", + "field": lang['Webhook'], + "fieldType": "select", + "form-group-class": "actions-row", + "default": "", + "example": "1", + "possible": [ + { + "name": lang['Original Choice'], + "value": "", + "selected": true + }, + { + "name": lang.Yes, + "value": "1", + } + ] + }) +} diff --git a/libs/onvifDeviceManager.js b/libs/onvifDeviceManager.js index 921971e7..1c66ba2b 100644 --- a/libs/onvifDeviceManager.js +++ b/libs/onvifDeviceManager.js @@ -17,16 +17,36 @@ const { } = require('./onvifDeviceManager/utils.js') module.exports = function(s,config,lang,app,io){ + async function getOnvifDevice(groupKey,monitorId){ + const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({id: monitorId, ke: groupKey})).device + return onvifDevice + } /** * API : Get ONVIF Data from Camera */ app.get(config.webPaths.apiPrefix+':auth/onvifDeviceManager/:ke/:id',function (req,res){ s.auth(req.params,async (user) => { + const groupKey = req.params.ke + const monitorId = req.params.id + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.get_monitors_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_monitors`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return + } const endData = {ok: true} try{ - const groupKey = req.params.ke - const monitorId = req.params.id - const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection + const onvifDevice = await getOnvifDevice(groupKey,monitorId) const cameraInfo = await getUIFieldValues(onvifDevice) endData.onvifData = cameraInfo }catch(err){ @@ -42,12 +62,30 @@ module.exports = function(s,config,lang,app,io){ */ app.post(config.webPaths.apiPrefix+':auth/onvifDeviceManager/:ke/:id/save',function (req,res){ s.auth(req.params,async (user) => { + const groupKey = req.params.ke + const monitorId = req.params.id + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId); + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed + ){ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Not Authorized'] + }); + return + } const endData = {ok: true} const responses = {} try{ - const groupKey = req.params.ke - const monitorId = req.params.id - const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection + const onvifDevice = await getOnvifDevice(groupKey,monitorId) const form = s.getPostData(req) const videoToken = form.VideoConfiguration && form.VideoConfiguration.videoToken ? form.VideoConfiguration.videoToken : null if(form.DateandTime){ @@ -96,11 +134,29 @@ module.exports = function(s,config,lang,app,io){ */ app.get(config.webPaths.apiPrefix+':auth/onvifDeviceManager/:ke/:id/reboot',function (req,res){ s.auth(req.params,async (user) => { + const groupKey = req.params.ke + const monitorId = req.params.id + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId); + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed + ){ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Not Authorized'] + }); + return + } const endData = {ok: true} try{ - const groupKey = req.params.ke - const monitorId = req.params.id - const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection + const onvifDevice = await getOnvifDevice(groupKey,monitorId) const cameraInfo = await rebootCamera(onvifDevice) endData.onvifData = cameraInfo }catch(err){ diff --git a/libs/onvifDeviceManager/utils.js b/libs/onvifDeviceManager/utils.js index 2ba4396a..0d632979 100644 --- a/libs/onvifDeviceManager/utils.js +++ b/libs/onvifDeviceManager/utils.js @@ -149,6 +149,7 @@ const getDeviceInformation = async (onvifDevice,options) => { response.ok = false response.error = err.stack.toString().toString() s.debugLog(err) + s.debugLog(onvifDevice) } return response } diff --git a/libs/plugins/superUser.js b/libs/plugins/superUser.js index 3fd34cff..3604ef23 100644 --- a/libs/plugins/superUser.js +++ b/libs/plugins/superUser.js @@ -1,13 +1,12 @@ const fs = require('fs-extra'); const express = require('express') -const request = require('request') const unzipper = require('unzipper') -const fetch = require("node-fetch") const spawn = require('child_process').spawn const { Worker } = require('worker_threads'); module.exports = async (s,config,lang,app,io,currentUse) => { + const { fetchDownloadAndWrite } = require('../basic/utils.js')(process.cwd(),config) const { currentPluginCpuUsage, currentPluginGpuUsage, @@ -96,10 +95,9 @@ module.exports = async (s,config,lang,app,io,currentUse) => { fs.mkdirSync(downloadPath) return new Promise(async (resolve, reject) => { fs.mkdir(downloadPath, () => { - request(downloadUrl).pipe(fs.createWriteStream(downloadPath + '.zip')) - .on('finish',() => { - zip = fs.createReadStream(downloadPath + '.zip') - .pipe(unzipper.Parse()) + fetchDownloadAndWrite(downloadUrl,downloadPath + '.zip', 1) + .then((readStream) => { + readStream.pipe(unzipper.Parse()) .on('entry', async (file) => { if(file.type === 'Directory'){ try{ diff --git a/libs/scanners.js b/libs/scanners.js index a30a406f..af570cdf 100644 --- a/libs/scanners.js +++ b/libs/scanners.js @@ -21,6 +21,20 @@ module.exports = function(s,config,lang,app,io){ */ app.get(config.webPaths.apiPrefix+':auth/probe/:ke',function (req,res){ s.auth(req.params,function(user){ + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed + ){ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Not Authorized'] + }); + return + } ffprobe(req.query.url,req.params.auth,(endData) => { s.closeJsonResponse(res,endData) }) diff --git a/libs/scanners/utils.js b/libs/scanners/utils.js index 4cd4f8f2..7169911b 100644 --- a/libs/scanners/utils.js +++ b/libs/scanners/utils.js @@ -1,7 +1,9 @@ var os = require('os'); const onvif = require("shinobi-onvif"); const { + addCredentialsToUrl, stringContains, + getBuffer, } = require('../common.js') module.exports = (s,config,lang) => { const ipRange = (start_ip, end_ip) => { @@ -119,6 +121,7 @@ module.exports = (s,config,lang) => { ProfileToken : device.current_profile.token, Protocol : 'RTSP' }) + var cameraResponse = { ip: camera.ip, port: camera.port, @@ -142,16 +145,17 @@ module.exports = (s,config,lang) => { } responseList.push(cameraResponse) var imageSnap - if(cameraResponse.uri){ - try{ - imageSnap = (await s.getSnapshotFromOnvif({ - username: onvifUsername, - password: onvifPassword, - uri: cameraResponse.uri, - })).toString('base64'); - }catch(err){ - s.debugLog(err) - } + try{ + const snapUri = addCredentialsToUrl({ + username: onvifUsername, + password: onvifPassword, + url: (await device.services.media.getSnapshotUri({ + ProfileToken : device.current_profile.token, + })).data.GetSnapshotUriResponse.MediaUri.Uri, + }); + imageSnap = (await getBuffer(snapUri)).toString('base64'); + }catch(err){ + s.debugLog(err) } if(foundCameraCallback)foundCameraCallback(Object.assign(cameraResponse,{f: 'onvif', snapShot: imageSnap})) }catch(err){ @@ -185,7 +189,7 @@ module.exports = (s,config,lang) => { error: errorMessage }) } - s.debugLog(err) + if(config.debugLogVerbose)s.debugLog(err); } }) return responseList diff --git a/libs/scheduler.js b/libs/scheduler.js index 861f906d..a25fe43d 100644 --- a/libs/scheduler.js +++ b/libs/scheduler.js @@ -8,10 +8,11 @@ module.exports = function(s,config,lang,app,io){ columns: "*", table: "Schedules" },(err,rows) => { - rows.forEach(function(schedule){ + + rows && rows.forEach(function(schedule){ s.updateSchedule(schedule) }) - if(callback)callback() + if(callback && typeof callback === 'function')callback() }) } //update schedule @@ -193,9 +194,11 @@ module.exports = function(s,config,lang,app,io){ var endData = { ok : false } - if(user.details.sub){ - endData.msg = user.lang['Not Permitted'] - s.closeJsonResponse(res,endData) + const { + isSubAccount, + } = s.checkPermission(user) + if(isSubAccount){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not an Administrator Account']}); return } var whereQuery = [ @@ -233,9 +236,11 @@ module.exports = function(s,config,lang,app,io){ var endData = { ok : false } - if(user.details.sub){ - endData.msg = user.lang['Not Permitted'] - s.closeJsonResponse(res,endData) + const { + isSubAccount, + } = s.checkPermission(user) + if(isSubAccount){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not an Administrator Account']}); return } switch(req.params.action){ diff --git a/libs/shinobiHub.js b/libs/shinobiHub.js index f809c939..b9914524 100644 --- a/libs/shinobiHub.js +++ b/libs/shinobiHub.js @@ -1,7 +1,10 @@ -var fs = require('fs') -var request = require('request') +const fs = require('fs') +const fetch = require('node-fetch') module.exports = function(s,config,lang,app,io){ if(config.shinobiHubEndpoint === undefined){config.shinobiHubEndpoint = `https://hub.shinobi.video/`}else{config.shinobiHubEndpoint = s.checkCorrectPathEnding(config.shinobiHubEndpoint)} + const { + fetchWithAuthentication, + } = require('./basic/utils.js')(process.cwd(),config) var stripUsernameAndPassword = function(string,username,password){ if(username)string = string.split(username).join('_USERNAME_') if(password)string = string.split(password).join('_PASSWORD_') @@ -53,20 +56,27 @@ module.exports = function(s,config,lang,app,io){ json: JSON.stringify(monitorConfig) }) if(validated.ok === true){ - request.post({ - url: `${config.shinobiHubEndpoint}api/${shinobiHubApiKey}/postConfiguration`, - form: { - "type": type, - "brand": monitorConfig.ke, - "name": monitorConfig.name, - "description": "Backup at " + (new Date()), - "json": validated.json, - "details": JSON.stringify({ - // maybe ip address? - }) + fetchWithAuthentication( + `${config.shinobiHubEndpoint}api/${shinobiHubApiKey}/postConfiguration`, + { + method: 'POST', + postData: { + "type": type, + "brand": monitorConfig.ke, + "name": monitorConfig.name, + "description": "Backup at " + (new Date()), + "json": validated.json, + "details": JSON.stringify({ + // maybe ip address? + }) + } } - }, function(err,httpResponse,body){ - callback(err,s.parseJSON(body) || {ok: false}) + ).then(res => res.text()) + .then((data) => { + callback(null,s.parseJSON(data) || {ok: false}) + }) + .catch((err) => { + callback(err,{ok: false}) }) }else{ callback(new Error(validated.msg),{ok: false}) @@ -78,7 +88,7 @@ module.exports = function(s,config,lang,app,io){ // s.userLog({ke:monitorConfig.ke,mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.mail,ip:cn.ip}}) }) } - if(s.group[monitorConfig.ke] && s.group[monitorConfig.ke].init.shinobihub === '1'){ + if(s.group[monitorConfig.ke] && s.group[monitorConfig.ke].init && s.group[monitorConfig.ke].init.shinobihub === '1'){ uploadConfiguration(s.group[monitorConfig.ke].init.shinobihub_key,'cam',monitorConfig,() => { // s.userLog({ke:monitorConfig.ke,mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.mail,ip:cn.ip}}) }) @@ -100,7 +110,11 @@ module.exports = function(s,config,lang,app,io){ queryString.push(key + '=' + value) }) } - request(`${config.shinobiHubEndpoint}api/${shinobiHubApiKey}/getConfiguration/${req.params.type}${req.params.id ? '/' + req.params.id : ''}${queryString.length > 0 ? '?' + queryString.join('&') : ''}`).pipe(res) + const configUrl = `${config.shinobiHubEndpoint}api/${shinobiHubApiKey}/getConfiguration/${req.params.type}${req.params.id ? '/' + req.params.id : ''}${queryString.length > 0 ? '?' + queryString.join('&') : ''}` + fetch(configUrl).then(actual => { + actual.headers.forEach((v, n) => res.setHeader(n, v)); + actual.body.pipe(res); + }) }else{ s.closeJsonResponse(res,{ ok: false, diff --git a/libs/socketio.js b/libs/socketio.js index 94e9fb88..94075854 100644 --- a/libs/socketio.js +++ b/libs/socketio.js @@ -7,12 +7,12 @@ const { stringToSqlTime, } = require('./common.js') module.exports = function(s,config,lang,io){ - const { - legacyFilterEvents - } = require('./events/utils.js')(s,config,lang) const { ptzControl } = require('./control/ptz.js')(s,config,lang) + const { + legacyFilterEvents + } = require('./events/utils.js')(s,config,lang) s.clientSocketConnection = {} //send data to socket client function s.tx = function(z,y,x){ @@ -144,39 +144,6 @@ module.exports = function(s,config,lang,io){ ////socket controller io.on('connection', function (cn) { var tx; - //unique h265 socket stream - cn.on('h265',function(d){ - if(!s.group[d.ke]||!s.group[d.ke].activeMonitors||!s.group[d.ke].activeMonitors[d.id]){ - cn.disconnect();return; - } - cn.ip=cn.request.connection.remoteAddress; - var toUTC = function(){ - return new Date().toISOString(); - } - var tx=function(z){cn.emit('data',z);} - const onFail = (msg) => { - tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); - cn.disconnect(); - } - const onSuccess = (r) => { - r = r[0]; - const Emitter = createStreamEmitter(d,cn) - validatedAndBindAuthenticationToSocketConnection(cn,d,true) - var contentWriter - cn.closeSocketVideoStream = function(){ - Emitter.removeListener('data', contentWriter); - } - Emitter.on('data',contentWriter = function(base64){ - tx(base64) - }) - } - //check if auth key is user's temporary session key - if(s.group[d.ke]&&s.group[d.ke].users&&s.group[d.ke].users[d.auth]){ - onSuccess(s.group[d.ke].users[d.auth]); - }else{ - streamConnectionAuthentication(d,cn.ip).then(onSuccess).catch(onFail) - } - }) //unique Base64 socket stream cn.on('Base64',function(d){ if(!s.group[d.ke]||!s.group[d.ke].activeMonitors||!s.group[d.ke].activeMonitors[d.id]){ @@ -360,7 +327,7 @@ module.exports = function(s,config,lang,io){ if(s.group[d.ke].users[d.auth].details.get_server_log!=='0'){ cn.join('GRPLOG_'+d.ke) } - s.group[d.ke].users[d.auth].lang=s.getLanguageFile(s.group[d.ke].users[d.auth].details.lang) + s.group[d.ke].users[d.auth].lang = s.getLanguageFile(s.group[d.ke].users[d.auth].details.lang) s.userLog({ke:d.ke,mid:'$USER'},{type:s.group[d.ke].users[d.auth].lang['Websocket Connected'],msg:{mail:r.mail,id:d.uid,ip:cn.ip}}) if(!s.group[d.ke].activeMonitors){ s.group[d.ke].activeMonitors={} @@ -380,7 +347,7 @@ module.exports = function(s,config,lang,io){ } }) try{ - Object.values(s.group[d.ke].rawMonitorConfigurations).forEach((monitor) => { + (s.group[d.ke] ? Object.values(s.group[d.ke].rawMonitorConfigurations) : []).forEach((monitor) => { s.cameraSendSnapshot({ mid: monitor.mid, ke: monitor.ke, @@ -400,7 +367,8 @@ module.exports = function(s,config,lang,io){ return; } if((d.id||d.uid||d.mid)&&cn.ke){ - try{ + try{ + d.callbackResponse = {ok: true} switch(d.f){ case'monitorOrder': if(d.monitorOrder && d.monitorOrder instanceof Object){ @@ -443,8 +411,18 @@ module.exports = function(s,config,lang,io){ ] },(err,r) => { if(r && r[0]){ + const monitorListOrder = {} + const orderKeys = Object.keys(d.monitorListOrder) details = JSON.parse(r[0].details) - details.monitorListOrder = d.monitorListOrder + orderKeys.forEach((orderKey) => { + const monitorIds = d.monitorListOrder[orderKey] + const uniqueList = {} + monitorIds.forEach((monitorId) => { + uniqueList[monitorId] = 1 + }) + monitorListOrder[orderKey] = Object.keys(uniqueList) + }) + details.monitorListOrder = monitorListOrder s.knexQuery({ action: "update", table: "Users", @@ -653,12 +631,6 @@ module.exports = function(s,config,lang,io){ break; } break; - case'control': - ptzControl(d,function(msg){ - s.userLog(d,msg) - tx({f:'control',response:msg}) - }) - break; case'jpeg_off': delete(cn.jpeg_on); if(cn.monitorsCurrentlyWatching){ @@ -692,6 +664,7 @@ module.exports = function(s,config,lang,io){ f: 'monitor_watch_on', id: d.id, ke: d.ke, + subStreamChannel: s.group[d.ke].activeMonitors[d.id].subStreamChannel, warnings: s.group[d.ke].activeMonitors[d.id].warnings || [] }) s.camera('watch_on',d,cn) @@ -721,6 +694,18 @@ module.exports = function(s,config,lang,io){ break; } break; + default: + s.onOtherWebSocketMessagesExtensions.forEach(function(extender){ + extender(d,cn,tx) + }) + break; + } + if(d.callbackId && !d.hasResponded){ + tx({ + f:'callback', + callbackId: d.callbackId, + args: [d.callbackResponse] + }) } }catch(er){ s.systemLog('ERROR CATCH 1',er) @@ -959,63 +944,6 @@ module.exports = function(s,config,lang,io){ } }) //functions for retrieving cron announcements - cn.on('cron',function(d){ - if(d.f==='init'){ - if(config.cron.key){ - if(config.cron.key===d.cronKey){ - s.cron={started:moment(),last_run:moment(),id:cn.id}; - }else{ - cn.disconnect() - } - }else{ - s.cron={started:moment(),last_run:moment(),id:cn.id}; - } - }else{ - if(s.cron&&cn.id===s.cron.id){ - delete(d.cronKey) - switch(d.f){ - case'filters': - legacyFilterEvents(d.ff,d) - break; - case's.tx': - s.tx(d.data,d.to) - break; - case's.deleteVideo': - s.deleteVideo(d.file) - break; - case's.deleteFileBinEntry': - s.deleteFileBinEntry(d.file) - break; - case's.setDiskUsedForGroup': - function doOnMain(){ - s.setDiskUsedForGroup(d.ke,d.size,d.target || undefined) - } - if(d.videoRow){ - let storageIndex = s.getVideoStorageIndex(d.videoRow); - if(storageIndex){ - s.setDiskUsedForGroupAddStorage(d.ke,{ - size: d.size, - storageIndex: storageIndex - }) - }else{ - doOnMain() - } - }else{ - doOnMain() - } - break; - case'start':case'end': - d.mid='_cron';s.userLog(d,{type:'cron',msg:d.msg}) - break; - default: - s.systemLog('CRON : ',d) - break; - } - }else{ - cn.disconnect() - } - } - }) cn.on('disconnect', function () { if(cn.socketVideoStream){ cn.closeSocketVideoStream() @@ -1050,7 +978,7 @@ module.exports = function(s,config,lang,io){ delete(s.clientSocketConnection[cn.id]) }) s.onWebSocketConnectionExtensions.forEach(function(extender){ - extender(cn) + extender(cn,validatedAndBindAuthenticationToSocketConnection,createStreamEmitter) }) }); } diff --git a/libs/sql.js b/libs/sql.js index 4954ac6c..24437651 100644 --- a/libs/sql.js +++ b/libs/sql.js @@ -162,6 +162,66 @@ module.exports = function(s,config){ }catch(err){ console.log(err) } + try{ + await s.databaseEngine.schema.table('Videos', table => { + table.string('objects') + }) + }catch(err){ + if(err && err.code !== 'ER_DUP_FIELDNAME'){ + s.debugLog(err) + } + } + try{ + await s.databaseEngine.schema.table('Videos', table => { + table.tinyint('archive',1).defaultTo(0) + }) + }catch(err){ + if(err && err.code !== 'ER_DUP_FIELDNAME'){ + s.debugLog(err) + } + } + try{ + await s.databaseEngine.schema.table('Monitors', table => { + table.string('saveDir',255).defaultTo('') + }) + }catch(err){ + if(err && err.code !== 'ER_DUP_FIELDNAME'){ + s.debugLog(err) + } + } + try{ + await s.databaseEngine.schema.table('Timelapse Frames', table => { + table.tinyint('archive',1).defaultTo(0) + table.string('saveDir',255).defaultTo('') + }) + }catch(err){ + if(err && err.code !== 'ER_DUP_FIELDNAME'){ + s.debugLog(err) + } + } + try{ + await s.databaseEngine.schema.table('Events', table => { + table.tinyint('archive',1).defaultTo(0) + }) + }catch(err){ + if(err && err.code !== 'ER_DUP_FIELDNAME'){ + s.debugLog(err) + } + } + try{ + s.databaseEngine.schema.table('Files', table => { + table.tinyint('archive',1).defaultTo(0) + }).then(() => { + console.log(`archive added to Files table`) + }).catch((err) => { + if(err && err.code !== 'ER_DUP_FIELDNAME'){ + console.log('error') + console.log(err) + } + }) + }catch(err){ + console.log(err) + } delete(s.preQueries) } } diff --git a/libs/startup.js b/libs/startup.js index 47d338bb..a61e5ab9 100644 --- a/libs/startup.js +++ b/libs/startup.js @@ -11,6 +11,9 @@ module.exports = function(s,config,lang,io){ const { checkSubscription } = require('./basic/utils.js')(process.cwd(),config) + const { + checkForStaticUsers + } = require('./user/startup.js')(s,config,lang,io) return new Promise((resolve, reject) => { var checkedAdminUsers = {} console.log('FFmpeg version : '+s.ffmpegVersion) @@ -84,7 +87,7 @@ module.exports = function(s,config,lang,io){ status: 'Stopped', code: 5 }); - var monObj = Object.assign(monitor,{id : monitor.mid}) + const monObj = Object.assign({},monitor,{id : monitor.mid}) s.camera(monitor.mode,monObj) checkAnother() },1000) @@ -262,11 +265,15 @@ module.exports = function(s,config,lang,io){ },function(err,frames) { if(frames && frames[0]){ frames.forEach(function(frame){ - var storageType = JSON.parse(frame.details).type - if(!storageType)storageType = 's3' - var frameSize = frame.size / 1048576 - user.cloudDiskUse[storageType].usedSpace += frameSize - user.cloudDiskUse[storageType].usedSpaceTimelapseFrames += frameSize + try{ + var storageType = JSON.parse(frame.details).type + if(!storageType)storageType = 's3' + var frameSize = frame.size / 1048576 + user.cloudDiskUse[storageType].usedSpace += frameSize + user.cloudDiskUse[storageType].usedSpaceTimelapseFrames += frameSize + }catch(err){ + s.debugLog(err) + } }) } callback() @@ -409,7 +416,8 @@ module.exports = function(s,config,lang,io){ s.databaseEngine = require('knex')(s.databaseOptions) //run prerequsite queries s.preQueries() - setTimeout(() => { + setTimeout(async () => { + await checkForStaticUsers() //check for subscription checkSubscription(config.subscriptionId,function(hasSubcribed){ config.userHasSubscribed = hasSubcribed diff --git a/libs/timelapse.js b/libs/timelapse.js index 4cdd42e0..99538238 100644 --- a/libs/timelapse.js +++ b/libs/timelapse.js @@ -1,7 +1,25 @@ -var fs = require('fs') -var moment = require('moment') -var express = require('express') +const fs = require('fs') +const moment = require('moment') +const express = require('express') +const exec = require('child_process').exec; +const spawn = require('child_process').spawn; +const events = require('events'); module.exports = function(s,config,lang,app,io){ + const { + sendTimelapseFrameToMasterNode, + } = require('./childNode/childUtils.js')(s,config,lang) + const { + splitForFFPMEG, + } = require('./ffmpeg/utils.js')(s,config,lang) + const { + getFileDirectory, + } = require('./basic/utils.js')(process.cwd(),config) + const { + processKill, + } = require('./monitor/utils.js')(s,config,lang) + const { + stitchMp4Files, + } = require('./video/utils.js')(s,config,lang) const timelapseFramesCache = {} const timelapseFramesCacheTimeouts = {} s.getTimelapseFrameDirectory = function(e){ @@ -22,8 +40,8 @@ module.exports = function(s,config,lang,app,io){ if(e.details && e.details.dir && e.details.dir !== ''){ details.dir = e.details.dir } - var timeNow = eventTime || new Date() - var queryInfo = { + const timeNow = eventTime || new Date() + const queryInfo = { ke: e.ke, mid: e.id, details: s.s(details), @@ -32,45 +50,16 @@ module.exports = function(s,config,lang,app,io){ time: timeNow } if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ - var currentDate = s.formattedTime(queryInfo.time,'YYYY-MM-DD') - s.cx({ - f: 'open_timelapse_file_transfer', + var currentDate = s.formattedTime(timeNow,'YYYY-MM-DD') + const childNodeData = { ke: e.ke, mid: e.id, - d: s.group[e.ke].rawMonitorConfigurations[e.id], + time: currentDate, filename: filename, currentDate: currentDate, queryInfo: queryInfo - }) - var formattedTime = s.timeObject(timeNow).format() - fs.createReadStream(filePath,{ highWaterMark: 500 }) - .on('data',function(data){ - s.cx({ - f: 'created_timelapse_file_chunk', - ke: e.ke, - mid: e.id, - time: formattedTime, - filesize: e.filesize, - chunk: data, - d: s.group[e.ke].rawMonitorConfigurations[e.id], - filename: filename, - currentDate: currentDate, - queryInfo: queryInfo - }) - }) - .on('close',function(){ - s.cx({ - f: 'created_timelapse_file', - ke: e.ke, - mid: e.id, - time: formattedTime, - filesize: e.filesize, - d: s.group[e.ke].rawMonitorConfigurations[e.id], - filename: filename, - currentDate: currentDate, - queryInfo: queryInfo - }) - }) + } + sendTimelapseFrameToMasterNode(filePath,childNodeData) }else{ s.insertTimelapseFrameDatabaseRow(e,queryInfo,filePath) } @@ -159,8 +148,14 @@ module.exports = function(s,config,lang,app,io){ table: "Timelapse Frames", where: frameSelector, limit: 1 - },function(){ + },async function(){ + s.setDiskUsedForGroup(e.ke,-(r.size / 1048576),'timelapeFrames') s.file('delete',e.fileLocation) + const fileDirectory = getFileDirectory(folderPath); + const folderIsEmpty = (await fs.promises.readdir(folderPath)).filter(file => file.indexOf('.jpg') > -1).length === 0; + if(folderIsEmpty){ + await fs.rmdir(folderPath, { recursive: true }) + } }) }else{ // console.log('Delete Failed',e) @@ -168,6 +163,261 @@ module.exports = function(s,config,lang,app,io){ } }) } + function splitArrayIntoMultiple(bigarray,size){ + size = size || 80; + var arrayOfArrays = []; + for (var i=0; i { + const frames = options.frames + const ke = frames[0].ke + const mid = frames[0].mid + const concatListFile = options.listFile + createTemporaryInputFile(frames,concatListFile).then((framesAccepted) => { + var completionTimeout + const framesPerSecond = options.fps + const finalMp4OutputLocation = options.output + const onPercentChange = options.onPercentChange + const numberOfFrames = framesAccepted.length + const commandString = `-y -threads 1 -re -f concat -safe 0 -r ${framesPerSecond} -i "${concatListFile}" -q:v 1 -c:v libx264 -preset ultrafast -r ${framesPerSecond} "${finalMp4OutputLocation}"` + s.debugLog("ffmpeg",commandString) + const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString)) + videoBuildProcess.stdout.on('data',function(data){ + s.debugLog('stdout',finalMp4OutputLocation,data.toString()) + }) + videoBuildProcess.stderr.on('data',function(data){ + const text = data.toString() + if(text.startsWith('frame=')){ + const currentFrame = parseInt(text.split(/(\s+)/)[2]) + const percent = (currentFrame / numberOfFrames * 100).toFixed(1) + onPercentChange(percent,currentFrame) + } + clearTimeout(completionTimeout) + completionTimeout = setTimeout(function(){ + s.debugLog('videoBuildProcess completionTimeout',finalMp4OutputLocation) + processKill(videoBuildProcess) + },20000) + }) + videoBuildProcess.on('exit',async function(data){ + clearTimeout(completionTimeout) + resolve() + await fs.promises.unlink(concatListFile) + }) + }) + }) + } + async function chunkFramesAndBuildMultipleVideosThenSticth(options){ + // a single video with too many frames makes the video unplayable, this is the fix. + const frames = options.frames + const ke = frames[0].ke + const mid = frames[0].mid + const finalFileName = options.finalFileName + const concatListFile = options.listFile + const framesPerSecond = options.fps + const finalMp4OutputLocation = options.output + const onPercentChange = options.onPercentChange + const frameChunks = splitArrayIntoMultiple(frames,80) + const numberOfSets = frameChunks.length + const filePathsList = [] + for (let i = 0; i < numberOfSets; i++) { + var frameSet = frameChunks[i] + var numberOfFrames = frameSet.length + var segmentFileOutput = `${s.dir.streams}${ke}/${mid}/${s.gid(10)}.mp4` + filePathsList.push(segmentFileOutput) + await buildVideoSegmentFromFrames({ + frames: frameSet, + listFile: `${concatListFile}${i}`, + fps: framesPerSecond, + output: segmentFileOutput, + onPercentChange: (percent,currentFrame) => { + const overallPercent = ((percent / numberOfSets) + (i * (100 / numberOfSets))).toFixed(1); + s.tx({ + f: 'timelapse_build_percent', + ke: ke, + mid: mid, + name: finalFileName, + percent: overallPercent, + },'GRP_'+ke); + if(percent == 100){ + s.debugLog('videoBuildProcess 100%',finalMp4OutputLocation) + } + s.debugLog(`Piece ${i}`,`${currentFrame} / ${numberOfFrames}`,`${percent}%`) + }, + }) + } + s.debugLog('videoBuildProcess Stitching...',finalMp4OutputLocation) + await createTemporaryInputFileForStitched(filePathsList,concatListFile) + await stitchMp4Files({ + listFile: concatListFile, + output: finalMp4OutputLocation, + }) + await fs.promises.rm(concatListFile) + for (let i = 0; i < filePathsList.length; i++) { + var segmentFileOutput = filePathsList[i] + await fs.promises.rm(segmentFileOutput) + } + s.debugLog('videoBuildProcess Stitching Complete!',finalMp4OutputLocation) + } + async function createVideoFromTimelapse(timelapseFrames,framesPerSecond){ + s.debugLog("Building Timelapse Frames Video",timelapseFrames.length) + framesPerSecond = !isNaN(framesPerSecond) ? framesPerSecond : parseInt(framesPerSecond) || 2 + const frames = timelapseFrames.reverse() + const numberOfFrames = timelapseFrames.length + const ke = frames[0].ke + const mid = frames[0].mid + const activeMonitor = s.group[ke].activeMonitors[mid] + const finalFileName = `${s.md5(JSON.stringify(frames))}-${framesPerSecond}fps.mp4` + const finalMp4OutputLocation = `${s.dir.fileBin}${ke}/${mid}/${finalFileName}` + const finalFileAlreadyExist = fs.existsSync(finalMp4OutputLocation) + const concatListFile = `${s.dir.streams}${ke}/${mid}/mergeJpegs_${finalFileName}.txt` + const response = { + ok: false, + ke: ke, + mid: mid, + name: finalFileName, + } + s.debugLog("activeMonitor.buildingTimelapseVideo",!!activeMonitor.buildingTimelapseVideo) + if(activeMonitor.buildingTimelapseVideo){ + s.debugLog("Timelapse Frames Video Building Already",finalMp4OutputLocation) + return activeMonitor.buildingTimelapseVideo + } + s.debugLog("finalFileAlreadyExist",finalFileAlreadyExist) + if(finalFileAlreadyExist){ + s.debugLog("Timelapse Frames Video finalFileAlreadyExist",finalMp4OutputLocation) + response.fileExists = true + response.msg = lang['Already exists'] + return response + } + if(frames.length < framesPerSecond){ + response.msg = lang.notEnoughFramesText1 + return response + } + activeMonitor.buildingTimelapseVideo = response + chunkFramesAndBuildMultipleVideosThenSticth({ + frames: frames, + listFile: concatListFile, + fps: framesPerSecond, + output: finalMp4OutputLocation, + finalFileName: finalFileName + }).then(async () => { + // videoBuildProcess exit + s.debugLog('videoBuildProcess exit',finalMp4OutputLocation) + const timeNow = new Date() + const fileStats = await fs.promises.stat(finalMp4OutputLocation) + const details = { + start: frames[0].time, + end: frames[frames.length - 1].time, + } + s.knexQuery({ + action: "insert", + table: "Files", + insert: { + ke: ke, + mid: mid, + details: s.s(details), + name: finalFileName, + size: fileStats.size, + time: timeNow, + } + }) + s.setDiskUsedForGroup(ke,fileStats.size / 1048576,'fileBin') + s.purgeDiskForGroup(ke) + s.tx({ + f: 'fileBin_item_added', + ke: ke, + mid: mid, + details: details, + name: finalFileName, + size: fileStats.size, + time: timeNow, + timelapseVideo: true, + },'GRP_'+ke); + delete(activeMonitor.buildingTimelapseVideo) + s.debugLog("Timelapse Frames Video Done!",finalMp4OutputLocation) + }) + response.ok = true + response.msg = `${lang.Building}... ${lang['Please Wait...']}` + return response + } + function initiateTimelapseVideoBuild({ + groupKey, + monitorId, + framesPerSecond, + framesPosted, + }){ + return new Promise((resolve,reject) => { + let response = {ok: false} + if(!monitorId){ + response.msg = lang['No Monitor Found, Ignoring Request'] + resolve(response) + }else{ + const frames = [] + var n = 0 + framesPosted.forEach((frame) => { + var firstParam = [['ke','=',groupKey],['mid','=',monitorId],['filename','=',frame.filename]] + if(n !== 0)firstParam[0] = (['or']).concat(firstParam[0]) + frames.push(...firstParam) + ++n + }) + s.knexQuery({ + action: "select", + columns: "*", + table: "Timelapse Frames", + where: frames + },async (err,r) => { + if(r.length > 0){ + response = await createVideoFromTimelapse(r.reverse(),framesPerSecond) + } + resolve(response) + }) + } + }) + } // Web Paths // // // // // /** @@ -177,61 +427,57 @@ module.exports = function(s,config,lang,app,io){ config.webPaths.apiPrefix+':auth/timelapse/:ke', config.webPaths.apiPrefix+':auth/timelapse/:ke/:id', config.webPaths.apiPrefix+':auth/timelapse/:ke/:id/:date', + config.webPaths.apiPrefix+':auth/cloudTimelapse/:ke', + config.webPaths.apiPrefix+':auth/cloudTimelapse/:ke/:id', + config.webPaths.apiPrefix+':auth/cloudTimelapse/:ke/:id/:date', ], function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' + const monitorId = req.params.id + const groupKey = req.params.ke + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); if( - user.permissions.watch_videos==="0" || - hasRestrictions && - ( - !user.details.video_view || - user.details.video_view.indexOf(req.params.id) === -1 + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_video_view`] || + monitorRestrictions.length === 0 ) ){ - s.closeJsonResponse(res,[]) + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], frames: []}); return } - const monitorRestrictions = s.getMonitorRestrictions(user.details,req.params.id) + var origURL = req.originalUrl.split('/') + var videoParam = origURL[origURL.indexOf(req.params.auth) + 1] + var dataSet = 'Timelapse Frames' + switch(videoParam){ + case'cloudTimelapse': + dataSet = 'Cloud Timelapse Frames' + break; + } s.getDatabaseRows({ monitorRestrictions: monitorRestrictions, - table: 'Timelapse Frames', + table: dataSet, groupKey: req.params.ke, date: req.query.date, startDate: req.query.start, endDate: req.query.end, startOperator: req.query.startOperator, endOperator: req.query.endOperator, + noLimit: req.query.noLimit, limit: req.query.limit, archived: req.query.archived, rowType: 'frames', endIsStartTo: true },(response) => { - var isMp4Call = !!(req.query.mp4 || (req.params.date && typeof req.params.date === 'string' && req.params.date.indexOf('.') > -1)) - if(isMp4Call && response.frames[0]){ - s.createVideoFromTimelapse(response.frames,req.query.fps,function(response){ - if(response.fileExists){ - if(req.query.download){ - res.setHeader('Content-Type', 'video/mp4') - s.streamMp4FileOverHttp(response.fileLocation,req,res) - }else{ - s.closeJsonResponse(res,{ - ok : response.ok, - fileExists : response.fileExists, - msg : response.msg, - }) - } - }else{ - s.closeJsonResponse(res,{ - ok : response.ok, - fileExists : response.fileExists, - msg : response.msg, - }) - } - }) - }else{ - s.closeJsonResponse(res,response.frames) - } + s.closeJsonResponse(res,response.frames) }) },res,req); }); @@ -244,54 +490,34 @@ module.exports = function(s,config,lang,app,io){ ], function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' + const groupKey = req.params.ke + const monitorId = req.params.id + const actionParameter = !!req.params.action + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) if( - user.permissions.watch_videos==="0" || - hasRestrictions && - ( - !user.details.video_view || - user.details.video_view.indexOf(req.params.id) === -1 - ) + isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_video_delete`] ){ - s.closeJsonResponse(res,[]) - return - } - const monitorRestrictions = s.getMonitorRestrictions(user.details,req.params.id) - if(monitorRestrictions.length === 0){ - s.closeJsonResponse(res,{ - ok: false - }) + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } + const framesPerSecond = s.getPostData(req, 'fps') const framesPosted = s.getPostData(req, 'frames', true) || [] - const frames = [] - var n = 0 - framesPosted.forEach((frame) => { - var firstParam = ['ke','=',req.params.ke] - if(n !== 0)firstParam = (['or']).concat(firstParam) - frames.push(firstParam,['mid','=',req.params.id],['filename','=',frame.filename]) - ++n - }) - s.knexQuery({ - action: "select", - columns: "*", - table: "Timelapse Frames", - where: frames - },(err,r) => { - if(r.length === 0){ - s.closeJsonResponse(res,{ - ok: false - }) - return - } - s.createVideoFromTimelapse(r.reverse(),s.getPostData(req, 'fps'),function(response){ - s.closeJsonResponse(res,{ - ok : response.ok, - filename : response.filename, - fileExists : response.fileExists, - msg : response.msg, - }) - }) + initiateTimelapseVideoBuild({ + groupKey, + monitorId, + framesPosted, + framesPerSecond, + }).then((buildResponse) => { + s.closeJsonResponse(res,buildResponse) }) },res,req); }); @@ -304,15 +530,31 @@ module.exports = function(s,config,lang,app,io){ ], function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' + const groupKey = req.params.ke + const monitorId = req.params.id + const actionParameter = !!req.params.action + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) if( - user.permissions.watch_videos==="0" || - hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1) + actionParameter && ( + isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_video_delete`] + ) || + !actionParameter && ( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && monitorId && !monitorPermissions[`${monitorId}_video_view`] + ) ){ - res.end(s.prettyPrint([])) + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } - const monitorRestrictions = s.getMonitorRestrictions(user.details,req.params.id) const cacheKey = req.params.ke + req.params.id + req.params.filename const processFrame = (frame) => { var fileLocation @@ -326,7 +568,7 @@ module.exports = function(s,config,lang,app,io){ selectedDate = req.params.filename.split('T')[0] } fileLocation = `${fileLocation}${frame.ke}/${frame.mid}_timelapse/${selectedDate}/${req.params.filename}` - if(req.params.action === 'delete'){ + if(actionParameter === 'delete'){ deleteTimelapseFrame({ ke: frame.ke, mid: frame.mid, @@ -356,6 +598,7 @@ module.exports = function(s,config,lang,app,io){ groupKey: req.params.ke, archived: req.query.archived, filename: req.params.filename, + limit: 1, rowType: 'frames', endIsStartTo: true },(response) => { @@ -393,39 +636,63 @@ module.exports = function(s,config,lang,app,io){ }) },res,req); }); - var buildTimelapseVideos = function(){ - var dateNow = new Date() - var hoursNow = dateNow.getHours() - if(hoursNow === 1){ - var dateNowMoment = moment(dateNow).utc().format('YYYY-MM-DDTHH:mm:ss') - var dateMinusOneDay = moment(dateNow).utc().subtract(1, 'days').format('YYYY-MM-DDTHH:mm:ss') - s.knexQuery({ - action: "select", - columns: "*", - table: "Timelapse Frames", - where: [ - ['time','=>',dateMinusOneDay], - ['time','=<',dateNowMoment], - ] - },function(err,frames) { - var groups = {} - frames.forEach(function(frame){ - if(groups[frame.ke])groups[frame.ke] = {} - if(groups[frame.ke][frame.mid])groups[frame.ke][frame.mid] = [] - groups[frame.ke][frame.mid].push(frame) + s.onOtherWebSocketMessages((d,connection) => { + switch(d.f){ + case'timelapseVideoBuild': + initiateTimelapseVideoBuild({ + groupKey: d.ke, + monitorId: d.mid, + framesPosted: d.frames, + framesPerSecond: d.fps, + }).then((buildResponse) => { + s.tx({ + f: 'timelapse_build_requested', + ke: d.ke, + mid: d.mid, + buildResponse: buildResponse, + },'GRP_'+d.ke); }) - Object.keys(groups).forEach(function(groupKey){ - Object.keys(groups[groupKey]).forEach(function(monitorId){ - var frameSet = groups[groupKey][monitorId] - s.createVideoFromTimelapse(frameSet,30,function(response){ - if(response.ok){ - - } - }) - }) - }) - }) + break; } + }) + function buildTimelapseVideos(){ + return new Promise((resolve,reject) => { + var dateNow = new Date() + var hoursNow = dateNow.getHours() + if(hoursNow === 1){ + var dateNowMoment = moment(dateNow).utc().format('YYYY-MM-DDTHH:mm:ss') + var dateMinusOneDay = moment(dateNow).utc().subtract(1, 'days').format('YYYY-MM-DDTHH:mm:ss') + s.knexQuery({ + action: "select", + columns: "*", + table: "Timelapse Frames", + where: [ + ['time','=>',dateMinusOneDay], + ['time','=<',dateNowMoment], + ] + },async function(err,frames) { + var groups = {} + frames.forEach(function(frame){ + if(groups[frame.ke])groups[frame.ke] = {} + if(groups[frame.ke][frame.mid])groups[frame.ke][frame.mid] = [] + groups[frame.ke][frame.mid].push(frame) + }) + const groupKeys = Object.keys(groups); + for (let i = 0; i < groupKeys.length; i++) { + const groupKey = groupKeys[i] + const monitorIds = Object.keys(groups[groupKey]); + for (let ii = 0; ii < monitorIds.length; ii++) { + const monitorId = monitorIds[ii] + const frameSet = groups[groupKey][monitorId] + await createVideoFromTimelapse(frameSet,30) + } + } + resolve() + }) + }else{ + resolve() + } + }) } // Auto Build Timelapse Videos if(config.autoBuildTimelapseVideosDaily === true){ diff --git a/libs/uploaders.js b/libs/uploaders.js index eb143039..f7bedf2e 100644 --- a/libs/uploaders.js +++ b/libs/uploaders.js @@ -15,6 +15,6 @@ module.exports = function(s,config,lang,app,io){ Object.keys(loadedLibraries).forEach((key) => { var loadedLib = loadedLibraries[key](s,config,lang,app,io) loadedLib.isFormGroupGroup = true - s.uploaderFields.push(loadedLib) + s.definitions["Account Settings"].blocks["Uploaders"].info.push(loadedLib) }) } diff --git a/libs/uploaders/amazonS3.js b/libs/uploaders/amazonS3.js index a16e351b..3f37796a 100644 --- a/libs/uploaders/amazonS3.js +++ b/libs/uploaders/amazonS3.js @@ -1,12 +1,12 @@ var fs = require('fs'); module.exports = function(s,config,lang){ //Amazon S3 - var beforeAccountSaveForAmazonS3 = function(d){ + function beforeAccountSave(d){ //d = save event d.formDetails.aws_use_global=d.d.aws_use_global d.formDetails.use_aws_s3=d.d.use_aws_s3 } - var cloudDiskUseStartupForAmazonS3 = function(group,userDetails){ + function cloudDiskUseStartup(group,userDetails){ group.cloudDiskUse['s3'].name = 'Amazon S3' group.cloudDiskUse['s3'].sizeLimitCheck = (userDetails.use_aws_s3_size_limit === '1') if(!userDetails.aws_s3_size_limit || userDetails.aws_s3_size_limit === ''){ @@ -15,7 +15,7 @@ module.exports = function(s,config,lang){ group.cloudDiskUse['s3'].sizeLimit = parseFloat(userDetails.aws_s3_size_limit) } } - var loadAmazonS3ForUser = function(e){ + function loadGroupApp(e){ // e = user var userDetails = JSON.parse(e.details) if(userDetails.aws_use_global === '1' && config.cloudUploaders && config.cloudUploaders.AmazonS3){ @@ -54,11 +54,11 @@ module.exports = function(s,config,lang){ s.group[e.ke].aws_s3 = new s.group[e.ke].aws.S3(); } } - var unloadAmazonS3ForUser = function(user){ + function unloadGroupApp(user){ s.group[user.ke].aws = null s.group[user.ke].aws_s3 = null } - var deleteVideoFromAmazonS3 = function(e,video,callback){ + function deleteVideo(e,video,callback){ // e = user try{ var videoDetails = JSON.parse(video.details) @@ -68,6 +68,9 @@ module.exports = function(s,config,lang){ if(!videoDetails.location){ videoDetails.location = video.href.split('.amazonaws.com')[1] } + if(videoDetails.type !== 's3'){ + return + } s.group[e.ke].aws_s3.deleteObject({ Bucket: s.group[e.ke].init.aws_s3_bucket, Key: videoDetails.location, @@ -76,7 +79,7 @@ module.exports = function(s,config,lang){ callback() }); } - var uploadVideoToAmazonS3 = function(e,k){ + function uploadVideo(e,k){ //e = video object //k = temporary values if(!k)k={}; @@ -92,9 +95,8 @@ module.exports = function(s,config,lang){ s.group[e.ke].aws_s3.upload({ Bucket: s.group[e.ke].init.aws_s3_bucket, Key: saveLocation, - Body:fileStream, - ACL:'public-read', - ContentType:'video/'+ext + Body: fileStream, + ContentType: 'video/'+ext },function(err,data){ if(err){ s.userLog(e,{type:lang['Amazon S3 Upload Error'],msg:err}) @@ -114,7 +116,7 @@ module.exports = function(s,config,lang){ }), size: k.filesize, end: k.endTime, - href: data.Location + href: '' } }) s.setCloudDiskUsedForGroup(e.ke,{ @@ -126,7 +128,7 @@ module.exports = function(s,config,lang){ }) } } - var onInsertTimelapseFrame = function(monitorObject,queryInfo,filePath){ + function onInsertTimelapseFrame(monitorObject,queryInfo,filePath){ var e = monitorObject if(s.group[e.ke].aws_s3 && s.group[e.ke].init.use_aws_s3 !== '0' && s.group[e.ke].init.aws_s3_save === '1'){ var fileStream = fs.createReadStream(filePath) @@ -152,6 +154,7 @@ module.exports = function(s,config,lang){ mid: queryInfo.mid, ke: queryInfo.ke, time: queryInfo.time, + filename: queryInfo.filename, details: s.s({ type : 's3', location : saveLocation @@ -169,7 +172,7 @@ module.exports = function(s,config,lang){ }) } } - var onDeleteTimelapseFrameFromCloud = function(e,frame,callback){ + function onDeleteTimelapseFrameFromCloud(e,frame,callback){ // e = user try{ var frameDetails = JSON.parse(frame.details) @@ -190,18 +193,30 @@ module.exports = function(s,config,lang){ callback() }); } + function onGetVideoData(video){ + const videoDetails = s.parseJSON(video.details) + return new Promise((resolve, reject) => { + const saveLocation = videoDetails.location + var fileStream = s.group[video.ke].aws_s3.getObject({ + Bucket: s.group[video.ke].init.aws_s3_bucket, + Key: saveLocation, + }).createReadStream(); + resolve(fileStream) + }) + } //amazon s3 s.addCloudUploader({ name: 's3', - loadGroupAppExtender: loadAmazonS3ForUser, - unloadGroupAppExtender: unloadAmazonS3ForUser, - insertCompletedVideoExtender: uploadVideoToAmazonS3, - deleteVideoFromCloudExtensions: deleteVideoFromAmazonS3, - cloudDiskUseStartupExtensions: cloudDiskUseStartupForAmazonS3, - beforeAccountSave: beforeAccountSaveForAmazonS3, - onAccountSave: cloudDiskUseStartupForAmazonS3, + loadGroupAppExtender: loadGroupApp, + unloadGroupAppExtender: unloadGroupApp, + insertCompletedVideoExtender: uploadVideo, + deleteVideoFromCloudExtensions: deleteVideo, + cloudDiskUseStartupExtensions: cloudDiskUseStartup, + beforeAccountSave: beforeAccountSave, + onAccountSave: cloudDiskUseStartup, onInsertTimelapseFrame: onInsertTimelapseFrame, - onDeleteTimelapseFrameFromCloud: onDeleteTimelapseFrameFromCloud + onDeleteTimelapseFrameFromCloud: onDeleteTimelapseFrameFromCloud, + onGetVideoData }) //return fields that will appear in settings return { diff --git a/libs/uploaders/backblazeB2.js b/libs/uploaders/backblazeB2.js index 9bc432d4..5ad505c5 100644 --- a/libs/uploaders/backblazeB2.js +++ b/libs/uploaders/backblazeB2.js @@ -57,6 +57,10 @@ module.exports = function(s,config,lang){ b2.listBuckets().then(function(resp){ var buckets = resp.buckets var bucketN = -2 + if(!buckets){ + s.userLog({mid:'$USER',ke:e.ke},{type: lang['Backblaze Error'],msg: lang['Not Authorized']}) + return + } buckets.forEach(function(item,n){ if(item.bucketName === userDetails.bb_b2_bucket){ bucketN = n diff --git a/libs/uploaders/googleDrive.js b/libs/uploaders/googleDrive.js index 9078b031..9822f6da 100644 --- a/libs/uploaders/googleDrive.js +++ b/libs/uploaders/googleDrive.js @@ -118,6 +118,9 @@ module.exports = (s,config,lang,app,io) => { var deleteVideoFromGoogleDrive = function(groupKey,video,callback){ // e = user var videoDetails = s.parseJSON(video.details) + if(videoDetails.type !== 'googd'){ + return + } s.group[groupKey].googleDrive.files.delete({ fileId: videoDetails.id }, function(err, resp){ diff --git a/libs/uploaders/s3based.js b/libs/uploaders/s3based.js index 23df56c8..2ff81825 100644 --- a/libs/uploaders/s3based.js +++ b/libs/uploaders/s3based.js @@ -84,6 +84,9 @@ module.exports = function(s,config,lang){ if(!videoDetails.location){ videoDetails.location = video.href.split(locationUrl)[1] } + if(videoDetails.type !== 'whcs'){ + return + } s.group[e.ke].whcs.deleteObject({ Bucket: s.group[e.ke].init.whcs_bucket, Key: videoDetails.location, @@ -114,9 +117,8 @@ module.exports = function(s,config,lang){ s.group[e.ke].whcs.upload({ Bucket: bucketName, Key: saveLocation, - Body:fileStream, - ACL:'public-read', - ContentType:'video/'+ext + Body: fileStream, + ContentType: 'video/'+ext },options,function(err,data){ if(err){ console.error(err) @@ -177,6 +179,7 @@ module.exports = function(s,config,lang){ mid: queryInfo.mid, ke: queryInfo.ke, time: queryInfo.time, + filename: queryInfo.filename, details: s.s({ type : 'whcs', location : saveLocation @@ -231,6 +234,17 @@ module.exports = function(s,config,lang){ } return cloudLink } + function onGetVideoData(video){ + const videoDetails = s.parseJSON(video.details) + return new Promise((resolve, reject) => { + const saveLocation = videoDetails.location + var fileStream = s.group[video.ke].whcs.getObject({ + Bucket: s.group[video.ke].init.whcs_bucket, + Key: saveLocation, + }).createReadStream(); + resolve(fileStream) + }) + } //wasabi s.addCloudUploader({ name: 'whcs', @@ -242,7 +256,8 @@ module.exports = function(s,config,lang){ beforeAccountSave: beforeAccountSaveForWasabiHotCloudStorage, onAccountSave: cloudDiskUseStartupForWasabiHotCloudStorage, onInsertTimelapseFrame: onInsertTimelapseFrame, - onDeleteTimelapseFrameFromCloud: onDeleteTimelapseFrameFromCloud + onDeleteTimelapseFrameFromCloud: onDeleteTimelapseFrameFromCloud, + onGetVideoData }) return { "evaluation": "details.use_whcs !== '0'", diff --git a/libs/uploaders/webdav.js b/libs/uploaders/webdav.js index ca9dc9ae..9d84430a 100644 --- a/libs/uploaders/webdav.js +++ b/libs/uploaders/webdav.js @@ -41,11 +41,10 @@ module.exports = function(s,config,lang){ userDetails.webdav_dir='/' } userDetails.webdav_dir = s.checkCorrectPathEnding(userDetails.webdav_dir) - s.group[e.ke].webdav = webdav( - userDetails.webdav_url, - userDetails.webdav_user, - userDetails.webdav_pass - ) + s.group[e.ke].webdav = webdav.createAdapter(userDetails.webdav_url, { + username: userDetails.webdav_user, + password: userDetails.webdav_pass + }) } } var unloadWebDavForUser = function(user){ @@ -95,7 +94,7 @@ module.exports = function(s,config,lang){ }), size: k.filesize, end: k.endTime, - href: webdavRemoteUrl + href: '' } }) s.setCloudDiskUsedForGroup(e.ke,{ @@ -158,6 +157,62 @@ module.exports = function(s,config,lang){ } } } + function onInsertTimelapseFrame(monitorObject,queryInfo,filePath){ + var e = monitorObject + if(s.group[e.ke].webdav && s.group[e.ke].init.use_webdav !== '0' && s.group[e.ke].init.webdav_save === '1'){ + const wfs = s.group[e.ke].webdav + const saveLocation = s.group[e.ke].init.webdav_dir+e.ke+'/'+e.mid+'_timelapse/' + queryInfo.filename + fs.createReadStream(filePath).pipe(wfs.createWriteStream(saveLocation)) + if(s.group[e.ke].init.webdav_log === '1'){ + s.knexQuery({ + action: "insert", + table: "Cloud Timelapse Frames", + insert: { + mid: queryInfo.mid, + ke: queryInfo.ke, + time: queryInfo.time, + filename: queryInfo.filename, + details: s.s({ + type : 'webdav', + location : saveLocation + }), + size: queryInfo.size, + href: '' + } + }) + s.setCloudDiskUsedForGroup(e.ke,{ + amount : s.kilobyteToMegabyte(queryInfo.size), + storageType : 'webdav' + },'timelapseFrames') + s.purgeCloudDiskForGroup(e,'webdav','timelapseFrames') + } + } + } + function onDeleteTimelapseFrameFromCloud(e,frame,callback){ + // e = user + try{ + var frameDetails = JSON.parse(frame.details) + }catch(err){ + var frameDetails = frame.details + } + if(frameDetails.type !== 'webdav'){ + return + } + if(!frameDetails.location){ + frameDetails.location = frame.href.split(locationUrl)[1] + } + s.group[e.ke].webdav.unlink(frameDetails.location, function(err) { + if (err) console.log(frameDetails.location,err) + callback() + }) + } + async function onGetVideoData(video){ + const wfs = s.group[video.ke].webdav + const videoDetails = s.parseJSON(video.details) + const saveLocation = videoDetails.location + const fileStream = wfs.createReadStream(saveLocation); + return fileStream + } //webdav s.addCloudUploader({ name: 'webdav', @@ -168,6 +223,9 @@ module.exports = function(s,config,lang){ cloudDiskUseStartupExtensions: cloudDiskUseStartupForWebDav, beforeAccountSave: beforeAccountSaveForWebDav, onAccountSave: cloudDiskUseStartupForWebDav, + onInsertTimelapseFrame, + onDeleteTimelapseFrameFromCloud, + onGetVideoData }) return { "evaluation": "details.use_webdav !== '0'", diff --git a/libs/user.js b/libs/user.js index a994d998..71ab5232 100644 --- a/libs/user.js +++ b/libs/user.js @@ -14,6 +14,7 @@ module.exports = function(s,config,lang){ deleteFileBinFiles, deleteCloudVideos, deleteCloudTimelapseFrames, + resetAllStorageCounters, } = require("./user/utils.js")(s,config,lang); let purgeDiskGroup = () => {} const runQuery = async.queue(function(groupKey, callback) { @@ -218,15 +219,19 @@ module.exports = function(s,config,lang){ } //change global size value s.group[e.ke].usedSpace += currentChange + s.group[e.ke].usedSpace = s.group[e.ke].usedSpace < 0 ? 0 : s.group[e.ke].usedSpace switch(storageType){ case'timelapeFrames': s.group[e.ke].usedSpaceTimelapseFrames += currentChange + s.group[e.ke].usedSpaceTimelapseFrames = s.group[e.ke].usedSpaceTimelapseFrames < 0 ? 0 : s.group[e.ke].usedSpaceTimelapseFrames break; case'fileBin': s.group[e.ke].usedSpaceFilebin += currentChange + s.group[e.ke].usedSpaceFilebin = s.group[e.ke].usedSpaceFilebin < 0 ? 0 : s.group[e.ke].usedSpaceFilebin break; default: s.group[e.ke].usedSpaceVideos += currentChange + s.group[e.ke].usedSpaceVideos = s.group[e.ke].usedSpaceVideos < 0 ? 0 : s.group[e.ke].usedSpaceVideos break; } //remove value just used from queue @@ -267,6 +272,12 @@ module.exports = function(s,config,lang){ } }) } + function filterMonitorListOrder(groupKey,details){ + const loadedMonitors = s.group[groupKey].rawMonitorConfigurations + var monitorListOrder = (details.monitorListOrder && details.monitorListOrder[0] ? details.monitorListOrder[0] : []).filter(monitorId => !!loadedMonitors[monitorId]); + monitorListOrder = [...new Set(monitorListOrder)]; + return monitorListOrder + } s.accountSettingsEdit = function(d,dontRunExtensions){ s.knexQuery({ action: "select", @@ -302,6 +313,7 @@ module.exports = function(s,config,lang){ } //admin permissions formDetails.permissions = details.permissions + formDetails.max_camera = details.max_camera formDetails.edit_size = details.edit_size formDetails.edit_days = details.edit_days formDetails.use_admin = details.use_admin @@ -358,7 +370,9 @@ module.exports = function(s,config,lang){ } readStorageArray() /// - formDetails = JSON.stringify(s.mergeDeep(details,formDetails)) + formDetails = s.mergeDeep(details,formDetails) + if(formDetails.monitorListOrder)formDetails.monitorListOrder[0] = filterMonitorListOrder(d.ke,formDetails); + formDetailsString = JSON.stringify(s.mergeDeep(details,formDetails)) /// const updateQuery = {} if(form.pass && form.pass !== ''){ @@ -371,7 +385,7 @@ module.exports = function(s,config,lang){ const value = form[key] updateQuery[key] = value }) - updateQuery.details = formDetails + updateQuery.details = formDetailsString s.knexQuery({ action: "update", table: "Users", @@ -381,20 +395,20 @@ module.exports = function(s,config,lang){ ['uid','=',d.uid], ] },() => { + const user = Object.assign({ke : d.ke},form) if(!details.sub){ - var user = Object.assign(form,{ke : d.ke}) - var userDetails = JSON.parse(formDetails) s.group[d.ke].sizeLimit = parseFloat(newSize) + resetAllStorageCounters(d.ke) if(!dontRunExtensions){ - s.onAccountSaveExtensions.forEach(function(extender){ - extender(s.group[d.ke],userDetails,user) - }) s.unloadGroupAppExtensions.forEach(function(extender){ extender(user) }) s.loadGroupApps(d) } } + s.onAccountSaveExtensions.forEach(function(extender){ + extender(s.group[d.ke],formDetails,user) + }) if(d.cnid)s.tx({f:'user_settings_change',uid:d.uid,ke:d.ke,form:form},d.cnid) }) } diff --git a/libs/user/startup.js b/libs/user/startup.js new file mode 100644 index 00000000..cfc5dea6 --- /dev/null +++ b/libs/user/startup.js @@ -0,0 +1,45 @@ +module.exports = function(s,config,lang,io){ + const { + createAdminUser + } = require("./utils.js")(s,config,lang); + function checkStaticUser(staticUser){ + return new Promise((resolve,reject) => { + const whereQuery = { + mail: staticUser.mail + } + if(staticUser.ke)whereQuery.ke = staticUser.ke + s.knexQuery({ + action: "select", + columns: "mail,ke,uid", + table: "Users", + where: whereQuery, + limit: 1, + },function(err,users) { + resolve(users[0]) + }) + }) + } + async function checkForStaticUsers(){ + if(config.staticUsers){ + try{ + for (let i = 0; i < config.staticUsers.length; i++) { + const staticUser = config.staticUsers[i] + s.debugLog(`Checking Static User...`,staticUser.mail) + const userExists = await checkStaticUser(staticUser) + if(!userExists){ + s.debugLog(`Static User does not exist, creating...`) + const creationResponse = await createAdminUser(staticUser) + s.debugLog(`Static User created!`,creationResponse) + }else{ + s.debugLog(`Static User exists!`) + } + } + }catch(err){ + s.debugLog(`Static User check error!`,err) + } + } + } + return { + checkForStaticUsers, + } +} diff --git a/libs/user/utils.js b/libs/user/utils.js index 3159ddd0..926444d9 100644 --- a/libs/user/utils.js +++ b/libs/user/utils.js @@ -142,7 +142,6 @@ module.exports = (s,config,lang) => { const groupKey = options.groupKey const err = options.err const files = options.files - const storageIndex = options.storageIndex var whereGroup = [] var whereQuery = [ ['ke','=',groupKey], @@ -151,7 +150,9 @@ module.exports = (s,config,lang) => { var completedCheck = 0 if(files){ files.forEach(function(file){ + var dir = s.getFileBinDirectory(file) + s.debugLog(`deleting FileBin File`,`${file}`,dir) var fileLocationMid = `${dir}` + file.name const queryGroup = { mid: file.mid, @@ -180,14 +181,7 @@ module.exports = (s,config,lang) => { }) } }) - if(storageIndex){ - s.setDiskUsedForGroupAddStorage(groupKey,{ - size: -(file.size/1048576), - storageIndex: storageIndex - },'fileBin') - }else{ - s.setDiskUsedForGroup(groupKey,-(file.size/1048576),'fileBin') - } + s.setDiskUsedForGroup(groupKey,-(file.size/1048576),'fileBin') }) }else{ console.log(err) @@ -227,7 +221,7 @@ module.exports = (s,config,lang) => { where: [ ['ke','=',groupKey], ['status','!=','0'], - ['details','NOT LIKE',`%"archived":"1"%`], + ['archive','!=',`1`], ['details','LIKE',`%"dir":"${storage.value}"%`], ], orderBy: ['time','asc'], @@ -272,7 +266,7 @@ module.exports = (s,config,lang) => { where: [ ['ke','=',groupKey], ['status','!=','0'], - ['details','NOT LIKE',`%"archived":"1"%`], + ['archive','!=',`1`], ['details','NOT LIKE',`%"dir"%`], ], orderBy: ['time','asc'], @@ -294,14 +288,17 @@ module.exports = (s,config,lang) => { } const deleteTimelapseFrames = function(groupKey,callback){ //run purge command - if(s.group[groupKey].usedSpaceTimelapseFrames > (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitTimelapseFramesPercent / 100) * config.cron.deleteOverMaxOffset)){ + const maxSize = (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitTimelapseFramesPercent / 100) * config.cron.deleteOverMaxOffset); + const currentlyUsedSize = s.group[groupKey].usedSpaceTimelapseFrames + s.debugLog(`deleteTimelapseFrames`,`${currentlyUsedSize}/${maxSize}`) + if(currentlyUsedSize > maxSize){ s.knexQuery({ action: "select", columns: "*", table: "Timelapse Frames", where: [ ['ke','=',groupKey], - ['details','NOT LIKE',`%"archived":"1"%`], + ['archive','!=',`1`], ], orderBy: ['time','asc'], limit: 3 @@ -319,14 +316,17 @@ module.exports = (s,config,lang) => { } const deleteFileBinFiles = function(groupKey,callback){ if(config.deleteFileBinsOverMax === true){ - //run purge command - if(s.group[groupKey].usedSpaceFileBin > (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitFileBinPercent / 100) * config.cron.deleteOverMaxOffset)){ + const maxSize = (s.group[groupKey].sizeLimit * (s.group[groupKey].sizeLimitFileBinPercent / 100) * config.cron.deleteOverMaxOffset); + const currentlyUsedSize = s.group[groupKey].usedSpaceFilebin + s.debugLog(`deleteFileBinFiles`,`${currentlyUsedSize}/${maxSize}`) + if(currentlyUsedSize > maxSize){ s.knexQuery({ action: "select", columns: "*", table: "Files", where: [ ['ke','=',groupKey], + ['archive','!=',`1`], ], orderBy: ['time','asc'], limit: 1 @@ -335,7 +335,6 @@ module.exports = (s,config,lang) => { groupKey: groupKey, err: err, files: files, - storageIndex: null },callback) }) }else{ @@ -410,7 +409,6 @@ module.exports = (s,config,lang) => { table: "Cloud Timelapse Frames", where: [ ['ke','=',groupKey], - ['details','NOT LIKE',`%"archived":"1"%`], ], orderBy: ['time','asc'], limit: 3 @@ -451,6 +449,61 @@ module.exports = (s,config,lang) => { callback() } } + function resetAllStorageCounters(groupKey){ + var storageIndexes = Object.keys(s.group[groupKey].addStorageUse) + storageIndexes.forEach((storageIndex) => { + s.setDiskUsedForGroupAddStorage(groupKey,{ + size: 0, + storageIndex: storageIndex + }) + }) + s.setDiskUsedForGroup(groupKey,0) + } + function createAdminUser(user){ + return new Promise((resolve,reject) => { + const detailsColumn = Object.assign({ + "factorAuth":"0", + "size": user.diskLimit || user.size || '', + "days":"", + "event_days":"", + "log_days":"", + "max_camera": user.cameraLimit || user.max_camera || '', + "permissions":"all", + "edit_size":"1", + "edit_days":"1", + "edit_event_days":"1", + "edit_log_days":"1", + "use_admin":"1", + "use_aws_s3":"1", + "use_whcs":"1", + "use_sftp":"1", + "use_webdav":"1", + "use_discordbot":"1", + "use_ldap":"1", + "aws_use_global":"0", + "b2_use_global":"0", + "webdav_use_global":"0" + },s.parseJSON(user.details) || {}); + const insertQuery = { + ke: user.ke || s.gid(7), + uid: user.uid || s.gid(6), + mail: user.mail, + pass: s.createHash(user.initialPassword || user.pass || s.gid()), + details: JSON.stringify(detailsColumn) + } + s.knexQuery({ + action: "insert", + table: "Users", + insert: insertQuery + },function(err,users) { + resolve({ + ok: !err, + inserted: !err ? insertQuery : undefined, + err: err + }) + }) + }) + } return { deleteSetOfVideos: deleteSetOfVideos, deleteSetOfTimelapseFrames: deleteSetOfTimelapseFrames, @@ -461,5 +514,7 @@ module.exports = (s,config,lang) => { deleteFileBinFiles: deleteFileBinFiles, deleteCloudVideos: deleteCloudVideos, deleteCloudTimelapseFrames: deleteCloudTimelapseFrames, + resetAllStorageCounters: resetAllStorageCounters, + createAdminUser: createAdminUser, } } diff --git a/libs/video/utils.js b/libs/video/utils.js index c36d21c5..d8020a17 100644 --- a/libs/video/utils.js +++ b/libs/video/utils.js @@ -1,6 +1,15 @@ const fs = require('fs') const { spawn } = require('child_process') +const async = require('async'); module.exports = (s,config,lang) => { + const { + ffprobe, + splitForFFPMEG, + } = require('../ffmpeg/utils.js')(s,config,lang) + const { + copyFile, + hmsToSeconds, + } = require('../basic/utils.js')(process.cwd(),config) // orphanedVideoCheck : new function const checkIfVideoIsOrphaned = (monitor,videosDirectory,filename) => { const response = {ok: true} @@ -59,15 +68,15 @@ module.exports = (s,config,lang) => { // const findCmd = [videosDirectory].concat(options.flags || ['-maxdepth','1']) fs.writeFileSync( tempDirectory + 'orphanCheck.sh', - `find "${videosDirectory}" -maxdepth 1 -type f -exec stat -c "%n" {} + | sort -r | head -n ${options.checkMax}` + `find "${s.checkCorrectPathEnding(videosDirectory,true)}" -maxdepth 1 -type f -exec stat -c "%n" {} + | sort -r | head -n ${options.checkMax}` ); let listing = spawn('sh',[tempDirectory + 'orphanCheck.sh']) // const onData = options.onData ? options.onData : () => {} const onError = options.onError ? options.onError : s.systemLog - const onExit = () => { + const onExit = async () => { try{ listing.kill('SIGTERM') - fs.unlink(tempDirectory + 'orphanCheck.sh',() => {}) + await fs.promises.rm(tempDirectory + 'orphanCheck.sh') }catch(err){ s.debugLog(err) } @@ -187,7 +196,7 @@ module.exports = (s,config,lang) => { let fileExt = inputFilePath.split('.') fileExt = fileExt[fileExt.length -1] const filename = `${s.gid(10)}.${fileExt}` - const videoOutPath = `${tempDirectory}` + const videoOutPath = `${tempDirectory}${filename}` const cuttingProcess = spawn(config.ffmpegDir,['-loglevel','warning','-i', inputFilePath, '-c','copy','-t',`${cutLength}`,videoOutPath]) cuttingProcess.stderr.on('data',(data) => { const err = data.toString() @@ -196,6 +205,7 @@ module.exports = (s,config,lang) => { cuttingProcess.on('close',(data) => { fs.stat(videoOutPath,(err) => { if(!err){ + response.ok = true response.filename = filename response.filePath = videoOutPath setTimeout(() => { @@ -209,9 +219,330 @@ module.exports = (s,config,lang) => { }) }) } + async function getVideosBasedOnTagFoundInMatrixOfAssociatedEvent({ + groupKey, + monitorId, + startTime, + endTime, + searchQuery, + monitorRestrictions + }){ + const initialEventQuery = [ + ['ke','=',groupKey], + ['objects','LIKE',`%${searchQuery}%`], + ] + if(monitorId)initialEventQuery.push(['mid','=',monitorId]); + if(startTime)initialEventQuery.push(['time','>',startTime]); + if(endTime)initialEventQuery.push(['end','<',endTime]); + if(monitorRestrictions)initialEventQuery.push(monitorRestrictions); + const videoSelectResponse = await s.knexQueryPromise({ + action: "select", + columns: "*", + table: "Videos", + orderBy: ['time','desc'], + where: initialEventQuery + }); + return videoSelectResponse + } + async function stitchMp4Files(options){ + return new Promise((resolve,reject) => { + const concatListFile = options.listFile + const finalMp4OutputLocation = options.output + const commandString = `-y -threads 1 -f concat -safe 0 -i "${concatListFile}" -c:v copy -an -preset ultrafast "${finalMp4OutputLocation}"` + s.debugLog("stitchMp4Files",commandString) + const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString)) + videoBuildProcess.stdout.on('data',function(data){ + s.debugLog('stdout',finalMp4OutputLocation,data.toString()) + }) + videoBuildProcess.stderr.on('data',function(data){ + s.debugLog('stderr',finalMp4OutputLocation,data.toString()) + }) + videoBuildProcess.on('exit',async function(data){ + resolve() + }) + }) + } + const fixingAlready = {} + function reEncodeVideoAndReplace(videoRow){ + return new Promise((resolve,reject) => { + const response = {ok: true} + const fixingId = `${videoRow.ke}${videoRow.mid}${videoRow.time}` + if(fixingAlready[fixingId]){ + response.ok = false + response.msg = lang['Already Processing'] + resolve(response) + }else{ + const filename = s.formattedTime(videoRow.time)+'.'+videoRow.ext + const tempFilename = s.formattedTime(videoRow.time)+'.reencoding.'+videoRow.ext + const videoFolder = s.getVideoDirectory(videoRow) + const inputFilePath = `${videoFolder}${filename}` + const outputFilePath = `${videoFolder}${tempFilename}` + const commandString = `-y -threads 1 -re -i "${inputFilePath}" -c:v copy -c:a copy -preset ultrafast "${outputFilePath}"` + fixingAlready[fixingId] = true + const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString)) + videoBuildProcess.stdout.on('data',function(data){ + s.debugLog('stdout',outputFilePath,data.toString()) + }) + videoBuildProcess.stderr.on('data',function(data){ + s.debugLog('stderr',outputFilePath,data.toString()) + }) + videoBuildProcess.on('exit',async function(data){ + fixingAlready[fixingId] = false + try{ + function failed(err){ + response.ok = false + response.err = err + resolve(response) + } + const newFileStats = await fs.promises.stat(outputFilePath) + await fs.promises.rm(inputFilePath) + let readStream = fs.createReadStream(outputFilePath); + let writeStream = fs.createWriteStream(inputFilePath); + readStream.pipe(writeStream); + writeStream.on('finish', async () => { + resolve(response) + await fs.promises.rm(outputFilePath) + }); + writeStream.on('error', failed); + readStream.on('error', failed); + }catch(err){ + failed() + } + }) + } + }) + } + const reEncodeVideoAndBinOriginalQueue = {} + function reEncodeVideoAndBinOriginalAddToQueue(data){ + const groupKey = data.video.ke + if(!reEncodeVideoAndBinOriginalQueue[groupKey]){ + reEncodeVideoAndBinOriginalQueue[groupKey] = async.queue(async function(data, callback) { + const response = await reEncodeVideoAndBinOriginal(data) + callback(response) + }, 1); + } + return new Promise((resolve) => { + reEncodeVideoAndBinOriginalQueue[groupKey].push(data,(response) => { + resolve(response) + }) + }) + } + function reEncodeVideoAndBinOriginal({ + video, + targetVideoCodec, + targetAudioCodec, + targetQuality, + targetExtension, + doSlowly, + onPercentChange, + automated, + }){ + targetVideoCodec = targetVideoCodec || `copy` + targetAudioCodec = targetAudioCodec || `copy` + targetQuality = targetQuality || `` + onPercentChange = onPercentChange || function(){}; + if(!targetVideoCodec || !targetAudioCodec){ + switch(targetExtension){ + case'mp4': + targetVideoCodec = `libx264` + targetAudioCodec = `aac -strict -2` + targetQuality = `-crf 1` + break; + case'webm': + case'mkv': + targetVideoCodec = `vp9` + targetAudioCodec = `libopus` + targetQuality = `-q:v 1 -q:a 1` + break; + } + } + const response = {ok: true} + const groupKey = video.ke + const monitorId = video.mid + const filename = s.formattedTime(video.time)+'.'+video.ext + const tempFilename = s.formattedTime(video.time)+'.reencoding.'+ targetExtension + const finalFilename = s.formattedTime(video.time)+'.'+ targetExtension + const tempFolder = s.getStreamsDirectory(video) + const videoFolder = s.getVideoDirectory(video) + const fileBinFolder = s.getFileBinDirectory(video) + const inputFilePath = `${videoFolder}${filename}` + const fileBinFilePath = `${fileBinFolder}${filename}` + const outputFilePath = `${tempFolder}${tempFilename}` + const finalFilePath = `${videoFolder}${finalFilename}` + const fixingId = `${video.ke}${video.mid}${video.time}` + return new Promise(async (resolve,reject) => { + function completeResolve(data){ + s.tx({ + f: 'video_compress_completed', + ke: groupKey, + mid: monitorId, + oldName: filename, + name: finalFilename, + automated: !!automated, + success: !!data.ok, + },'GRP_'+groupKey); + resolve(data) + } + try{ + if(fixingAlready[fixingId]){ + response.ok = false + response.msg = lang['Already Processing'] + resolve(response) + }else{ + const inputFileStats = await fs.promises.stat(inputFilePath) + const originalFileInfo = (await ffprobe(inputFilePath,inputFilePath)).result + const videoDuration = originalFileInfo.format.duration + const commandString = `-y ${doSlowly ? `-re -threads 1` : ''} -i "${inputFilePath}" -c:v ${targetVideoCodec} -c:a ${targetAudioCodec} ${targetQuality} "${outputFilePath}"` + fixingAlready[fixingId] = true + s.tx({ + f: 'video_compress_started', + ke: groupKey, + mid: monitorId, + oldName: filename, + name: finalFilename, + },'GRP_'+groupKey); + const videoBuildProcess = spawn(config.ffmpegDir,splitForFFPMEG(commandString)) + videoBuildProcess.stdout.on('data',function(data){ + s.debugLog('stdout',outputFilePath,data.toString()) + }) + videoBuildProcess.stderr.on('data',function(data){ + const text = data.toString() + if(text.includes('frame=')){ + const durationSoFar = hmsToSeconds(text.split('time=')[1].trim().split(/(\s+)/)[0]) + const percent = (durationSoFar / videoDuration * 100).toFixed(1) + s.tx({ + f: 'video_compress_percent', + ke: groupKey, + mid: monitorId, + oldName: filename, + name: finalFilename, + percent: percent, + },'GRP_'+groupKey); + onPercentChange(percent) + s.debugLog('stderr',outputFilePath,`${percent}%`) + } + }) + videoBuildProcess.on('exit',async function(data){ + fixingAlready[fixingId] = false + try{ + // check that new file is existing + const newFileStats = await fs.promises.stat(outputFilePath) + // move old file to fileBin + await copyFile(inputFilePath,fileBinFilePath) + const fileBinInsertQuery = { + ke: video.ke, + mid: video.mid, + name: filename, + size: video.size, + details: video.details, + status: video.status, + time: video.time, + } + await s.insertFileBinEntry(fileBinInsertQuery) + // delete original + await s.deleteVideo(video) + // copy temp file to final path + await copyFile(outputFilePath,finalFilePath) + await fs.promises.rm(outputFilePath) + s.insertCompletedVideo({ + id: video.mid, + mid: video.mid, + ke: video.ke, + ext: targetExtension, + },{ + file: finalFilename, + objects: video.objects, + endTime: video.end, + ext: targetExtension, + },function(){ + completeResolve({ + ok: true, + path: finalFilePath, + time: video.time, + fileBin: fileBinInsertQuery, + videoCodec: targetVideoCodec, + audioCodec: targetAudioCodec, + videoQuality: targetQuality, + }) + }) + }catch(err){ + response.ok = false + response.err = err + completeResolve(response) + } + }) + } + }catch(err){ + response.ok = false + response.err = err + completeResolve(response) + } + }) + } + function archiveVideo(video,unarchive){ + return new Promise((resolve) => { + s.knexQuery({ + action: "update", + table: 'Videos', + update: { + archive: unarchive ? '0' : 1 + }, + where: { + ke: video.ke, + mid: video.mid, + time: video.time, + } + },function(errVideos){ + s.knexQuery({ + action: "update", + table: 'Events', + update: { + archive: unarchive ? '0' : 1 + }, + where: [ + ['ke','=',video.ke], + ['mid','=',video.mid], + ['time','>=',video.time], + ['time','<=',video.end], + ] + },function(errEvents){ + s.knexQuery({ + action: "update", + table: 'Timelapse Frames', + update: { + archive: unarchive ? '0' : 1 + }, + limit: 1, + where: [ + ['ke','=',video.ke], + ['mid','=',video.mid], + ['time','>=',video.time], + ['time','<=',video.end], + ] + },function(errTimelapseFrames){ + resolve({ + ok: !errVideos && !errEvents && !errTimelapseFrames, + err: errVideos || errEvents || errTimelapseFrames ? { + errVideos, + errEvents, + errTimelapseFrames, + } : undefined, + archived: !unarchive + }) + }) + }) + }) + }) + } return { - orphanedVideoCheck: orphanedVideoCheck, - scanForOrphanedVideos: scanForOrphanedVideos, - cutVideoLength: cutVideoLength, + reEncodeVideoAndReplace, + stitchMp4Files, + orphanedVideoCheck, + scanForOrphanedVideos, + cutVideoLength, + getVideosBasedOnTagFoundInMatrixOfAssociatedEvent, + reEncodeVideoAndBinOriginal, + reEncodeVideoAndBinOriginalAddToQueue, + archiveVideo, } } diff --git a/libs/videos.js b/libs/videos.js index 3731874c..4249311e 100644 --- a/libs/videos.js +++ b/libs/videos.js @@ -2,6 +2,9 @@ var fs = require('fs'); var exec = require('child_process').exec; var spawn = require('child_process').spawn; module.exports = function(s,config,lang){ + const { + sendVideoToMasterNode, + } = require('./childNode/childUtils.js')(s,config,lang) /** * Gets the video directory of the supplied video or monitor database row. * @constructor @@ -60,30 +63,38 @@ module.exports = function(s,config,lang){ }) } s.insertDatabaseRow = function(e,k,callback){ - s.checkDetails(e) - //save database row - if(!k.details)k.details = {} - if(e.details && e.details.dir && e.details.dir !== ''){ - k.details.dir = e.details.dir - } - if(config.useUTC === true)k.details.isUTC = config.useUTC; - s.knexQuery({ - action: "insert", - table: "Videos", - insert: { + return new Promise((resolve) => { + s.checkDetails(e) + //save database row + if(!k.details)k.details = {} + if(e.details && e.details.dir && e.details.dir !== ''){ + k.details.dir = e.details.dir + } + const insertQuery = { ke: e.ke, mid: e.mid, time: k.startTime, - ext: e.ext, + ext: k.ext || e.ext, status: 1, details: s.s(k.details), + objects: k.objects || '', size: k.filesize, end: k.endTime, } - },(err) => { - if(callback)callback(err) - fs.chmod(k.dir+k.file,0o777,function(err){ - + s.knexQuery({ + action: "insert", + table: "Videos", + insert: insertQuery + },(err) => { + const response = { + ok: !err, + err: err, + insertQuery: insertQuery, + } + if(callback)callback(err,response) + fs.chmod(k.dir+k.file,0o777,function(err){ + resolve(response) + }) }) }) } @@ -95,180 +106,165 @@ module.exports = function(s,config,lang){ if(!k)k={}; e.dir = s.getVideoDirectory(e) k.dir = e.dir.toString() - if(s.group[e.ke].activeMonitors[e.id].childNode){ - s.cx({ - f: 'insertCompleted', - d: s.group[e.ke].rawMonitorConfigurations[e.id], - k: k - },s.group[e.ke].activeMonitors[e.id].childNodeId); - }else{ - //get file directory + const activeMonitor = s.group[e.ke].activeMonitors[e.id] + //get file directory + k.fileExists = fs.existsSync(k.dir+k.file) + if(k.fileExists!==true){ + k.dir = s.dir.videos+'/'+e.ke+'/'+e.id+'/' k.fileExists = fs.existsSync(k.dir+k.file) - if(k.fileExists!==true){ - k.dir = s.dir.videos+'/'+e.ke+'/'+e.id+'/' - k.fileExists = fs.existsSync(k.dir+k.file) - if(k.fileExists !== true){ - s.dir.addStorage.forEach(function(v){ - if(k.fileExists !== true){ - k.dir = s.checkCorrectPathEnding(v.path)+e.ke+'/'+e.id+'/' - k.fileExists = fs.existsSync(k.dir+k.file) - } - }) - } + if(k.fileExists !== true){ + s.dir.addStorage.forEach(function(v){ + if(k.fileExists !== true){ + k.dir = s.checkCorrectPathEnding(v.path)+e.ke+'/'+e.id+'/' + k.fileExists = fs.existsSync(k.dir+k.file) + } + }) } - if(k.fileExists===true){ - //close video row - k.details = k.details && k.details instanceof Object ? k.details : {} - k.stat = fs.statSync(k.dir+k.file) - k.filesize = k.stat.size - k.filesizeMB = parseFloat((k.filesize/1048576).toFixed(2)) - - k.startTime = new Date(s.nameToTime(k.file)) - k.endTime = new Date(k.endTime || k.stat.mtime) - if(config.useUTC === true){ - fs.rename(k.dir+k.file, k.dir+s.formattedTime(k.startTime)+'.'+e.ext, (err) => { - if (err) return console.error(err); - }); - k.filename = s.formattedTime(k.startTime)+'.'+e.ext - }else{ - k.filename = k.file - } - if(!e.ext){e.ext = k.filename.split('.')[1]} - //send event for completed recording + } + if(k.fileExists === true){ + //close video row + k.details = k.details && k.details instanceof Object ? k.details : {} + var listOEvents = activeMonitor.detector_motion_count || [] + var listOTags = listOEvents.filter(row => row.details.reason === 'object').map(row => row.details.matrices.map(matrix => matrix.tag).join(',')).join(',').split(',') + if(listOTags && !k.objects)k.objects = [...new Set(listOTags)].join(','); + k.filename = k.filename || k.file + k.ext = k.ext || e.ext || k.filename.split('.')[1] + k.stat = fs.statSync(k.dir+k.file) + k.filesize = k.stat.size + k.filesizeMB = parseFloat((k.filesize/1048576).toFixed(2)) + k.startTime = new Date(s.nameToTime(k.file)) + k.endTime = new Date(k.endTime || k.stat.mtime) + //send event for completed recording + if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ const response = { mid: e.mid, ke: e.ke, filename: k.filename, - d: s.cleanMonitorObject(s.group[e.ke].rawMonitorConfigurations[e.id]), + ext: k.ext, + size: k.filesize, filesize: k.filesize, + objects: k.objects, time: s.timeObject(k.startTime).format('YYYY-MM-DD HH:mm:ss'), end: s.timeObject(k.endTime).format('YYYY-MM-DD HH:mm:ss') } - if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ - fs.createReadStream(k.dir+k.filename,{ highWaterMark: 500 }) - .on('data',function(data){ - s.cx(Object.assign(response,{ - f:'created_file_chunk', - chunk: data, - })) - }) - .on('close',function(){ - clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingChecker) - clearTimeout(s.group[e.ke].activeMonitors[e.id].streamChecker) - s.cx(Object.assign(response,{ - f:'created_file', - })) + var filePath = k.dir + k.filename; + sendVideoToMasterNode(filePath,response) + }else{ + var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename + const monitorEventsCounted = activeMonitor.detector_motion_count + s.txWithSubPermissions({ + f: 'video_build_success', + hrefNoAuth: href, + filename: k.filename, + mid: e.mid, + ke: e.ke, + ext: k.ext, + time: k.startTime, + size: k.filesize, + end: k.endTime, + objects: k.objects, + events: monitorEventsCounted && monitorEventsCounted.length > 0 ? monitorEventsCounted : null + },'GRP_'+e.ke,'video_view') + //purge over max + s.purgeDiskForGroup(e.ke) + //send new diskUsage values + var storageIndex = s.getVideoStorageIndex(e) + if(storageIndex){ + s.setDiskUsedForGroupAddStorage(e.ke,{ + size: k.filesizeMB, + storageIndex: storageIndex }) }else{ - var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename - if(config.useUTC === true)href += '?isUTC=true'; - const monitorEventsCounted = s.group[e.ke].activeMonitors[e.mid].detector_motion_count - s.txWithSubPermissions({ - f: 'video_build_success', - hrefNoAuth: href, - filename: k.filename, - mid: e.mid, - ke: e.ke, - time: k.startTime, - size: k.filesize, - end: k.endTime, - events: monitorEventsCounted && monitorEventsCounted.length > 0 ? monitorEventsCounted : null - },'GRP_'+e.ke,'video_view') - //purge over max - s.purgeDiskForGroup(e.ke) - //send new diskUsage values - var storageIndex = s.getVideoStorageIndex(e) - if(storageIndex){ - s.setDiskUsedForGroupAddStorage(e.ke,{ - size: k.filesizeMB, - storageIndex: storageIndex - }) - }else{ - s.setDiskUsedForGroup(e.ke,k.filesizeMB) - } - s.onBeforeInsertCompletedVideoExtensions.forEach(function(extender){ - extender(e,k) - }) - s.insertDatabaseRow(e,k,callback) - s.insertCompletedVideoExtensions.forEach(function(extender){ - extender(e,k,response) - }) + s.setDiskUsedForGroup(e.ke,k.filesizeMB) } + s.onBeforeInsertCompletedVideoExtensions.forEach(function(extender){ + extender(e,k) + }) + s.insertDatabaseRow(e,k,(err,response) => { + if(callback)callback(err,response); + s.insertCompletedVideoExtensions.forEach(function(extender){ + extender(e,k,response.insertQuery,response) + }) + }) } } s.group[e.ke].activeMonitors[e.mid].detector_motion_count = [] } s.deleteVideo = function(e){ - //e = video object - s.checkDetails(e) - e.dir = s.getVideoDirectory(e) - if(!e.filename && e.time){ - e.filename = s.formattedTime(e.time) - } - var filename, - time - if(e.filename.indexOf('.')>-1){ - filename = e.filename - }else{ - filename = e.filename+'.'+e.ext - } - if(e.filename && !e.time){ - time = s.nameToTime(filename) - }else{ - time = e.time - } - time = new Date(time) - const whereQuery = { - ke: e.ke, - mid: e.id, - time: time, - } - s.knexQuery({ - action: "select", - columns: "*", - table: "Videos", - where: whereQuery - },(err,r) => { - if(r && r[0]){ - r = r[0] - fs.chmod(e.dir+filename,0o777,function(err){ - s.tx({ - f: 'video_delete', - filename: filename, - mid: e.id, - ke: e.ke, - time: s.nameToTime(filename), - end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') - },'GRP_'+e.ke); - var storageIndex = s.getVideoStorageIndex(e) - if(storageIndex){ - s.setDiskUsedForGroupAddStorage(e.ke,{ - size: -(r.size / 1048576), - storageIndex: storageIndex - }) - }else{ - s.setDiskUsedForGroup(e.ke,-(r.size / 1048576)) - } - s.knexQuery({ - action: "delete", - table: "Videos", - where: whereQuery - },(err) => { - if(err){ - s.systemLog(lang['File Delete Error'] + ' : '+e.ke+' : '+' : '+e.id,err) + return new Promise((resolve) => { + //e = video object + s.checkDetails(e) + e.dir = s.getVideoDirectory(e) + if(!e.filename && e.time){ + e.filename = s.formattedTime(e.time) + } + var filename, + time + if(e.filename.indexOf('.')>-1){ + filename = e.filename + }else{ + filename = e.filename+'.'+e.ext + } + if(e.filename && !e.time){ + time = s.nameToTime(filename) + }else{ + time = e.time + } + time = new Date(time) + const whereQuery = { + ke: e.ke, + mid: e.id, + time: time, + } + s.knexQuery({ + action: "select", + columns: "*", + table: "Videos", + where: whereQuery + },(err,r) => { + if(r && r[0]){ + r = r[0] + fs.chmod(e.dir+filename,0o777,function(err){ + s.tx({ + f: 'video_delete', + filename: filename, + mid: e.id, + ke: e.ke, + time: new Date(s.nameToTime(filename)), + end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') + },'GRP_'+e.ke); + var storageIndex = s.getVideoStorageIndex(e) + if(storageIndex){ + s.setDiskUsedForGroupAddStorage(e.ke,{ + size: -(r.size / 1048576), + storageIndex: storageIndex + }) + }else{ + s.setDiskUsedForGroup(e.ke,-(r.size / 1048576)) } - }) - fs.unlink(e.dir+filename,function(err){ - fs.stat(e.dir+filename,function(err){ - if(!err){ - s.file('delete',e.dir+filename) + s.knexQuery({ + action: "delete", + table: "Videos", + where: whereQuery + },(err) => { + if(err){ + s.systemLog(lang['File Delete Error'] + ' : '+e.ke+' : '+' : '+e.id,err) } }) + fs.unlink(e.dir+filename,function(err){ + fs.stat(e.dir+filename,function(err){ + if(!err){ + s.file('delete',e.dir+filename) + } + }) + resolve() + }) }) - }) - }else{ - console.log(lang['Database row does not exist'],whereQuery) - } + }else{ + console.log(lang['Database row does not exist'],whereQuery) + resolve() + } + }) }) } s.deleteListOfVideos = function(videos){ @@ -488,124 +484,6 @@ module.exports = function(s,config,lang){ file.pipe(res) return file } - s.createVideoFromTimelapse = function(timelapseFrames,framesPerSecond,callback){ - framesPerSecond = parseInt(framesPerSecond) - if(!framesPerSecond || isNaN(framesPerSecond))framesPerSecond = 2 - var frames = timelapseFrames.reverse() - var ke = frames[0].ke - var mid = frames[0].mid - var finalFileName = frames[0].filename.split('.')[0] + '_' + frames[frames.length - 1].filename.split('.')[0] + `-${framesPerSecond}fps` - var concatFiles = [] - var createLocation - frames.forEach(function(frame,frameNumber){ - var selectedDate = frame.filename.split('T')[0] - var fileLocationMid = `${frame.ke}/${frame.mid}_timelapse/${selectedDate}/` - frame.details = s.parseJSON(frame.details) - var fileLocation - if(frame.details.dir){ - fileLocation = `${s.checkCorrectPathEnding(frame.details.dir)}` - }else{ - fileLocation = `${s.dir.videos}` - } - fileLocation = `${fileLocation}${fileLocationMid}${frame.filename}` - concatFiles.push(fileLocation) - if(frameNumber === 0){ - createLocation = fileLocationMid - } - }) - if(concatFiles.length > framesPerSecond){ - var commandTempLocation = `${s.dir.streams}${ke}/${mid}/mergeJpegs_${finalFileName}.sh` - var finalMp4OutputLocation = `${s.dir.fileBin}${ke}/${mid}/${finalFileName}.mp4` - if(!s.group[ke].activeMonitors[mid].buildingTimelapseVideo){ - if(!fs.existsSync(finalMp4OutputLocation)){ - var currentFile = 0 - var completionTimeout - var commandString = `ffmpeg -y -f image2pipe -vcodec mjpeg -r ${framesPerSecond} -analyzeduration 10 -i - -q:v 1 -c:v libx264 -r ${framesPerSecond} "${finalMp4OutputLocation}"` - fs.writeFileSync(commandTempLocation,commandString) - var videoBuildProcess = spawn('sh',[commandTempLocation]) - videoBuildProcess.stderr.on('data',function(data){ - // console.log(data.toString()) - clearTimeout(completionTimeout) - completionTimeout = setTimeout(function(){ - if(currentFile === concatFiles.length - 1){ - videoBuildProcess.kill('SIGTERM') - } - },4000) - }) - videoBuildProcess.on('exit',function(data){ - var timeNow = new Date() - var fileStats = fs.statSync(finalMp4OutputLocation) - var details = {} - s.knexQuery({ - action: "insert", - table: "Files", - insert: { - ke: ke, - mid: mid, - details: s.s(details), - name: finalFileName + '.mp4', - size: fileStats.size, - time: timeNow, - } - }) - s.setDiskUsedForGroup(ke,fileStats.size / 1048576,'fileBin') - fs.unlink(commandTempLocation,function(){ - - }) - s.purgeDiskForGroup(ke) - setTimeout(() => { - delete(s.group[ke].activeMonitors[mid].buildingTimelapseVideo) - },5000) - }) - var readFile = function(){ - var filePath = concatFiles[currentFile] - // console.log(filePath,currentFile,'/',concatFiles.length - 1) - fs.readFile(filePath,function(err,buffer){ - if(!err)videoBuildProcess.stdin.write(buffer) - if(currentFile === concatFiles.length - 1){ - //is last - - }else{ - setTimeout(function(){ - ++currentFile - readFile() - },1/framesPerSecond) - } - }) - } - readFile() - s.group[ke].activeMonitors[mid].buildingTimelapseVideo = videoBuildProcess - callback({ - ok: true, - fileExists: false, - fileLocation: finalMp4OutputLocation, - msg: lang['Started Building'] - }) - }else{ - callback({ - ok: false, - fileExists: true, - filename: finalFileName + '.mp4', - fileLocation: finalMp4OutputLocation, - msg: lang['Already exists'] - }) - } - }else{ - callback({ - ok: false, - fileExists: false, - fileLocation: finalMp4OutputLocation, - msg: lang.Building - }) - } - }else{ - callback({ - ok: false, - fileExists: false, - msg: lang.notEnoughFramesText1 - }) - } - } s.getVideoStorageIndex = function(video){ try{ const monitorId = video.id || video.mid diff --git a/libs/webServer.js b/libs/webServer.js index a335c955..8c578104 100644 --- a/libs/webServer.js +++ b/libs/webServer.js @@ -1,9 +1,11 @@ -var fs = require('fs'); -var http = require('http'); -var https = require('https'); -var express = require('express'); -var app = express() +const fs = require('fs'); +const url = require('url'); +const http = require('http'); +const https = require('https'); +const express = require('express'); +const app = express() module.exports = function(s,config,lang,io){ + app.disable('x-powered-by'); //get page URL if(!config.baseURL){ config.baseURL = "" @@ -103,10 +105,21 @@ module.exports = function(s,config,lang,io){ }) } //start HTTP + const onHttpRequestUpgradeExtensions = s.onHttpRequestUpgradeExtensions; var server = http.createServer(app); server.listen(config.port,config.bindip,function(){ console.log(lang.Shinobi+' : Web Server Listening on '+config.port); }); + server.on('upgrade', function upgrade(request, socket, head) { + const pathname = url.parse(request.url).pathname; + if(typeof onHttpRequestUpgradeExtensions[pathname] === 'function'){ + onHttpRequestUpgradeExtensions[pathname](request, socket, head) + } else if (pathname.indexOf('/socket.io') > -1) { + return; + } else { + socket.destroy(); + } + }); if(config.webPaths.home !== '/'){ io.attach(server,{ path:'/socket.io', diff --git a/libs/webServerAdminPaths.js b/libs/webServerAdminPaths.js index 55f78259..6612ea9e 100644 --- a/libs/webServerAdminPaths.js +++ b/libs/webServerAdminPaths.js @@ -1,12 +1,13 @@ var fs = require('fs'); var os = require('os'); var moment = require('moment') -var request = require('request') -var jsonfile = require("jsonfile") var exec = require('child_process').exec; var spawn = require('child_process').spawn; var execSync = require('child_process').execSync; module.exports = function(s,config,lang,app){ + const { + deleteMonitorData, + } = require('./monitor/utils.js')(s,config,lang) /** * API : Administrator : Edit Sub-Account (Account to share cameras with) */ @@ -15,14 +16,16 @@ module.exports = function(s,config,lang,app){ var endData = { ok : false } - if(user.details.sub){ - endData.msg = user.lang['Not Permitted'] - s.closeJsonResponse(res,endData) + const { + isSubAccount, + } = s.checkPermission(user) + if(isSubAccount){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not an Administrator Account']}); return } var form = s.getPostData(req) var uid = form.uid || s.getPostData(req,'uid',false) - var mail = form.mail || s.getPostData(req,'mail',false) + var mail = (form.mail || s.getPostData(req,'mail',false) || '').trim() if(form){ var keys = ['details'] form.details = s.parseJSON(form.details) || {"sub": 1, "allmonitors": "1"} @@ -51,6 +54,8 @@ module.exports = function(s,config,lang,app){ s.closeJsonResponse(res,endData) return } + }else{ + updateQuery.mail = form.mail } } await s.knexQueryPromise({ @@ -96,57 +101,73 @@ module.exports = function(s,config,lang,app){ */ app.all(config.webPaths.adminApiPrefix+':auth/accounts/:ke/delete', function (req,res){ s.auth(req.params,function(user){ + const groupKey = req.params.ke; var endData = { ok : false } - if(user.details.sub){ - endData.msg = user.lang['Not Permitted'] - s.closeJsonResponse(res,endData) + const { + isSubAccount, + } = s.checkPermission(user) + if(isSubAccount){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not an Administrator Account']}); return } var form = s.getPostData(req) || {} var uid = form.uid || s.getPostData(req,'uid',false) - var mail = form.mail || s.getPostData(req,'mail',false) - s.knexQuery({ - action: "delete", - table: "Users", - where: { - ke: req.params.ke, - uid: uid, - mail: mail, - } - }) s.knexQuery({ action: "select", columns: "*", - table: "API", + table: "Users", where: [ - ['ke','=',req.params.ke], + ['ke','=',groupKey], ['uid','=',uid], ] - },function(err,rows){ - if(rows && rows[0]){ - rows.forEach(function(row){ - delete(s.api[row.code]) - }) + },function(err,usersFound){ + const theUserUpForDeletion = usersFound[0] + if(theUserUpForDeletion){ s.knexQuery({ action: "delete", - table: "API", + table: "Users", where: { - ke: req.params.ke, + ke: groupKey, uid: uid, } }) + s.knexQuery({ + action: "select", + columns: "*", + table: "API", + where: [ + ['ke','=',groupKey], + ['uid','=',uid], + ] + },function(err,rows){ + if(rows && rows[0]){ + rows.forEach(function(row){ + delete(s.api[row.code]) + }) + s.knexQuery({ + action: "delete", + table: "API", + where: { + ke: groupKey, + uid: uid, + } + }) + } + }) + s.tx({ + f: 'delete_sub_account', + ke: groupKey, + uid: uid, + mail: theUserUpForDeletion.mail + },'ADM_'+groupKey) + endData.ok = true + }else{ + endData.msg = user.lang['User Not Found'] } + s.closeJsonResponse(res,endData) }) - s.tx({ - f: 'delete_sub_account', - ke: req.params.ke, - uid: uid, - mail: mail - },'ADM_'+req.params.ke) - endData.ok = true - s.closeJsonResponse(res,endData) },res,req) }) /** @@ -157,9 +178,11 @@ module.exports = function(s,config,lang,app){ var endData = { ok : false } - if(user.details.sub){ - endData.msg = user.lang['Not Permitted'] - s.closeJsonResponse(res,endData) + const { + isSubAccount, + } = s.checkPermission(user) + if(isSubAccount){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not an Administrator Account']}); return }else{ endData.ok = true @@ -192,9 +215,11 @@ module.exports = function(s,config,lang,app){ } res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - if(user.details.sub){ - endData.msg = user.lang['Not an Administrator Account'] - s.closeJsonResponse(res,endData) + const { + isSubAccount, + } = s.checkPermission(user) + if(isSubAccount){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not an Administrator Account']}); return } var form = s.getPostData(req) @@ -273,7 +298,26 @@ module.exports = function(s,config,lang,app){ } res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' + const groupKey = req.params.ke + const monitorId = req.params.id + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + userPermissions, + } = s.checkPermission(user) + if( + userPermissions.monitor_create_disallowed || + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_monitor_edit`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return + } if(req.params.f !== 'delete'){ var form = s.getPostData(req) if(!form){ @@ -282,82 +326,46 @@ module.exports = function(s,config,lang,app){ return } form.mid = req.params.id.replace(/[^\w\s]/gi,'').replace(/ /g,'') - if(!user.details.sub || - user.details.allmonitors === '1' || - hasRestrictions && user.details.monitor_edit.indexOf(form.mid) >- 1 || - hasRestrictions && user.details.monitor_create === '1'){ - if(form && form.name){ - s.checkDetails(form) - form.ke = req.params.ke - s.addOrEditMonitor(form,function(err,endData){ - res.end(s.prettyPrint(endData)) - },user) - }else{ - endData.msg = user.lang.monitorEditText1; - res.end(s.prettyPrint(endData)) - } - }else{ - endData.msg = user.lang['Not Permitted'] + if(form && form.name){ + s.checkDetails(form) + form.ke = req.params.ke + s.addOrEditMonitor(form,function(err,endData){ res.end(s.prettyPrint(endData)) + },user) + }else{ + endData.msg = user.lang.monitorEditText1; + res.end(s.prettyPrint(endData)) } }else{ - if(!user.details.sub || user.details.allmonitors === '1' || user.details.monitor_edit.indexOf(req.params.id) > -1 || hasRestrictions && user.details.monitor_create === '1'){ - s.userLog({ + s.userLog({ + ke: req.params.ke, + mid: req.params.id + },{ + type: 'Monitor Deleted', + msg: 'by user : '+user.uid + }); + req.params.delete=1; + s.camera('stop',req.params); + s.tx({f:'monitor_delete',uid:user.uid,mid:req.params.id,ke:req.params.ke},'GRP_'+req.params.ke); + s.knexQuery({ + action: "delete", + table: "Monitors", + where: { ke: req.params.ke, - mid: req.params.id - },{ - type: 'Monitor Deleted', - msg: 'by user : '+user.uid - }); - req.params.delete=1;s.camera('stop',req.params); - s.tx({f:'monitor_delete',uid:user.uid,mid:req.params.id,ke:req.params.ke},'GRP_'+req.params.ke); - s.knexQuery({ - action: "delete", - table: "Monitors", - where: { + mid: req.params.id, + } + }) + if(req.query.deleteFiles === 'true'){ + deleteMonitorData(req.params.ke,req.params.id).then(() => { + s.debugLog(`Deleted Monitor Data`,{ ke: req.params.ke, mid: req.params.id, - } + }) }) - // s.knexQuery({ - // action: "delete", - // table: "Files", - // where: { - // ke: req.params.ke, - // mid: req.params.id, - // } - // }) - if(req.query.deleteFiles === 'true'){ - //videos - s.dir.addStorage.forEach(function(v,n){ - var videosDir = v.path+req.params.ke+'/'+req.params.id+'/' - fs.stat(videosDir,function(err,stat){ - if(!err){ - s.file('deleteFolder',videosDir) - } - }) - }) - var videosDir = s.dir.videos+req.params.ke+'/'+req.params.id+'/' - fs.stat(videosDir,function(err,stat){ - if(!err){ - s.file('deleteFolder',videosDir) - } - }) - //fileBin - var binDir = s.dir.fileBin+req.params.ke+'/'+req.params.id+'/' - fs.stat(binDir,function(err,stat){ - if(!err){ - s.file('deleteFolder',binDir) - } - }) - } - endData.ok=true; - endData.msg='Monitor Deleted by user : '+user.uid - res.end(s.prettyPrint(endData)) - }else{ - endData.msg=user.lang['Not Permitted']; - res.end(s.prettyPrint(endData)) } + endData.ok=true; + endData.msg='Monitor Deleted by user : '+user.uid + res.end(s.prettyPrint(endData)) } },res,req) }) @@ -508,9 +516,18 @@ module.exports = function(s,config,lang,app){ var endData = { ok : false } - if(user.details.sub){ - endData.msg = user.lang['Not Permitted'] - s.closeJsonResponse(res,endData) + const groupKey = req.params.ke + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + userPermissions, + } = s.checkPermission(user) + if( + userPermissions.monitor_create_disallowed || + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } s.knexQuery({ @@ -546,9 +563,18 @@ module.exports = function(s,config,lang,app){ var endData = { ok : false } - if(user.details.sub){ - endData.msg = user.lang['Not Permitted'] - s.closeJsonResponse(res,endData) + const groupKey = req.params.ke + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + userPermissions, + } = s.checkPermission(user) + if( + userPermissions.monitor_create_disallowed || + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } var presetQueryVals = [req.params.ke,'monitorStates',req.params.stateName] diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js index 8e5b29e6..e9587bca 100644 --- a/libs/webServerPaths.js +++ b/libs/webServerPaths.js @@ -3,7 +3,7 @@ var fs = require('fs'); var bodyParser = require('body-parser'); var os = require('os'); var moment = require('moment'); -var request = require('request'); +var fetch = require('node-fetch'); var execSync = require('child_process').execSync; var exec = require('child_process').exec; var spawn = require('child_process').spawn; @@ -26,6 +26,16 @@ module.exports = function(s,config,lang,app,io){ twoFactorVerification, ldapLogin, } = require('./auth/utils.js')(s,config,lang) + const { + spawnSubstreamProcess, + destroySubstreamProcess, + } = require('./monitor/utils.js')(s,config,lang) + const { + archiveVideo, + reEncodeVideoAndReplace, + reEncodeVideoAndBinOriginalAddToQueue, + getVideosBasedOnTagFoundInMatrixOfAssociatedEvent, + } = require('./video/utils.js')(s,config,lang) s.renderPage = function(req,res,paths,passables,callback){ passables.window = {} passables.data = req.params @@ -75,9 +85,14 @@ module.exports = function(s,config,lang,app,io){ if(config.webPaths.home !== '/'){ app.use('/libs',express.static(s.mainDirectory + '/web/libs')) } - app.use(s.checkCorrectPathEnding(config.webPaths.home)+'libs',express.static(s.mainDirectory + '/web/libs')) - app.use(s.checkCorrectPathEnding(config.webPaths.admin)+'libs',express.static(s.mainDirectory + '/web/libs')) - app.use(s.checkCorrectPathEnding(config.webPaths.super)+'libs',express.static(s.mainDirectory + '/web/libs')) + [ + [config.webPaths.home,'libs','/web/libs'], + [config.webPaths.super,'libs','/web/libs'], + [config.webPaths.home,'assets','/web/assets'], + [config.webPaths.super,'assets','/web/assets'], + ].forEach((piece) => { + app.use(s.checkCorrectPathEnding(piece[0])+piece[1],express.static(s.mainDirectory + piece[2])) + }) app.use(bodyParser.json()); app.use(bodyParser.urlencoded({extended: true})); app.use(function (req,res,next){ @@ -120,12 +135,6 @@ module.exports = function(s,config,lang,app,io){ s.renderPage(req,res,config.renderPaths.index,{lang:lang,config: s.getConfigWithBranding(req.hostname),screen:'dashboard'}) }); /** - * Page : Administrator Login Screen - */ - app.get(config.webPaths.admin, function (req,res){ - s.renderPage(req,res,config.renderPaths.index,{lang:lang,config: s.getConfigWithBranding(req.hostname),screen:'admin'}) - }); - /** * Page : Superuser Login Screen */ app.get(config.webPaths.super, function (req,res){ @@ -155,14 +164,18 @@ module.exports = function(s,config,lang,app,io){ } } /** + * Page : Admin page redirect to regular page now + */ + app.get(config.webPaths.admin, function (req,res){ + res.redirect('/'); + }); + /** * API : Login handler. Dashboard, Streamer, Dashcam Administrator, Superuser */ app.post([ config.webPaths.home, - config.webPaths.admin, config.webPaths.super, s.checkCorrectPathEnding(config.webPaths.home)+':screen', - s.checkCorrectPathEnding(config.webPaths.admin)+':screen', s.checkCorrectPathEnding(config.webPaths.super)+':screen', ],async function (req,res){ var response = {ok: false}; @@ -303,6 +316,7 @@ module.exports = function(s,config,lang,app,io){ $user: userInfo, lang: userInfo.lang, define: s.getDefinitonFile(userInfo.details.lang), + __dirname: s.mainDirectory, customAutoLoad: s.customAutoLoadTree }) break; @@ -312,10 +326,12 @@ module.exports = function(s,config,lang,app,io){ $user: userInfo, lang: userInfo.lang, define: s.getDefinitonFile(userInfo.details.lang), + __dirname: s.mainDirectory, customAutoLoad: s.customAutoLoadTree }) break; case'admin': + // dash default: var chosenRender = 'home' if(userInfo.details.sub && userInfo.details.landing_page && userInfo.details.landing_page !== '' && config.renderPaths[userInfo.details.landing_page]){ @@ -424,9 +440,12 @@ module.exports = function(s,config,lang,app,io){ $user:{ ke: user.ke, uid: user.uid, - mail: user.mail + mail: user.mail, + details: { + sub: user.details.sub + } }, - lang: user.lang + lang: user.lang, }) return; } @@ -444,12 +463,13 @@ module.exports = function(s,config,lang,app,io){ failedAuthentication(req.body.function,req.body.mail) } } - if(req.body.function === 'super'){ + if(req.body.function === 'super' && !config.superUserLoginDisabled){ const superLoginResponse = await superLogin(req.body.mail,req.body.pass); if(superLoginResponse.ok){ renderPage(config.renderPaths.super,{ config: config, lang: lang, + define: s.getDefinitonFile(config.language), $user: superLoginResponse.user, customAutoLoad: s.customAutoLoadTree, currentVersion: s.currentVersion, @@ -631,8 +651,22 @@ module.exports = function(s,config,lang,app,io){ s.auth(req.params,function(user){ const groupKey = req.params.ke const monitorId = req.params.id - const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) - if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.get_monitors_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_monitors`] || + monitorRestrictions.length === 0 + ) + ){ s.closeJsonResponse(res,[]); return } @@ -736,8 +770,22 @@ module.exports = function(s,config,lang,app,io){ s.auth(req.params,(user) => { const groupKey = req.params.ke const monitorId = req.params.id - const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) - if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.get_monitors_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_monitors`] || + monitorRestrictions.length === 0 + ) + ){ s.closeJsonResponse(res,[]); return } @@ -757,6 +805,7 @@ module.exports = function(s,config,lang,app,io){ r[n].currentCpuUsage = activeMonitor.currentCpuUsage r[n].status = activeMonitor.monitorStatus r[n].code = activeMonitor.monitorStatusCode + r[n].subStreamChannel = activeMonitor.subStreamChannel } var buildStreamURL = function(type,channelNumber){ var streamURL @@ -807,63 +856,108 @@ module.exports = function(s,config,lang,app,io){ },res,req); }); /** - * API : Merge Recorded Videos into one file + * API : Toggle Substream Process on and off */ - app.get(config.webPaths.apiPrefix+':auth/videosMerge/:ke', function (req,res){ - var failed = function(resp){ - res.setHeader('Content-Type', 'application/json'); - res.end(s.prettyPrint(resp)) - } - if(req.query.videos && req.query.videos !== ''){ - s.auth(req.params,function(user){ - var videosSelected = JSON.parse(req.query.videos) - const whereQuery = [] - var didOne = false - videosSelected.forEach(function(video){ - var time = s.nameToTime(video.filename) - if(req.query.isUTC === 'true'){ - time = s.utcToLocal(time) - } - if(didOne){ - whereQuery.push(['or','ke','=',req.params.ke]) - }else{ - didOne = true - whereQuery.push(['ke','=',req.params.ke]) - } - whereQuery.push( - ['mid','=',video.mid], - ['time','=',time], - ) - - }) - s.knexQuery({ - action: "select", - columns: "*", - table: "Videos", - where: whereQuery - },(err,r) => { - var resp = {ok: false} - if(r && r[0]){ - s.mergeRecordedVideos(r,req.params.ke,function(fullPath,filename){ - res.setHeader('Content-Disposition', 'attachment; filename="'+filename+'"') - var file = fs.createReadStream(fullPath) - file.on('close',function(){ - setTimeout(function(){ - s.file('delete',fullPath) - },1000 * 60 * 3) - res.end() - }) - file.pipe(res) - }) - }else{ - failed({ok:false,msg:'No Videos Found'}) - } - }) - },res,req); - }else{ - failed({ok:false,msg:'"videos" query variable is missing from request.'}) - } - }) + app.get(config.webPaths.apiPrefix+':auth/toggleSubstream/:ke/:id', function (req,res){ + const response = {ok: false}; + s.auth(req.params,async (user) => { + const groupKey = req.params.ke + const monitorId = req.params.id + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + userPermissions, + apiKeyPermissions, + isRestrictedApiKey, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_monitors`] + ){ + response.msg = user.lang['Not Permitted'] + }else{ + const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId] + const activeMonitor = s.group[groupKey].activeMonitors[monitorId] + const substreamConfig = monitorConfig.details.substream + if( + substreamConfig.output + ){ + if(!activeMonitor.subStreamProcess){ + response.ok = true + activeMonitor.allowDestroySubstream = false; + spawnSubstreamProcess(monitorConfig) + }else{ + activeMonitor.allowDestroySubstream = true + await destroySubstreamProcess(activeMonitor) + } + }else{ + response.msg = lang['Invalid Settings'] + } + } + s.closeJsonResponse(res,response); + },res,req); + }); + // /** + // * API : Merge Recorded Videos into one file + // */ + // app.get(config.webPaths.apiPrefix+':auth/videosMerge/:ke', function (req,res){ + // var failed = function(resp){ + // res.setHeader('Content-Type', 'application/json'); + // res.end(s.prettyPrint(resp)) + // } + // if(req.query.videos && req.query.videos !== ''){ + // s.auth(req.params,function(user){ + // var videosSelected = JSON.parse(req.query.videos) + // const whereQuery = [] + // var didOne = false + // videosSelected.forEach(function(video){ + // var time = s.nameToTime(video.filename) + // if(req.query.isUTC === 'true'){ + // time = s.utcToLocal(time) + // } + // if(didOne){ + // whereQuery.push(['or','ke','=',req.params.ke]) + // }else{ + // didOne = true + // whereQuery.push(['ke','=',req.params.ke]) + // } + // whereQuery.push( + // ['mid','=',video.mid], + // ['time','=',time], + // ) + // + // }) + // s.knexQuery({ + // action: "select", + // columns: "*", + // table: "Videos", + // where: whereQuery + // },(err,r) => { + // var resp = {ok: false} + // if(r && r[0]){ + // s.mergeRecordedVideos(r,req.params.ke,function(fullPath,filename){ + // res.setHeader('Content-Disposition', 'attachment; filename="'+filename+'"') + // var file = fs.createReadStream(fullPath) + // file.on('close',function(){ + // setTimeout(function(){ + // s.file('delete',fullPath) + // },1000 * 60 * 3) + // res.end() + // }) + // file.pipe(res) + // }) + // }else{ + // failed({ok:false,msg:'No Videos Found'}) + // } + // }) + // },res,req); + // }else{ + // failed({ok:false,msg:'"videos" query variable is missing from request.'}) + // } + // }) /** * API : Get Videos */ @@ -875,10 +969,27 @@ module.exports = function(s,config,lang,app,io){ ], function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - const userDetails = user.details const monitorId = req.params.id const groupKey = req.params.ke - const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_video_view`] || + monitorRestrictions.length === 0 + ) + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], videos: []}); + return + } var origURL = req.originalUrl.split('/') var videoParam = origURL[origURL.indexOf(req.params.auth) + 1] var videoSet = 'Videos' @@ -897,16 +1008,13 @@ module.exports = function(s,config,lang,app,io){ endTime: req.query.end, startTimeOperator: req.query.startOperator, endTimeOperator: req.query.endOperator, + noLimit: req.query.noLimit, limit: req.query.limit, archived: req.query.archived, endIsStartTo: !!req.query.endIsStartTo, parseRowDetails: false, rowName: 'videos', - preliminaryValidationFailed: ( - user.permissions.watch_videos === "0" || - hasRestrictions && - (!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1) - ) + preliminaryValidationFailed: false },(response) => { if(response && response.videos){ s.buildVideoLinks(response.videos,{ @@ -920,6 +1028,60 @@ module.exports = function(s,config,lang,app,io){ },res,req); }); /** + * API : Get Videos + */ + app.get([ + config.webPaths.apiPrefix+':auth/videosByEventTag/:ke', + config.webPaths.apiPrefix+':auth/videosByEventTag/:ke/:id' + ], function (req,res){ + res.setHeader('Content-Type', 'application/json'); + s.auth(req.params,function(user){ + const searchQuery = s.getPostData(req,'search') + const startTime = s.getPostData(req,'start') + const endTime = s.getPostData(req,'end') + const monitorId = req.params.id + const groupKey = req.params.ke + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_video_view`] || + monitorRestrictions.length === 0 + ) + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], videos: []}); + return + } + getVideosBasedOnTagFoundInMatrixOfAssociatedEvent({ + groupKey, + monitorId, + startTime, + endTime, + searchQuery, + monitorRestrictions, + }).then((response) => { + if(response && response.rows){ + s.buildVideoLinks(response.rows,{ + auth : req.params.auth, + videoParam : 'videos', + }) + } + s.closeJsonResponse(res,{ + ok: true, + videos: response.rows, + }) + }) + },res,req); + }); + /** * API : Get Events */ app.get([ @@ -928,33 +1090,71 @@ module.exports = function(s,config,lang,app,io){ ], function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - const userDetails = user.details const monitorId = req.params.id const groupKey = req.params.ke - const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; - s.sqlQueryBetweenTimesWithPermissions({ - table: 'Events', - user: user, - groupKey: req.params.ke, - monitorId: req.params.id, - startTime: req.query.start, - endTime: req.query.end, - startTimeOperator: req.query.startOperator, - endTimeOperator: req.query.endOperator, - limit: req.query.limit, - endIsStartTo: true, - parseRowDetails: true, - noFormat: true, - noCount: true, - rowName: 'events', - preliminaryValidationFailed: ( - user.permissions.watch_videos === "0" || - hasRestrictions && - (!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1) + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_video_view`] || + monitorRestrictions.length === 0 ) - },(response) => { - res.end(s.prettyPrint(response)) - }) + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], events: []}); + return + } + if(req.query.onlyCount === '1'){ + const response = {ok: true} + s.knexQuery({ + action: "count", + columns: "mid", + table: "Events", + where: [ + ['ke','=',groupKey], + ['time','>=',req.query.start], + ['time','<=',req.query.end], + monitorRestrictions + ] + },(err,r) => { + if(err){ + s.debugLog(err) + response.ok = false + }else{ + response.count = r[0]['count(`mid`)'] + } + s.closeJsonResponse(res,response) + }) + }else{ + s.sqlQueryBetweenTimesWithPermissions({ + table: 'Events', + user: user, + groupKey: req.params.ke, + monitorId: req.params.id, + startTime: req.query.start, + endTime: req.query.end, + startTimeOperator: req.query.startOperator, + endTimeOperator: req.query.endOperator, + noLimit: req.query.noLimit, + limit: req.query.limit, + archived: req.query.archived, + endIsStartTo: true, + parseRowDetails: true, + noFormat: true, + noCount: true, + rowName: 'events', + preliminaryValidationFailed: false + },(response) => { + res.end(s.prettyPrint(response)) + }) + } }) }) /** @@ -966,10 +1166,21 @@ module.exports = function(s,config,lang,app,io){ ], function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - const userDetails = user.details - const monitorId = req.params.id const groupKey = req.params.ke - const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; + const monitorId = req.params.id + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + userPermissions, + } = s.checkPermission(user) + if( + userPermissions.view_logs_disallowed || + isRestrictedApiKey && apiKeyPermissions.get_logs_disallowed + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], logs: []}); + return + } s.sqlQueryBetweenTimesWithPermissions({ table: 'Logs', user: user, @@ -984,9 +1195,7 @@ module.exports = function(s,config,lang,app,io){ noFormat: true, noCount: true, rowName: 'logs', - preliminaryValidationFailed: ( - user.permissions.get_logs === "0" || userDetails.sub && userDetails.view_logs !== '1' - ) + preliminaryValidationFailed: false },(response) => { response.forEach(function(v,n){ v.info = JSON.parse(v.info) @@ -996,51 +1205,30 @@ module.exports = function(s,config,lang,app,io){ },res,req) }) /** - * API : Get Monitors Online - */ - app.get(config.webPaths.apiPrefix+':auth/smonitor/:ke', function (req,res){ - var response = {ok:false}; - res.setHeader('Content-Type', 'application/json'); - s.auth(req.params,(user) => { - const groupKey = req.params.ke - const monitorId = req.params.id - const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) - if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ - s.closeJsonResponse(res,[]); - return - } - s.knexQuery({ - action: "select", - columns: "*", - table: "Monitors", - where: [ - ['ke','=',groupKey], - monitorRestrictions - ] - },(err,r) => { - const startedMonitors = [] - r.forEach(function(v){ - if( - s.group[groupKey] && - s.group[groupKey].activeMonitors[v.mid] && - s.group[groupKey].activeMonitors[v.mid].isStarted === true - ){ - startedMonitors.push(v) - } - }) - s.closeJsonResponse(res,startedMonitors) - }) - },res,req); - }); - /** * API : Monitor Mode Controller */ app.get([config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f',config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f/:ff',config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f/:ff/:fff'], function (req,res){ var response = {ok:false}; res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - if(user.permissions.control_monitors==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitor_edit.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) + const groupKey = req.params.ke + const monitorId = req.params.id + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + userPermissions, + } = s.checkPermission(user) + if( + userPermissions.monitor_create_disallowed || + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_monitor_edit`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } if(req.params.f===''){response.msg = user.lang.monitorGetText1;res.end(s.prettyPrint(response));return} @@ -1073,10 +1261,6 @@ module.exports = function(s,config,lang,app,io){ } r.mode=req.params.f; try{r.details=JSON.parse(r.details);}catch(er){} - if(req.query.fps){ - r.fps=parseFloat(r.details.detector_trigger_record_fps) - s.group[r.ke].activeMonitors[r.mid].currentState.detector_trigger_record_fps=r.fps - } r.id=r.mid; s.knexQuery({ action: "update", @@ -1163,12 +1347,20 @@ module.exports = function(s,config,lang,app,io){ s.auth(req.params,function(user){ const groupKey = req.params.ke const monitorId = req.params.id - const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) - if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.watch_videos === "0" || monitorRestrictions.length === 0)){ - s.closeJsonResponse(res,{ - ok: false, - msg: lang['Not Permitted'] - }) + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_video_view`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } var time = s.nameToTime(req.params.file) @@ -1189,15 +1381,21 @@ module.exports = function(s,config,lang,app,io){ },(err,r) => { if(r&&r[0]){ r = r[0] - if(JSON.parse(r.details).type === 'googd' && s.cloudDiskUseOnGetVideoDataExtensions['googd']){ - s.cloudDiskUseOnGetVideoDataExtensions['googd'](r).then((dataPipe) => { + const videoDetails = JSON.parse(r.details) + const storageType = videoDetails.type + const onGetVideoData = s.cloudDiskUseOnGetVideoDataExtensions[storageType] + if(onGetVideoData){ + onGetVideoData(r).then((dataPipe) => { dataPipe.pipe(res) }).catch((err) => { console.log(err) res.end(user.lang['File Not Found in Database']) }) }else{ - req.pipe(request(r.href)).pipe(res) + fetch(r.href).then(actual => { + actual.headers.forEach((v, n) => res.setHeader(n, v)); + actual.body.pipe(res); + }) } }else{ res.end(user.lang['File Not Found in Database']) @@ -1214,12 +1412,20 @@ module.exports = function(s,config,lang,app,io){ s.auth(req.params,function(user){ const groupKey = req.params.ke const monitorId = req.params.id - const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) - if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.watch_videos === "0" || monitorRestrictions.length === 0)){ - s.closeJsonResponse(res,{ - ok: false, - msg: lang['Not Permitted'] - }) + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_video_view`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } var time = s.nameToTime(req.params.file) @@ -1279,23 +1485,35 @@ module.exports = function(s,config,lang,app,io){ */ app.get(config.webPaths.apiPrefix+':auth/motion/:ke/:id', function (req,res){ s.auth(req.params,function(user){ - const groupKey = req.params.ke - const monitorId = req.params.id - const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) - if(user.details.sub && user.details.allmonitors === '0' && monitorRestrictions.length === 0){ - s.closeJsonResponse(res,{ - ok: false, - msg: lang['Not Permitted'] - }) - return - } + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId] + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId); + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + !monitorConfig || + isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed + ){ + s.closeJsonResponse(res,{ + ok: false, + msg: !monitorConfig ? user.lang['Monitor or Key does not exist.'] : lang['Not Authorized'] + }); + return + } + let simulatedEvent = { + id: req.params.id, + ke: req.params.ke + } if(req.query.data){ try{ - var d = { - id: req.params.id, - ke: req.params.ke, - details: s.parseJSON(req.query.data) - } + Object.assign(simulatedEvent, {details: s.parseJSON(req.query.data)}); }catch(err){ s.closeJsonResponse(res,{ ok: false, @@ -1303,58 +1521,46 @@ module.exports = function(s,config,lang,app,io){ }) return } - }else{ + } + // fallback for cameras that doesn't support JSON in query parameters ( i.e Sercom ICamera1000 will fail to save HTTP_Notifications as invalid url) + else if(req.query.plug && req.query.name && req.query.reason && req.query.confidence) { + const { + plug, + reason, + confidence, + name, + } = req.query; + Object.assign(simulatedEvent,{ + details: { + plug, + reason, + confidence, + name, + } + }); + } + else{ s.closeJsonResponse(res,{ ok: false, msg: user.lang['No Data'] }) return } - if(!d.ke||!d.id||!s.group[d.ke]){ - s.closeJsonResponse(res,{ - ok: false, - msg: user.lang['No Group with this key exists'] - }) - return - } - if(!s.group[d.ke].rawMonitorConfigurations[d.id]){ - s.closeJsonResponse(res,{ - ok: false, - msg: user.lang['Monitor or Key does not exist.'] - }) - return - } - var details = s.group[d.ke].rawMonitorConfigurations[d.id].details + var details = s.group[groupKey].rawMonitorConfigurations[monitorId].details var detectorHttpApi = details.detector_http_api var detectorOn = (details.detector === '1') - switch(detectorHttpApi){ - case'0': - s.closeJsonResponse(res,{ - ok: false, - msg: user.lang['Trigger Blocked'] - }) - return - break; - case'2': - if(!detectorOn){ - s.closeJsonResponse(res,{ - ok: false, - msg: user.lang['Trigger Blocked'] - }) - return - } - break; - case'2': - if(detectorOn){ - s.closeJsonResponse(res,{ - ok: false, - msg: user.lang['Trigger Blocked'] - }) - return - } - break; + if( + detectorHttpApi === '0' || + detectorHttpApi === '2' && !detectorOn || + detectorHttpApi === '3' && detectorOn + ){ + s.closeJsonResponse(res,{ + ok: false, + msg: user.lang['Trigger Blocked'] + }) + return; } - triggerEvent(d) + triggerEvent(simulatedEvent) s.closeJsonResponse(res,{ ok: true, msg: user.lang['Trigger Successful'] @@ -1377,8 +1583,25 @@ module.exports = function(s,config,lang,app,io){ app.get(config.webPaths.apiPrefix+':auth/eventCountStatus/:ke/:id', function (req,res){ res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) + const monitorId = req.params.id + const groupKey = req.params.ke + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_video_view`] || + monitorRestrictions.length === 0 + ) + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], counted: 0, tags: []}); return } var selectedObject = s.group[req.params.ke].activeMonitors[req.params.id].eventsCounted @@ -1398,10 +1621,27 @@ module.exports = function(s,config,lang,app,io){ ], function (req,res){ res.setHeader('Content-Type', 'application/json') s.auth(req.params,function(user){ - const userDetails = user.details const monitorId = req.params.id const groupKey = req.params.ke - var hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_video_view`] || + monitorRestrictions.length === 0 + ) + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], videos: []}); + return + } s.sqlQueryBetweenTimesWithPermissions({ table: 'Events Counts', user: user, @@ -1417,11 +1657,7 @@ module.exports = function(s,config,lang,app,io){ endIsStartTo: !!req.query.endIsStartTo, parseRowDetails: true, rowName: 'counts', - preliminaryValidationFailed: ( - user.permissions.watch_videos === "0" || - hasRestrictions && - (!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1) - ) + preliminaryValidationFailed: false },(response) => { res.end(s.prettyPrint(response)) }) @@ -1462,12 +1698,24 @@ module.exports = function(s,config,lang,app,io){ var response = {ok:false} res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_delete.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) + const groupKey = req.params.ke + const monitorId = req.params.id + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_video_delete`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } - var groupKey = req.params.ke - var monitorId = req.params.id // req.query.overwrite === '1' if(s.group[groupKey] && s.group[groupKey].activeMonitors && s.group[groupKey].activeMonitors[monitorId]){ var monitor = s.group[groupKey].rawMonitorConfigurations[monitorId] @@ -1520,17 +1768,28 @@ module.exports = function(s,config,lang,app,io){ config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file/:mode', config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file/:mode/:f' ], function (req,res){ - var response = {ok:false}; + let response = { ok: false }; res.setHeader('Content-Type', 'application/json'); s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_delete.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) + const monitorId = req.params.id + const groupKey = req.params.ke + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed || + isRestricted && !monitorPermissions[`${monitorId}_video_delete`] + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); return } var time = s.nameToTime(req.params.file) - if(req.query.isUTC === 'true'){ - time = s.utcToLocal(time) - } time = new Date(time) var origURL = req.originalUrl.split('/') var videoParam = origURL[origURL.indexOf(req.params.auth) + 1] @@ -1540,8 +1799,6 @@ module.exports = function(s,config,lang,app,io){ videoSet = 'Cloud Videos' break; } - const groupKey = req.params.ke - const monitorId = req.params.id s.knexQuery({ action: "select", columns: "*", @@ -1552,15 +1809,36 @@ module.exports = function(s,config,lang,app,io){ ['time','=',time] ], limit: 1 - },(err,r) => { + },async (err,r) => { if(r && r[0]){ r=r[0]; - r.filename=s.formattedTime(r.time)+'.'+r.ext; + const originalFileName = `${s.formattedTime(r.time)+'.'+r.ext}` var details = s.parseJSON(r.details) || {} switch(req.params.mode){ + case'archive': + response.ok = true + const unarchive = s.getPostData(req,'unarchive') == '1'; + const archiveResponse = await archiveVideo(r,unarchive) + response.ok = archiveResponse.ok + response.archived = archiveResponse.archived + break; case'fix': - response.ok = true; - s.video('fix',r) + await reEncodeVideoAndReplace(r) + break; + case'compress': + response.ok = true + reEncodeVideoAndBinOriginalAddToQueue({ + video: r, + targetVideoCodec: 'vp9', + targetAudioCodec: 'libopus', + targetQuality: '-q:v 1 -q:a 1', + targetExtension: 'webm', + doSlowly: false + }).then((encodeResponse) => { + s.debugLog('Complete Compression',encodeResponse) + }).catch((err) => { + console.log(err) + }) break; case'status': r.f = 'video_edit' diff --git a/libs/webServerStreamPaths.js b/libs/webServerStreamPaths.js index 3e379531..96dc6d34 100644 --- a/libs/webServerStreamPaths.js +++ b/libs/webServerStreamPaths.js @@ -3,7 +3,6 @@ var fs = require('fs'); var bodyParser = require('body-parser'); var os = require('os'); var moment = require('moment'); -var request = require('request'); var execSync = require('child_process').execSync; var exec = require('child_process').exec; var spawn = require('child_process').spawn; @@ -11,6 +10,24 @@ var httpProxy = require('http-proxy'); var proxy = httpProxy.createProxyServer({}) var ejs = require('ejs'); module.exports = function(s,config,lang,app){ + function cantLiveStreamPermission(user,monitorId,permission){ + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId) + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user) + if( + isRestrictedApiKey && apiKeyPermissions[`${permission}_disallowed`] || + isRestricted && !monitorPermissions[`${monitorId}_monitors`] + ){ + return true + } + return false + } var noCache = function(res){ res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate') res.setHeader('Expires', '-1') @@ -22,6 +39,11 @@ module.exports = function(s,config,lang,app){ app.get([config.webPaths.apiPrefix+':auth/embed/:ke/:id',config.webPaths.apiPrefix+':auth/embed/:ke/:id/:addon'], function (req,res){ req.params.protocol=req.protocol; s.auth(req.params,function(user){ + const monitorId = req.params.id + if(cantLiveStreamPermission(user,monitorId)){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return; + } noCache(res) if(user.permissions.watch_stream==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ res.end(user.lang['Not Permitted']) @@ -45,6 +67,11 @@ module.exports = function(s,config,lang,app){ */ app.get([config.webPaths.apiPrefix+':auth/mp4/:ke/:id/:channel/s.mp4',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/s.mp4',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/:channel/s.ts',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/s.ts'], function (req, res) { s.auth(req.params,function(user){ + const monitorId = req.params.id + if(cantLiveStreamPermission(user,monitorId)){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return; + } if(!s.group[req.params.ke] || !s.group[req.params.ke].activeMonitors[req.params.id]){ res.status(404); res.end('404 : Monitor not found'); @@ -109,6 +136,11 @@ module.exports = function(s,config,lang,app){ res.end() }else{ s.auth(req.params,function(user){ + const monitorId = req.params.id + if(cantLiveStreamPermission(user,monitorId)){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return; + } s.checkChildProxy(req.params,function(){ if(s.group[req.params.ke]&&s.group[req.params.ke].activeMonitors[req.params.id]){ if(user.permissions.watch_stream==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ @@ -117,10 +149,11 @@ module.exports = function(s,config,lang,app){ } var Emitter + const chosenChannel = parseInt(req.params.channel) + config.pipeAddition if(!req.params.channel){ Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitter }else{ - Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition] + Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitterChannel[chosenChannel] } res.writeHead(200, { 'Content-Type': 'multipart/x-mixed-replace; boundary=shinobi', @@ -168,9 +201,18 @@ module.exports = function(s,config,lang,app){ * API : Get HLS Stream */ app.get([config.webPaths.apiPrefix+':auth/hls/:ke/:id/:file',config.webPaths.apiPrefix+':auth/hls/:ke/:id/:channel/:file'], function (req,res){ - req.fn=function(user){ + s.auth(req.params,function(user){ + const monitorId = req.params.id + if(cantLiveStreamPermission(user,monitorId)){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return; + } s.checkChildProxy(req.params,function(){ noCache(res) + if(user.permissions.watch_stream==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ + res.end(user.lang['Not Permitted']) + return + } req.dir=s.dir.streams+req.params.ke+'/'+req.params.id+'/' if(req.params.channel){ req.dir+='channel'+(parseInt(req.params.channel)+config.pipeAddition)+'/'+req.params.file; @@ -184,14 +226,18 @@ module.exports = function(s,config,lang,app){ res.end(lang['File Not Found']) } },res,req) - } - s.auth(req.params,req.fn,res,req); + },res,req); }) /** * API : Get JPEG Snapshot */ app.get(config.webPaths.apiPrefix+':auth/jpeg/:ke/:id/s.jpg', function(req,res){ s.auth(req.params,function(user){ + const monitorId = req.params.id + if(cantLiveStreamPermission(user,monitorId)){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return; + } s.checkChildProxy(req.params,function(){ noCache(res) if(user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors&&user.details.monitors.indexOf(req.params.id)===-1){ @@ -218,9 +264,10 @@ module.exports = function(s,config,lang,app){ */ app.get(config.webPaths.apiPrefix+':auth/icon/:ke/:id', function(req,res){ s.auth(req.params,async (user) => { - if(user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors&&user.details.monitors.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) - return + const monitorId = req.params.id + if(cantLiveStreamPermission(user,monitorId,'watch_snapshot')){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return; } const flags = req.query.noflags ? '' : req.query.flags || '-s 200x200' res.writeHead(200, { @@ -242,6 +289,10 @@ module.exports = function(s,config,lang,app){ */ app.get([config.webPaths.apiPrefix+':auth/flv/:ke/:id/s.flv',config.webPaths.apiPrefix+':auth/flv/:ke/:id/:channel/s.flv'], function(req,res) { s.auth(req.params,function(user){ + if(cantLiveStreamPermission(user,monitorId)){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return; + } s.checkChildProxy(req.params,function(){ noCache(res) var Emitter,chunkChannel @@ -289,50 +340,6 @@ module.exports = function(s,config,lang,app){ },res,req) }) /** - * API : Get H.265/h265 HEVC stream - */ - app.get([config.webPaths.apiPrefix+':auth/h265/:ke/:id/s.hevc',config.webPaths.apiPrefix+':auth/h265/:ke/:id/:channel/s.hevc'], function(req,res) { - s.auth(req.params,function(user){ - s.checkChildProxy(req.params,function(){ - noCache(res) - var Emitter,chunkChannel - if(!req.params.channel){ - Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition] - chunkChannel = parseInt(req.params.channel)+config.pipeAddition - } - //variable name of contentWriter - var contentWriter - //set headers - res.setHeader('Content-Type', 'video/mp4'); - res.setHeader('Access-Control-Allow-Origin','*'); - var ip = s.getClientIp(req) - s.camera('watch_on',{ - id : req.params.id, - ke : req.params.ke - },{ - id : req.params.auth + ip + req.headers['user-agent'] - }) - //write new frames as they happen - Emitter.on('data',contentWriter=function(buffer){ - res.write(buffer) - }) - //remove contentWriter when client leaves - res.on('close', function () { - Emitter.removeListener('data',contentWriter) - s.camera('watch_off',{ - id : req.params.id, - ke : req.params.ke - },{ - id : req.params.auth + ip + req.headers['user-agent'] - }) - }) - },res,req) - },res,req) - }) - /** * API : Get H.264 over HTTP */ app.get([ @@ -343,6 +350,10 @@ module.exports = function(s,config,lang,app){ config.webPaths.apiPrefix+':auth/h264/:ke/:id' ], function (req, res) { s.auth(req.params,function(user){ + if(cantLiveStreamPermission(user,monitorId)){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized']}); + return; + } s.checkChildProxy(req.params,function(){ noCache(res) if(!req.query.feed){req.query.feed='1'} diff --git a/libs/webServerSuperPaths.js b/libs/webServerSuperPaths.js index bfbccf97..e8698036 100644 --- a/libs/webServerSuperPaths.js +++ b/libs/webServerSuperPaths.js @@ -1,7 +1,6 @@ var fs = require('fs'); var os = require('os'); var moment = require('moment') -var request = require('request') var exec = require('child_process').exec; var spawn = require('child_process').spawn; var execSync = require('child_process').execSync; diff --git a/package-lock.json b/package-lock.json index da4c7dfa..7497c537 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,39 +12,46 @@ "aws-sdk": "^2.731.0", "backblaze-b2": "^0.9.12", "body-parser": "^1.19.0", + "bson": "^4.6.1", "connection-tester": "^0.2.0", - "cws": "^1.2.11", + "cws": "^2.0.0", + "digest-fetch": "^1.2.1", "discord.js": "^12.2.0", - "ejs": "^2.5.5", + "ejs": "^2.7.4", "express": "^4.16.4", - "express-fileupload": "^1.1.6-alpha.6", + "express-fileupload": "^1.4.0", "fs-extra": "9.0.1", "ftp-srv": "^4.4.0", - "googleapis": "^71.0.0", + "googleapis": "^100.0.0", "http-proxy": "^1.17.0", "jsonfile": "^3.0.1", "knex": "^0.21.4", - "ldapauth-fork": "^5.0.1", - "moment": "^2.27.0", + "ldapauth-fork": "^5.0.2", + "moment": "^2.29.4", "mp4frag": "^0.2.0", + "mqtt": "^4.3.7", "mysql": "^2.18.1", - "node-fetch": "3.0.0-beta.9", - "node-ssh": "^11.1.1", - "node-telegram-bot-api": "^0.52.0", + "node-abort-controller": "^3.0.1", + "node-fetch": "^2.6.7", + "node-onvif-events": "^2.0.5", + "node-ssh": "^12.0.4", + "node-telegram-bot-api": "^0.58.0", "nodemailer": "^6.4.11", - "pam-diff": "^1.0.0", + "pam-diff": "^1.1.0", "path": "^0.12.7", "pipe2pam": "^0.6.2", - "request": "^2.88.0", + "pixel-change": "^1.1.0", + "pushover-notifications": "^1.2.2", "sat": "^0.7.1", "shinobi-onvif": "0.1.9", - "shinobi-sound-detection": "^0.1.8", + "shinobi-sound-detection": "^0.1.13", + "shinobi-zwave": "^1.0.11", "smtp-server": "^3.5.0", - "socket.io": "^2.3.0", - "socket.io-client": "^2.3.0", + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1", "tree-kill": "1.2.2", "unzipper": "0.10.11", - "webdav-fs": "^3.0.0" + "webdav-fs": "^4.0.1" }, "bin": { "shinobi": "camera.js" @@ -53,12 +60,14 @@ "node_modules/@discordjs/collection": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", - "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" + "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==", + "deprecated": "no longer supported" }, "node_modules/@discordjs/form-data": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -68,18 +77,46 @@ "node": ">= 6" } }, + "node_modules/@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz", + "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==" + }, + "node_modules/@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "node_modules/@types/ldapjs": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-1.0.10.tgz", - "integrity": "sha512-AMkMxkK/wjYtWebNH2O+rARfo7scBpW3T23g6zmGCwDgbyDbR79XWpcSqhPWdU+fChaF+I3dVyl9X2dT1CyI9w==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.2.tgz", + "integrity": "sha512-U5HdnwIZ5uZa+f3usxdqgyfNmOROxOxXvQdQtsu6sKo8fte5vej9br2csHxPvXreAbAO1bs8/rdEzvCLpi67nQ==", "dependencies": { "@types/node": "*" } }, "node_modules/@types/node": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", - "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==" + "version": "16.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", + "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==" }, "node_modules/abort-controller": { "version": "3.0.0", @@ -109,11 +146,6 @@ "node": ">= 0.6" } }, - "node_modules/after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -126,9 +158,9 @@ } }, "node_modules/agent-base/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dependencies": { "ms": "2.1.2" }, @@ -162,9 +194,9 @@ } }, "node_modules/ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "engines": { "node": ">=8" } @@ -245,11 +277,6 @@ "es-abstract": "^1.17.4" } }, - "node_modules/arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" - }, "node_modules/arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", @@ -259,9 +286,9 @@ } }, "node_modules/asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "dependencies": { "safer-buffer": "~2.1.0" } @@ -275,9 +302,9 @@ } }, "node_modules/assertion-error": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", - "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", "engines": { "node": "*" } @@ -291,9 +318,9 @@ } }, "node_modules/async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -320,10 +347,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.924.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.924.0.tgz", - "integrity": "sha512-EwJmZDNhEY1/hrihile8+EdrYrT5VKcLuL5F+OA9L+AYWxNou0i4fP36N5KFtMikkAGB31qhAuRDPcr132RnUw==", - "hasInstallScript": true, + "version": "2.1030.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1030.0.tgz", + "integrity": "sha512-to0STOb8DsSGuSsUb/WCbg/UFnMGfIYavnJH5ZlRCHzvCFjTyR+vfE8ku+qIZvfFM4+5MNTQC/Oxfun2X/TuyA==", "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -336,7 +362,7 @@ "xml2js": "0.4.19" }, "engines": { - "node": ">= 0.8.0" + "node": ">= 10.0.0" } }, "node_modules/aws-sign2": { @@ -353,11 +379,11 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "node_modules/axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", "dependencies": { - "follow-redirects": "^1.10.0" + "follow-redirects": "^1.14.4" } }, "node_modules/backblaze-b2": { @@ -410,9 +436,9 @@ } }, "node_modules/base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" }, "node_modules/base/node_modules/define-property": { "version": "1.0.0", @@ -433,14 +459,6 @@ "node": ">=0.12.0" } }, - "node_modules/base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -487,9 +505,9 @@ "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, "node_modules/big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==", + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", "engines": { "node": ">=0.6" } @@ -515,18 +533,42 @@ } }, "node_modules/bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dependencies": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, - "node_modules/blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/bluebird": { "version": "3.7.2", @@ -601,6 +643,40 @@ "node": ">=0.10.0" } }, + "node_modules/bson": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.6.1.tgz", + "integrity": "sha512-I1LQ7Hz5zgwR4QquilLNZwbhPw0Apx7i7X9kGMBTsqPdml/03Q9NBtD9nt/19ahjlphktQImrnderxqpzeVDjw==", + "dependencies": { + "buffer": "^5.6.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/bson/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -617,9 +693,9 @@ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, "node_modules/buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "node_modules/buffer-indexof-polyfill": { "version": "1.0.2", @@ -637,6 +713,15 @@ "node": ">=0.2.0" } }, + "node_modules/buildcheck": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.3.tgz", + "integrity": "sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==", + "optional": true, + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/bunyan": { "version": "1.8.15", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", @@ -718,14 +803,19 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "node_modules/chai": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-1.7.2.tgz", - "integrity": "sha1-ugfr1OGsE4opbN9pB3znS39KExc=", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "dependencies": { - "assertion-error": "1.0.0" + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" }, "engines": { - "node": ">= 0.4.0" + "node": ">=4" } }, "node_modules/chainsaw": { @@ -739,6 +829,22 @@ "node": "*" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=", + "engines": { + "node": "*" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", + "engines": { + "node": "*" + } + }, "node_modules/class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -891,20 +997,38 @@ "node": ">= 6" } }, - "node_modules/component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + "node_modules/commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "dependencies": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, - "node_modules/component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + "node_modules/compute-array-constructors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-array-constructors/-/compute-array-constructors-1.0.1.tgz", + "integrity": "sha1-h+16ZZZo8yv0cM4hybsiSWSJJmU=" + }, + "node_modules/compute-array-dtype": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-array-dtype/-/compute-array-dtype-1.0.1.tgz", + "integrity": "sha1-0EkmtoSQIkWQU5pK6BLxikmY2/M=" + }, + "node_modules/compute-dtype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/compute-dtype/-/compute-dtype-1.0.0.tgz", + "integrity": "sha1-90ZfwmgWbitaK4pktXzsaRGNNXQ=", + "dependencies": { + "compute-array-dtype": "^1.0.0", + "type-name": "^1.0.1" + } }, "node_modules/compute-incrmmean": { "version": "1.0.2", @@ -914,10 +1038,29 @@ "validate.io-positive-integer": "^1.0.0" } }, + "node_modules/compute-indexspace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-indexspace/-/compute-indexspace-1.0.1.tgz", + "integrity": "sha1-2B0KWCFFyW5Ls5k2rr2ubMV+hX8=", + "dependencies": { + "validate.io-nonnegative-integer": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } + }, "node_modules/compute-qmean": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/compute-qmean/-/compute-qmean-1.0.0.tgz", - "integrity": "sha1-e4UvolbK3QTKsqyFNpp2nkfz7S4=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compute-qmean/-/compute-qmean-2.0.0.tgz", + "integrity": "sha512-RfK2okcvka8kdc3ePKShaw2DRc5CZZwUFxM4bZA8n/Lb3/zb3H5PtUy46IFApk8HZ3TcFhhgj0nId2ueGwNzwQ==", + "dependencies": { + "compute-array-constructors": "^1.0.0", + "dstructs-matrix": "^2.0.0", + "validate.io-array-like": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-matrix-like": "^1.0.2", + "validate.io-object": "^1.0.4", + "validate.io-positive-integer": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } }, "node_modules/concat-map": { "version": "0.0.1", @@ -925,16 +1068,16 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "engines": [ - "node >= 0.8" + "node >= 6.0" ], "dependencies": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, @@ -943,6 +1086,16 @@ "resolved": "https://registry.npmjs.org/connection-tester/-/connection-tester-0.2.1.tgz", "integrity": "sha512-EPARP2G3rGQQFhNozGA/bqq8WBgbZlv7tSJJTWX0EJcBzj5EgXTFBVO5of2JZlYNd3Z1i9sO2GANWWVf4GB21g==" }, + "node_modules/const-max-uint32": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/const-max-uint32/-/const-max-uint32-1.0.2.tgz", + "integrity": "sha1-8Am7YjDmeO2HTdLWqc2ePL+rtnY=" + }, + "node_modules/const-pinf-float64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/const-pinf-float64/-/const-pinf-float64-1.0.0.tgz", + "integrity": "sha1-9u+w15+cCYbT558pI6v5twtj1yY=" + }, "node_modules/content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -988,12 +1141,46 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "node_modules/cws": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/cws/-/cws-1.2.15.tgz", - "integrity": "sha512-H+KHHxkRyAHgJtoEmqjD4owlj/y6u1JCkhxNp7m+XA1Nhd9w56HOquhj8XaGEWjHdXQCk/vND0/NLthLBQr4Sg==", + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "dependencies": { - "ws": "^7.4.6" + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cpu-features": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.3.tgz", + "integrity": "sha512-p6C/uud4F4bDyCz9+BNU22KdV1AGxPK6L9rQG9x3x4SSzdMPyBPErP7Rxn8avT2ex1M2g5Rpjz5Us/ri/766Qg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "buildcheck": "0.0.3", + "nan": "^2.15.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=", + "engines": { + "node": "*" + } + }, + "node_modules/cws": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cws/-/cws-2.0.0.tgz", + "integrity": "sha512-kKI08jiYEE9td+506Q3mqbRGI5L5gRpEzZYMNnKwtHVKC46E1Y5ZZHE2LeH46FD7+t2+l/DB1u7bGMujNaDeFw==", + "dependencies": { + "ws": "^8.2.2" }, "funding": { "url": "https://github.com/sponsors/finwo" @@ -1010,14 +1197,6 @@ "node": ">=0.10" } }, - "node_modules/data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==", - "engines": { - "node": ">= 6" - } - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -1042,6 +1221,17 @@ "node": ">=0.10" } }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, "node_modules/define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -1105,30 +1295,126 @@ "node": ">=4.5.0" } }, - "node_modules/discord.js": { - "version": "12.5.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", - "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", + "node_modules/digest-fetch": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.2.1.tgz", + "integrity": "sha512-Do130fdya/9DPUl80PaltD7R0WIOKkROnLjrdh0CxvS+s3WGGWFMPZvB2eH48BRnVxlSKrLdp/wBH19f0Kag6Q==", "dependencies": { - "@discordjs/collection": "^0.1.6", + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, + "node_modules/digest-fetch/node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" + }, + "node_modules/discord.js": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.2.0.tgz", + "integrity": "sha512-Ueb/0SOsxXyqwvwFYFe0msMrGqH1OMqpp2Dpbplnlr4MzcRrFWwsBM9gKNZXPVBHWUKiQkwU8AihXBXIvTTSvg==", + "deprecated": "no longer supported", + "dependencies": { + "@discordjs/collection": "^0.1.5", "@discordjs/form-data": "^3.0.1", "abort-controller": "^3.0.0", - "node-fetch": "^2.6.1", - "prism-media": "^1.2.9", + "node-fetch": "^2.6.0", + "prism-media": "^1.2.0", "setimmediate": "^1.0.5", "tweetnacl": "^1.0.3", - "ws": "^7.4.4" + "ws": "^7.2.1" }, "engines": { "node": ">=12.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "erlpack": "discordapp/erlpack", + "libsodium-wrappers": "^0.7.6", + "sodium": "^3.0.2", + "utf-8-validate": "^5.0.2", + "zlib-sync": "^0.1.6" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "erlpack": { + "optional": true + }, + "libsodium-wrappers": { + "optional": true + }, + "sodium": { + "optional": true + }, + "utf-8-validate": { + "optional": true + }, + "zlib-sync": { + "optional": true + } } }, - "node_modules/discord.js/node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", + "node_modules/discord.js/node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", "engines": { - "node": "4.x || >=6.0.0" + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/dstructs-array-constructors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dstructs-array-constructors/-/dstructs-array-constructors-1.0.2.tgz", + "integrity": "sha1-DwaK1bN9AUpEAhpPnJ3Q2JpSZFo=" + }, + "node_modules/dstructs-array-dtype": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dstructs-array-dtype/-/dstructs-array-dtype-1.0.2.tgz", + "integrity": "sha1-w2j7bkYinfokCfcAzEOiCbSA524=" + }, + "node_modules/dstructs-cast-arrays": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dstructs-cast-arrays/-/dstructs-cast-arrays-1.0.3.tgz", + "integrity": "sha1-8sugaep0MdrV13lgGlHVmXbCvCU=", + "dependencies": { + "dstructs-array-constructors": "^1.0.2", + "dstructs-array-dtype": "^1.0.2", + "type-name": "^1.0.1", + "validate.io-array-like": "^1.0.1" + } + }, + "node_modules/dstructs-matrix": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/dstructs-matrix/-/dstructs-matrix-2.1.2.tgz", + "integrity": "sha1-tJYlYAogtCHGCdGRG7HYOCkHR9o=", + "dependencies": { + "compute-dtype": "^1.0.0", + "compute-indexspace": "^1.0.1", + "dstructs-cast-arrays": "^1.0.2", + "utils-copy": "^1.0.0", + "validate.io-array": "^1.0.6", + "validate.io-contains": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-integer-primitive": "^1.0.0", + "validate.io-nan": "^1.0.3", + "validate.io-nonnegative-integer": "^1.0.0", + "validate.io-nonnegative-integer-array": "^1.0.1", + "validate.io-number-primitive": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" } }, "node_modules/dtrace-provider": { @@ -1152,6 +1438,39 @@ "readable-stream": "^2.0.2" } }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, "node_modules/ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -1205,97 +1524,162 @@ } }, "node_modules/engine.io": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", - "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", + "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", - "debug": "~4.1.0", - "engine.io-parser": "~2.2.0", - "ws": "~7.4.2" + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" }, "engines": { - "node": ">=8.0.0" + "node": ">=10.0.0" } }, "node_modules/engine.io-client": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", - "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz", + "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==", "dependencies": { - "component-emitter": "~1.3.0", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.2.0", + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", "has-cors": "1.1.0", - "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.6.2", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0", "yeast": "0.1.2" } }, "node_modules/engine.io-client/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "ms": "2.0.0" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, "node_modules/engine.io-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", - "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", "dependencies": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.4", - "blob": "0.0.5", - "has-binary2": "~1.0.2" + "@socket.io/base64-arraybuffer": "~1.0.2" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/engine.io/node_modules/cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==", + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", "engines": { "node": ">= 0.6" } }, "node_modules/engine.io/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/engine.io/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, "node_modules/es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "dependencies": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", "string.prototype.trimend": "^1.0.4", @@ -1630,9 +2014,12 @@ "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" }, "node_modules/fast-xml-parser": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz", - "integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==", + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz", + "integrity": "sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==", + "dependencies": { + "strnum": "^1.0.4" + }, "bin": { "xml2js": "cli.js" }, @@ -1641,19 +2028,6 @@ "url": "https://paypal.me/naturalintelligence" } }, - "node_modules/fetch-blob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz", - "integrity": "sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==", - "engines": { - "node": "^10.17.0 || >=12.3.0" - }, - "peerDependenciesMeta": { - "domexception": { - "optional": true - } - } - }, "node_modules/file-type": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", @@ -1762,9 +2136,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", - "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==", + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==", "funding": [ { "type": "individual", @@ -1880,6 +2254,11 @@ "node": ">= 10.0.0" } }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "node_modules/fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -1895,9 +2274,9 @@ } }, "node_modules/ftp-srv": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ftp-srv/-/ftp-srv-4.4.0.tgz", - "integrity": "sha512-n/a9Z62VPvgdb/ltPSilSvH+hffGgwCJCVaHmGCVYgYxvh+GlyStapg3TNBXrKstBINRDe5wQGfl03RQ4+b1/w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/ftp-srv/-/ftp-srv-4.5.0.tgz", + "integrity": "sha512-AzUJwQqLGLndMPpVfGq54CzFZhd1ebFLNfiOatLtRSoNfmLpxQFnmpxwjdOsWChyDk7oWs0Pf5cDarlcFdgPIw==", "dependencies": { "bluebird": "^3.5.1", "bunyan": "^1.8.12", @@ -1920,32 +2299,24 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "node_modules/gaxios": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.0.tgz", - "integrity": "sha512-pHplNbslpwCLMyII/lHPWFQbJWOX0B3R1hwBEOvzYi1GmdKZruuEHK4N9V6f7tf1EaPYyF80mui1+344p6SmLg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.2.tgz", + "integrity": "sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q==", "dependencies": { "abort-controller": "^3.0.0", "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" + "node-fetch": "^2.6.1" }, "engines": { "node": ">=10" } }, - "node_modules/gaxios/node_modules/node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "engines": { - "node": "4.x || >=6.0.0" - } - }, "node_modules/gcp-metadata": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", - "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", "dependencies": { "gaxios": "^4.0.0", "json-bigint": "^1.0.0" @@ -1962,6 +2333,14 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=", + "engines": { + "node": "*" + } + }, "node_modules/get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -1975,6 +2354,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -1997,18 +2391,22 @@ } }, "node_modules/glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dependencies": { + "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "2 || 3", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" }, "engines": { "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/global-modules": { @@ -2040,9 +2438,9 @@ } }, "node_modules/google-auth-library": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.1.1.tgz", - "integrity": "sha512-+Q1linq/To3DYLyPz4UTEkQ0v5EOXadMM/S+taLV3W9611hq9zqg8kgGApqbTQnggtwdO9yU1y2YT7+83wdTRg==", + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.10.2.tgz", + "integrity": "sha512-M37o9Kxa/TLvOLgF71SXvLeVEP5sbSTmKl1zlIgl72SFy5PtsU3pOdu8G8MIHHpQ3/NZabDI8rQkA9DvQVKkPA==", "dependencies": { "arrify": "^2.0.0", "base64-js": "^1.3.0", @@ -2059,11 +2457,11 @@ } }, "node_modules/google-p12-pem": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", - "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", "dependencies": { - "node-forge": "^0.10.0" + "node-forge": "^1.0.0" }, "bin": { "gp12-pem": "build/src/bin/gp12-pem.js" @@ -2073,9 +2471,9 @@ } }, "node_modules/googleapis": { - "version": "71.0.0", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-71.0.0.tgz", - "integrity": "sha512-pi3aWckvsEJbZmJ/+MeO8eIWOK2wmSKRRLvCQbgqTLrFce/jEHLusPly+H3FkpmbO3oTtYRUMJJ07Y4F3DeOgA==", + "version": "100.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-100.0.0.tgz", + "integrity": "sha512-RToFQGY54B756IDbjdyjb1vWFmn03bYpXHB2lIf0eq2UBYsIbYOLZ0kqSomfJnpclEukwEmMF7Jn6Wsev871ew==", "dependencies": { "google-auth-library": "^7.0.2", "googleapis-common": "^5.0.2" @@ -2085,9 +2483,9 @@ } }, "node_modules/googleapis-common": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.0.2.tgz", - "integrity": "sha512-TL7qronKNZwE/XBvqshwzCPmZGq2gz/beXzANF7EVoO7FsQjOd7dk40DYrXkoCpvbnJHCQKWESq6NansiIPFqA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.0.5.tgz", + "integrity": "sha512-o2dgoW4x4fLIAN+IVAOccz3mEH8Lj1LP9c9BSSvkNJEn+U7UZh0WSr4fdH08x5VH7+sstIpd1lOYFZD0g7j4pw==", "dependencies": { "extend": "^3.0.2", "gaxios": "^4.0.0", @@ -2109,14 +2507,14 @@ } }, "node_modules/graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "node_modules/gtoken": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", - "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.1.tgz", + "integrity": "sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ==", "dependencies": { "gaxios": "^4.0.0", "google-p12-pem": "^3.0.3", @@ -2166,19 +2564,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "dependencies": { - "isarray": "2.0.1" - } - }, - "node_modules/has-binary2/node_modules/isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - }, "node_modules/has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", @@ -2195,6 +2580,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -2239,6 +2638,15 @@ "he": "bin/he" } }, + "node_modules/help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "dependencies": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, "node_modules/homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -2266,6 +2674,42 @@ "html": "bin/html.js" } }, + "node_modules/html/node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/html/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/html/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/http-errors": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", @@ -2321,9 +2765,9 @@ } }, "node_modules/https-proxy-agent/node_modules/debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "dependencies": { "ms": "2.1.2" }, @@ -2357,11 +2801,6 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, - "node_modules/indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -2381,6 +2820,19 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "node_modules/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -2431,19 +2883,23 @@ } }, "node_modules/is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dependencies": { + "has-bigints": "^1.0.1" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dependencies": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2458,9 +2914,9 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "node_modules/is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", "engines": { "node": ">= 0.4" }, @@ -2469,9 +2925,9 @@ } }, "node_modules/is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "dependencies": { "has": "^1.0.3" }, @@ -2491,9 +2947,12 @@ } }, "node_modules/is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2542,9 +3001,9 @@ } }, "node_modules/is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dependencies": { "is-extglob": "^2.1.1" }, @@ -2575,9 +3034,12 @@ } }, "node_modules/is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2608,12 +3070,12 @@ } }, "node_modules/is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dependencies": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -2633,18 +3095,32 @@ "node": ">=0.10.0" } }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -2682,6 +3158,17 @@ "node": ">=0.10.0" } }, + "node_modules/is-weakref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", + "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", + "dependencies": { + "call-bind": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -2721,6 +3208,11 @@ "node": ">= 0.6.0" } }, + "node_modules/js-sdsl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-2.1.4.tgz", + "integrity": "sha512-/Ew+CJWHNddr7sjwgxaVeIORIH4AMVC9dy0hPf540ZGMVgS9d3ajwuVdyhDt6/QUvT8ATjR3yuYBKsS79F+H4A==" + }, "node_modules/jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -2735,9 +3227,9 @@ } }, "node_modules/json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "node_modules/json-schema-traverse": { "version": "0.4.1", @@ -2758,17 +3250,30 @@ } }, "node_modules/jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jsprim/node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "engines": [ "node >=0.6.0" ], "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" } }, "node_modules/jwa": { @@ -2799,9 +3304,9 @@ } }, "node_modules/knex": { - "version": "0.21.19", - "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.19.tgz", - "integrity": "sha512-6etvrq9XI1Ck6mEc/XiXFGVpD1Lmj6v9XWojqZgEbOvyMbW7XRvgZ99yIhN/kaBH+43FEy3xv/AcbRaH+1pJtw==", + "version": "0.21.21", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.21.tgz", + "integrity": "sha512-cjw5qO1EzVKjbywcVa61IQJMLt7PfYBRI/2NwCA/B9beXgbw652wDNLz+JM+UKKNsfwprq0ugYqBYc9q4JN36A==", "dependencies": { "colorette": "1.2.1", "commander": "^6.2.0", @@ -2868,6 +3373,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/layerr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/layerr/-/layerr-0.1.2.tgz", + "integrity": "sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ==" + }, "node_modules/ldap-filter": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", @@ -2880,11 +3390,11 @@ } }, "node_modules/ldapauth-fork": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.1.tgz", - "integrity": "sha512-EdELQz8zgPruqV2y88PAuAiZCgTaMjex/kEA2PIcOlPYFt75C9QFt5HGZKVQo8Sf/3Mwnr1AtiThHKcq+pRtEg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.2.tgz", + "integrity": "sha512-fWrrBwJ162rzQIIqfPsfCHy/861kEalQNIu16gmwOMr5cmdfjNkw7XfTlzCTJHybnFg9oW9WaX4DGXa0xiGPmA==", "dependencies": { - "@types/ldapjs": "^1.0.9", + "@types/ldapjs": "^2.2.2", "bcryptjs": "^2.4.0", "ldapjs": "^2.2.1", "lru-cache": "^6.0.0" @@ -2894,9 +3404,9 @@ } }, "node_modules/ldapjs": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.0.tgz", - "integrity": "sha512-3Rbm3CS7vzTccpP1QnzKCEPok60L/b3BFlWU8r93P5oadCAaqCWEH9Td08crPnw4Ti20W8y0+ZKtFFNzxVu4kA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.1.tgz", + "integrity": "sha512-kf0tHHLrpwKaBAQOhYHXgdeh2PkFuCCxWgLb1MRn67ZQVo787D2pij3mmHVZx193GIdM8xcfi8HF6AIYYnj0fQ==", "dependencies": { "abstract-logging": "^2.0.0", "asn1": "^0.2.4", @@ -2911,6 +3421,14 @@ "node": ">=10.13.0" } }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -2950,6 +3468,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3005,6 +3528,16 @@ "node": ">=0.10.0" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -3061,28 +3594,28 @@ } }, "node_modules/mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "dependencies": { - "mime-db": "1.48.0" + "mime-db": "1.51.0" }, "engines": { "node": ">= 0.6" } }, "node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -3091,9 +3624,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "node_modules/mixin-deep": { "version": "1.3.2", @@ -3131,6 +3664,110 @@ "resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.2.0.tgz", "integrity": "sha512-zrLws5vFuUvaivVXu4ZPg7fdJynSbcIT6kI00okZ+jCvxqMIs6zhhh7sw16BE+lL1OD6RyCsFgJEdzxZaeb5fQ==" }, + "node_modules/mqtt": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", + "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "dependencies": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + }, + "bin": { + "mqtt": "bin/mqtt.js", + "mqtt_pub": "bin/pub.js", + "mqtt_sub": "bin/sub.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt-packet/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt-packet/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mqtt/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mqtt/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/mqtt/node_modules/ws": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", + "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -3172,10 +3809,32 @@ "node": "*" } }, + "node_modules/mysql/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/mysql/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", "optional": true }, "node_modules/nanomatch": { @@ -3221,43 +3880,44 @@ "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==" }, - "node_modules/node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==" + "node_modules/node-abort-controller": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz", + "integrity": "sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==" }, "node_modules/node-fetch": { - "version": "3.0.0-beta.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.9.tgz", - "integrity": "sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "dependencies": { - "data-uri-to-buffer": "^3.0.1", - "fetch-blob": "^2.1.1" + "whatwg-url": "^5.0.0" }, "engines": { - "node": "^10.17 || >=12.3" + "node": "4.x || >=6.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", "engines": { - "node": ">= 6.0.0" + "node": ">= 6.13.0" } }, - "node_modules/node-gyp-build": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz", - "integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==", - "bin": { - "node-gyp-build": "bin.js", - "node-gyp-build-optional": "optional.js", - "node-gyp-build-test": "build-test.js" + "node_modules/node-onvif-events": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/node-onvif-events/-/node-onvif-events-2.0.5.tgz", + "integrity": "sha512-rZOHirBe/O47qD0zdjUBQn9dPFO9VdCw6ZSQGUN/Grjxh9p+LOZImN3kLuDdfNfczI8gIfg4JUV0LnSUSxzIeA==", + "dependencies": { + "onvif": "git+https://github.com/agsh/onvif.git" } }, "node_modules/node-sha1": { @@ -3266,24 +3926,25 @@ "integrity": "sha1-Mu2EfYUTFXuW3sa3noxHvK63jhw=" }, "node_modules/node-ssh": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-11.1.1.tgz", - "integrity": "sha512-B3Tb3t54nCj2PyA8vnUMeH19Z2hybJzg5n4t9mRCOTfVGwGlJrv0frDjhPjisTAg3JplJiSxzfImOTMvFPkraQ==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-12.0.4.tgz", + "integrity": "sha512-5M3FBeAWjEpAQvVakQde6CeviEoEiYb6IjJL9mrMen9at63GAv0Q5vOFHFP+SM1Y7pTN3EBvJ/I+oxn2Lpydbw==", "dependencies": { + "is-stream": "^2.0.0", "make-dir": "^3.1.0", "sb-promise-queue": "^2.1.0", "sb-scandir": "^3.1.0", "shell-escape": "^0.2.0", - "ssh2": "^0.8.9" + "ssh2": "^1.5.0" }, "engines": { - "node": ">= 8" + "node": ">= 10" } }, "node_modules/node-telegram-bot-api": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.52.0.tgz", - "integrity": "sha512-HOOHJ14OcuAWcVZQb5kth2lrWJeeOdaO7XFdYXcJT9Dxpznm7iZDHBq9ODLknDTE4dhDMDL6TsjjgYV468gtDQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.58.0.tgz", + "integrity": "sha512-DmP5wBON9stOiunvUw/NvTb1clMYvj+c3NnSqbPZdVd6hNkNRnM97eqPZIH4UsBJ+4n+XFGpU33dCzjqD1sv3A==", "dependencies": { "array.prototype.findindex": "^2.0.2", "bl": "^1.2.3", @@ -3301,6 +3962,15 @@ "node": ">=0.12" } }, + "node_modules/node-telegram-bot-api/node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "node_modules/node-telegram-bot-api/node_modules/debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -3319,14 +3989,75 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/node-telegram-bot-api/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/node-telegram-bot-api/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/node-telegram-bot-api/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/nodemailer": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.1.tgz", - "integrity": "sha512-1xzFN3gqv+/qJ6YRyxBxfTYstLNt0FCtZaFRvf4Sg9wxNGWbwFmGXVpfSi6ThGK6aRxAo+KjHtYSW8NvCsNSAg==", + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.1.tgz", + "integrity": "sha512-E1C8G3rnXrGjznwGP1k+OrW5k4rl0XtqTEB19f7vtJAMYwfxZVSsAu2iY5xJkrZsbVYr6PwwAwRmFlakPoFC0A==", "engines": { "node": ">=6.0.0" } }, + "node_modules/number-allocator": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.10.tgz", + "integrity": "sha512-K4AvNGKo9lP6HqsZyfSr9KDaqnwFzW203inhQEOwFrmFaYevpdX4VNwdOLk197aHujzbT//z6pCBrCOUYSM5iw==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "^2.1.2" + } + }, + "node_modules/number-allocator/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/number-allocator/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "node_modules/oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", @@ -3335,6 +4066,14 @@ "node": "*" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -3414,9 +4153,9 @@ } }, "node_modules/object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3513,6 +4252,38 @@ "wrappy": "1" } }, + "node_modules/onvif": { + "version": "0.6.5", + "resolved": "git+ssh://git@github.com/agsh/onvif.git#9742ea117b2d19eb31deb54cfb9e2b98f21f9d1f", + "license": "MIT", + "dependencies": { + "lodash.get": "^4.4.2", + "xml2js": "^0.4.23" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/onvif/node_modules/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/onvif/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -3547,12 +4318,15 @@ } }, "node_modules/pam-diff": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pam-diff/-/pam-diff-1.0.0.tgz", - "integrity": "sha512-ihMxKsNC4EDLIqtQbWzWZvvamAvxF0NVLG6p4LPurkYRwBPXJlVjNcvkSMrEy6eIrerhO3V0LVKTxGZYS4aPbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pam-diff/-/pam-diff-1.1.0.tgz", + "integrity": "sha512-4Xo6u4amQzhMcff372t7UfZBqmXd06av/GDVD6dQWyND7a4nW42ScJf5yr2WYf6JHTdPdVG82cDquuJkGI1FYA==", "dependencies": { - "pixel-change": "^1.0.0", + "pixel-change": "1.1.0", "polygon-points": "^0.6.0" + }, + "engines": { + "node": ">=10" } }, "node_modules/parse-filepath": { @@ -3661,29 +4435,14 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "node_modules/pcm-boilerplate": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pcm-boilerplate/-/pcm-boilerplate-0.1.1.tgz", - "integrity": "sha1-y4O0mf4lCMD2wYM4AD/OI54VCTE=", - "dependencies": { - "async": "0.2.x", - "chai": "1.7.x", - "underscore": "1.4.x" - }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", "engines": { - "node": ">=0.10" + "node": "*" } }, - "node_modules/pcm-boilerplate/node_modules/async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - }, - "node_modules/pcm-boilerplate/node_modules/underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" - }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -3700,13 +4459,31 @@ "integrity": "sha512-gUWldPYgNjCp1q8qKpTsSalDqXWaLlaXVO+la1jgiJMbMlokMdOhzNyVAsRKJR23FVyPOAUHdi2YpDfneSOcbw==" }, "node_modules/pixel-change": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pixel-change/-/pixel-change-1.0.0.tgz", - "integrity": "sha512-wSMUgbWPUWaFqR+rJH3GOrSpRe6MaoHxPaY4NhchhSVeQmKai+8a+zRBn49RJkgRmHx9RzLmQyl1A+qzf5LTWw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pixel-change/-/pixel-change-1.1.0.tgz", + "integrity": "sha512-p0J+CXVpeULyzlQTFzRnNcvQnbSn5kOw6qlMWPE09JNybicy/rr6ZC3AS6Z2gKhHINmo62KzynxQNlRIk6YJNQ==", "hasInstallScript": true, "dependencies": { - "node-addon-api": "^1.6.3", - "node-gyp-build": "^3.8.0" + "node-addon-api": "^4.2.0", + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/pixel-change/node_modules/node-addon-api": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.2.0.tgz", + "integrity": "sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==" + }, + "node_modules/pixel-change/node_modules/node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" } }, "node_modules/polygon-points": { @@ -3731,12 +4508,12 @@ } }, "node_modules/prism-media": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.9.tgz", - "integrity": "sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.4.tgz", + "integrity": "sha512-eW7LXORkTCQznZs+eqe9VjGOrLBxcBPXgNyHXMTSRVhphvd/RrxgIR7WaWt4fkLuhshcdT5KHL88LAfcvS3f5g==", "peerDependencies": { - "@discordjs/opus": "^0.5.0", - "ffmpeg-static": "^4.2.7 || ^3.0.0 || ^2.4.0", + "@discordjs/opus": "^0.8.0", + "ffmpeg-static": "^5.0.2 || ^4.2.7 || ^3.0.0 || ^2.4.0", "node-opus": "^0.3.3", "opusscript": "^0.0.8" }, @@ -3786,9 +4563,9 @@ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -3802,6 +4579,14 @@ "node": ">=6" } }, + "node_modules/pushover-notifications": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-1.2.2.tgz", + "integrity": "sha512-+3Xcj+kiMiouZK1Ws8yGBTyl8WMPZZdELgl/iVxYqNwDdlaObBHMhEGPRC6Zb9t0BE27ikOoOqSIO1cKZOtsDA==", + "engines": { + "node": "*" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -3823,6 +4608,7 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", "engines": { "node": ">=0.4.x" } @@ -3855,17 +4641,16 @@ } }, "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, "node_modules/rechoir": { @@ -3891,6 +4676,16 @@ "node": ">=0.10.0" } }, + "node_modules/regex-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regex-regex/-/regex-regex-1.0.0.tgz", + "integrity": "sha1-kEih6uuHD01IDavHb8Qs3MC8OnI=" + }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, "node_modules/repeat-element": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", @@ -4042,6 +4837,11 @@ "node": ">=0.12" } }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "node_modules/rimraf": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", @@ -4053,6 +4853,21 @@ "rimraf": "bin.js" } }, + "node_modules/rimraf/node_modules/glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "dependencies": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", @@ -4222,14 +5037,51 @@ } }, "node_modules/shinobi-sound-detection": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/shinobi-sound-detection/-/shinobi-sound-detection-0.1.8.tgz", - "integrity": "sha512-rZzLQ+pbJNb4IDV7fljzuGRKrEJcAw01kzB1E0R9F7lwrKXZhLXSD2H32dgQS71+V75308j1uEhv+SYTsfdq7g==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/shinobi-sound-detection/-/shinobi-sound-detection-0.1.13.tgz", + "integrity": "sha512-QWGzkGENV5y+3Bc7DTAAggXaTWVr7UT+yRX65uKjtDEE9aqU5sW9sck0k/D74PaRS+p/vBaCYMCbyJH+q21FZA==", "dependencies": { + "async": "3.2.0", + "chai": "4.3.4", "compute-incrmmean": "^1.0.2", - "compute-qmean": "^1.0.0", - "pcm-boilerplate": "^0.1.1", - "underscore": "^1.8.3" + "compute-qmean": "^2.0.0", + "underscore": "^1.13.1" + } + }, + "node_modules/shinobi-sound-detection/node_modules/async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + }, + "node_modules/shinobi-zwave": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/shinobi-zwave/-/shinobi-zwave-1.0.11.tgz", + "integrity": "sha512-3torRdlIGkpM+TBHOqgCY0VcBe7FmNptLP8TAAq5YHZBmvHtIu4kjU28dwhi3+a7MjEPtP9T9/OANyebMxqg4A==", + "funding": [ + { + "type": "PayPal", + "url": "https://paypal.me/ShinobiCCTV" + }, + { + "type": "ShinobiShop", + "url": "https://licenses.shinobi.video" + } + ], + "dependencies": { + "request": "^2.88.2" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/smtp-server": { @@ -4245,6 +5097,14 @@ "node": ">=6.0.0" } }, + "node_modules/smtp-server/node_modules/nodemailer": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.1.tgz", + "integrity": "sha512-1xzFN3gqv+/qJ6YRyxBxfTYstLNt0FCtZaFRvf4Sg9wxNGWbwFmGXVpfSi6ThGK6aRxAo+KjHtYSW8NvCsNSAg==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -4405,111 +5265,129 @@ } }, "node_modules/socket.io": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz", - "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", "dependencies": { - "debug": "~4.1.0", - "engine.io": "~3.5.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.4.0", - "socket.io-parser": "~3.4.0" + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/socket.io-adapter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", - "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==" }, "node_modules/socket.io-client": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", - "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz", + "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==", "dependencies": { - "backo2": "1.0.2", - "component-bind": "1.0.0", - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "engine.io-client": "~3.5.0", - "has-binary2": "~1.0.2", - "indexof": "0.0.1", - "parseqs": "0.0.6", + "@socket.io/component-emitter": "~3.0.0", + "backo2": "~1.0.2", + "debug": "~4.3.2", + "engine.io-client": "~6.1.1", "parseuri": "0.0.6", - "socket.io-parser": "~3.3.0", - "to-array": "0.1.4" + "socket.io-parser": "~4.1.1" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/socket.io-client/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "ms": "2.0.0" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/socket.io-client/node_modules/isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/socket.io-client/node_modules/socket.io-parser": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", - "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.2.tgz", + "integrity": "sha512-j3kk71QLJuyQ/hh5F/L2t1goqzdTL0gvDzuhTuNSwihfuFUrcSji0qFZmJJPtG6Rmug153eOPsUizeirf1IIog==", "dependencies": { - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "isarray": "2.0.1" + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" } }, "node_modules/socket.io-parser": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", - "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", "dependencies": { - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "isarray": "2.0.1" + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" } }, - "node_modules/socket.io-parser/node_modules/component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, "node_modules/socket.io-parser/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/socket.io-parser/node_modules/isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - }, "node_modules/socket.io-parser/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/socket.io/node_modules/debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "deprecated": "Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dependencies": { - "ms": "^2.1.1" + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, "node_modules/socket.io/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/source-map": { "version": "0.5.7", @@ -4547,6 +5425,14 @@ "node": ">=0.10.0" } }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, "node_modules/sqlstring": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", @@ -4556,27 +5442,20 @@ } }, "node_modules/ssh2": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", - "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.8.0.tgz", + "integrity": "sha512-NVIRkIwJvWl+mcRozp+EBzHMVCcbDKBea64ToPdZEk43yAVGwmfqYZRPFRnnvGjsKC34wYCmiupTcKgCVNVNNg==", + "hasInstallScript": true, "dependencies": { - "ssh2-streams": "~0.4.10" + "asn1": "^0.2.4", + "bcrypt-pbkdf": "^1.0.2" }, "engines": { - "node": ">=5.2.0" - } - }, - "node_modules/ssh2-streams": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", - "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", - "dependencies": { - "asn1": "~0.2.0", - "bcrypt-pbkdf": "^1.0.2", - "streamsearch": "~0.1.2" + "node": ">=10.16.0" }, - "engines": { - "node": ">=5.2.0" + "optionalDependencies": { + "cpu-features": "0.0.3", + "nan": "^2.15.0" } }, "node_modules/sshpk": { @@ -4712,6 +5591,11 @@ "node": ">=0.10.0" } }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, "node_modules/streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", @@ -4721,21 +5605,40 @@ } }, "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "dependencies": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" } }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" @@ -4766,16 +5669,21 @@ } }, "node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "node_modules/tarn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz", @@ -4797,11 +5705,6 @@ "node": ">=8" } }, - "node_modules/to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" - }, "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -4870,6 +5773,11 @@ "node": ">=0.8" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, "node_modules/traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -4902,6 +5810,14 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -4914,6 +5830,11 @@ "node": ">= 0.6" } }, + "node_modules/type-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-1.1.0.tgz", + "integrity": "sha1-rZw/fDMPWy8I3n159W0rlFHkKw4=" + }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -5050,6 +5971,28 @@ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" }, + "node_modules/unzipper/node_modules/readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/unzipper/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -5079,9 +6022,9 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, "node_modules/url-parse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", - "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -5118,6 +6061,45 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/utils-copy": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/utils-copy/-/utils-copy-1.1.1.tgz", + "integrity": "sha1-biuXmCqozXPhGCo+b4vsPA9AWKc=", + "dependencies": { + "const-pinf-float64": "^1.0.0", + "object-keys": "^1.0.9", + "type-name": "^2.0.0", + "utils-copy-error": "^1.0.0", + "utils-indexof": "^1.0.0", + "utils-regex-from-string": "^1.0.0", + "validate.io-array": "^1.0.3", + "validate.io-buffer": "^1.0.1", + "validate.io-nonnegative-integer": "^1.0.0" + } + }, + "node_modules/utils-copy-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-copy-error/-/utils-copy-error-1.0.1.tgz", + "integrity": "sha1-eR3jk8DwmJCv1Z88vqY18HmpT6U=", + "dependencies": { + "object-keys": "^1.0.9", + "utils-copy": "^1.1.0" + } + }, + "node_modules/utils-copy/node_modules/type-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-2.0.2.tgz", + "integrity": "sha1-7+fUEj2KxSr/9/QMfk3sUmYAj7Q=" + }, + "node_modules/utils-indexof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-indexof/-/utils-indexof-1.0.0.tgz", + "integrity": "sha1-IP6r8J7xAYtSNkPoOA57yD7GG1w=", + "dependencies": { + "validate.io-array-like": "^1.0.1", + "validate.io-integer-primitive": "^1.0.0" + } + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5126,6 +6108,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utils-regex-from-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-regex-from-string/-/utils-regex-from-string-1.0.0.tgz", + "integrity": "sha1-/hopCfjeD/DVGCyA+8ZU1qaH0Yk=", + "dependencies": { + "regex-regex": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } + }, "node_modules/uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -5146,6 +6137,39 @@ "node": ">= 0.10" } }, + "node_modules/validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha1-W1osr9j4uFq7L4hroVPy2Tond00=" + }, + "node_modules/validate.io-array-like": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-array-like/-/validate.io-array-like-1.0.2.tgz", + "integrity": "sha1-evn363tRcVvrIhVmjsXM5U+t21o=", + "dependencies": { + "const-max-uint32": "^1.0.2", + "validate.io-integer-primitive": "^1.0.0" + } + }, + "node_modules/validate.io-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-buffer/-/validate.io-buffer-1.0.2.tgz", + "integrity": "sha1-hS1nNAIZFNXROvwyUxdh43IO1E4=" + }, + "node_modules/validate.io-contains": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-contains/-/validate.io-contains-1.0.0.tgz", + "integrity": "sha1-vwm6TyfGQlB7CQXbs6dKUncInP4=", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-nan-primitive": "^1.0.0" + } + }, + "node_modules/validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha1-NDoZgC7TsZaCaceA5VjpNBHAutc=" + }, "node_modules/validate.io-integer": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", @@ -5154,11 +6178,64 @@ "validate.io-number": "^1.0.3" } }, + "node_modules/validate.io-integer-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-primitive/-/validate.io-integer-primitive-1.0.0.tgz", + "integrity": "sha1-qaoBA1X+hoHA/qbBp0rSQZyt3cY=", + "dependencies": { + "validate.io-number-primitive": "^1.0.0" + } + }, + "node_modules/validate.io-matrix-like": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-matrix-like/-/validate.io-matrix-like-1.0.2.tgz", + "integrity": "sha1-XsMqddCInaxzbepovdYUWxVe38M=" + }, + "node_modules/validate.io-nan": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-nan/-/validate.io-nan-1.0.3.tgz", + "integrity": "sha1-1DjhOGjJy9N/26EllN5Nj+FEMKQ=" + }, + "node_modules/validate.io-nan-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-nan-primitive/-/validate.io-nan-primitive-1.0.0.tgz", + "integrity": "sha1-R1zC0DXQuvLQCRItg+opGjLX+ww=" + }, + "node_modules/validate.io-nonnegative-integer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-nonnegative-integer/-/validate.io-nonnegative-integer-1.0.0.tgz", + "integrity": "sha1-gGkkOgjF+Y6VQTySnf17GPP28p8=", + "dependencies": { + "validate.io-integer": "^1.0.5" + } + }, + "node_modules/validate.io-nonnegative-integer-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/validate.io-nonnegative-integer-array/-/validate.io-nonnegative-integer-array-1.0.1.tgz", + "integrity": "sha1-ZjMKZl9VmLlvJfaQgfgfYy6k208=", + "dependencies": { + "validate.io-array": "^1.0.3", + "validate.io-nonnegative-integer": "^1.0.0" + } + }, "node_modules/validate.io-number": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", "integrity": "sha1-9j/+2iSL8opnqNSODjtGGhZluvg=" }, + "node_modules/validate.io-number-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-number-primitive/-/validate.io-number-primitive-1.0.0.tgz", + "integrity": "sha1-0uAfICmJNp3PEVVElWQgOv5YTlU=" + }, + "node_modules/validate.io-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/validate.io-object/-/validate.io-object-1.0.4.tgz", + "integrity": "sha1-3KAezu45DhENvCr4Q8gfe/M6Qas=", + "dependencies": { + "validate.io-array": "^1.0.1" + } + }, "node_modules/validate.io-positive-integer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/validate.io-positive-integer/-/validate.io-positive-integer-1.0.0.tgz", @@ -5167,6 +6244,11 @@ "validate.io-integer": "^1.0.5" } }, + "node_modules/validate.io-string-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/validate.io-string-primitive/-/validate.io-string-primitive-1.0.1.tgz", + "integrity": "sha1-uBNbn7E3K94C/dU60dDM1t55j+4=" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -5176,9 +6258,9 @@ } }, "node_modules/vasync": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.0.tgz", - "integrity": "sha1-z951GGChWCLbOxMrxZsRakra8Bs=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", "engines": [ "node >=0.6.0" ], @@ -5186,7 +6268,7 @@ "verror": "1.10.0" } }, - "node_modules/verror": { + "node_modules/vasync/node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", @@ -5199,30 +6281,64 @@ "extsprintf": "^1.2.0" } }, - "node_modules/webdav": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-3.6.2.tgz", - "integrity": "sha512-HFRiI1jluMSPQMVgxVD6VVYNtaglO53vHG0uf7Zec+wl0A1Mei2z8/IFgAAAJMUuEWAx2AfBD5lcWhAiYA9LUw==", - "deprecated": "WebDAV 3.x is deprecated: Please upgrade to v4.", + "node_modules/verror": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", "dependencies": { - "axios": "^0.21.1", - "base-64": "^0.1.0", - "fast-xml-parser": "^3.17.4", + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/webdav": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-4.8.0.tgz", + "integrity": "sha512-CVJvxu0attEfoQUKraDiNh3uMjNPNl+BY0pbcKbyc/X+8IXDnqAT4tT4Ge12w+j49fYuVpFVkpEGwBZabv7Uhw==", + "dependencies": { + "axios": "^0.24.0", + "base-64": "^1.0.0", + "fast-xml-parser": "^3.19.0", "he": "^1.2.0", "hot-patcher": "^0.5.0", + "layerr": "^0.1.2", + "md5": "^2.3.0", "minimatch": "^3.0.4", "nested-property": "^4.0.0", "path-posix": "^1.0.0", "url-join": "^4.0.1", - "url-parse": "^1.4.7" + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=10" } }, "node_modules/webdav-fs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/webdav-fs/-/webdav-fs-3.0.0.tgz", - "integrity": "sha512-/rwiAPiPymjHXSgobrze/jMIvap+9cF75A7iKvUCUDrU58MIEEp4tsBtt4gQlOQWHkqAeMbcUBrW6xMNf8JYhg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/webdav-fs/-/webdav-fs-4.0.1.tgz", + "integrity": "sha512-2W3UCjvFQ+V8vHFiRPuM/TR8cPjhvTeFq3wG/xZKJgjxhnh4trJvxoG9RczUWBPFtkVJD/rDuuCwtD8jNmYtdw==", "dependencies": { - "webdav": "^3.4.0" + "webdav": "^4.7.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "node_modules/which": { @@ -5275,11 +6391,11 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", "engines": { - "node": ">=8.3.0" + "node": ">=10.0.0" }, "peerDependencies": { "bufferutil": "^4.0.1", @@ -5312,13 +6428,21 @@ } }, "node_modules/xmlhttprequest-ssl": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", - "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", "engines": { "node": ">=0.4.0" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", @@ -5384,18 +6508,43 @@ "mime-types": "^2.1.12" } }, + "@socket.io/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@socket.io/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-dOlCBKnDw4iShaIsH/bxujKTM18+2TOAsYz+KSc11Am38H4q5Xw8Bbz97ZYdrVNM+um3p7w86Bvvmcn9q+5+eQ==" + }, + "@socket.io/component-emitter": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.0.0.tgz", + "integrity": "sha512-2pTGuibAXJswAPJjaKisthqS/NOK5ypG4LYT6tEAV0S/mxW0zOIvYvGK0V8w8+SHxAm6vRMSjqSalFXeBAqs+Q==" + }, + "@types/component-emitter": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.11.tgz", + "integrity": "sha512-SRXjM+tfsSlA9VuG8hGO2nft2p8zjXCK1VcC6N4NXbBbYbSia9kzCChYQajIjzIqOOOuh5Ock6MmV2oux4jDZQ==" + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.12", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz", + "integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==" + }, "@types/ldapjs": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-1.0.10.tgz", - "integrity": "sha512-AMkMxkK/wjYtWebNH2O+rARfo7scBpW3T23g6zmGCwDgbyDbR79XWpcSqhPWdU+fChaF+I3dVyl9X2dT1CyI9w==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.2.tgz", + "integrity": "sha512-U5HdnwIZ5uZa+f3usxdqgyfNmOROxOxXvQdQtsu6sKo8fte5vej9br2csHxPvXreAbAO1bs8/rdEzvCLpi67nQ==", "requires": { "@types/node": "*" } }, "@types/node": { - "version": "15.12.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz", - "integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==" + "version": "16.11.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz", + "integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw==" }, "abort-controller": { "version": "3.0.0", @@ -5419,11 +6568,6 @@ "negotiator": "0.6.2" } }, - "after": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -5433,9 +6577,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -5459,9 +6603,9 @@ } }, "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" }, "ansi-styles": { "version": "4.3.0", @@ -5515,20 +6659,15 @@ "es-abstract": "^1.17.4" } }, - "arraybuffer.slice": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==" - }, "arrify": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==" }, "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "requires": { "safer-buffer": "~2.1.0" } @@ -5539,9 +6678,9 @@ "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=" }, "assertion-error": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.0.tgz", - "integrity": "sha1-x/hUOP3UZrx8oWq5DIFRN5el0js=" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" }, "assign-symbols": { "version": "1.0.0", @@ -5549,9 +6688,9 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=" }, "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.2.tgz", + "integrity": "sha512-H0E+qZaDEfx/FY4t7iLRv1W2fFI6+pyCeTw1uN20AQPiwqwM6ojPxHxdLv4z8hi2DtnW9BOckSspLucW7pIE5g==" }, "asynckit": { "version": "0.4.0", @@ -5569,9 +6708,9 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, "aws-sdk": { - "version": "2.924.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.924.0.tgz", - "integrity": "sha512-EwJmZDNhEY1/hrihile8+EdrYrT5VKcLuL5F+OA9L+AYWxNou0i4fP36N5KFtMikkAGB31qhAuRDPcr132RnUw==", + "version": "2.1030.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1030.0.tgz", + "integrity": "sha512-to0STOb8DsSGuSsUb/WCbg/UFnMGfIYavnJH5ZlRCHzvCFjTyR+vfE8ku+qIZvfFM4+5MNTQC/Oxfun2X/TuyA==", "requires": { "buffer": "4.9.2", "events": "1.1.1", @@ -5595,11 +6734,11 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", "requires": { - "follow-redirects": "^1.10.0" + "follow-redirects": "^1.14.4" } }, "backblaze-b2": { @@ -5656,20 +6795,15 @@ } }, "base-64": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", - "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", + "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" }, "base32.js": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/base32.js/-/base32.js-0.1.0.tgz", "integrity": "sha1-tYLexpPC8R6JPPBk7mrFthMaIgI=" }, - "base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" - }, "base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -5701,9 +6835,9 @@ "integrity": "sha1-mrVie5PmBiH/fNrF2pczAn3x0Ms=" }, "big-integer": { - "version": "1.6.48", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.48.tgz", - "integrity": "sha512-j51egjPa7/i+RdiRuJbPdJ2FIUYYPhvYLjzoYbcMMm62ooO6F94fETG4MTs46zPAF9Brs04OajboA/qTGuz78w==" + "version": "1.6.51", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", + "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==" }, "bignumber.js": { "version": "9.0.1", @@ -5720,19 +6854,31 @@ } }, "bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + } } }, - "blob": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/blob/-/blob-0.0.5.tgz", - "integrity": "sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==" - }, "bluebird": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", @@ -5796,6 +6942,25 @@ } } }, + "bson": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.6.1.tgz", + "integrity": "sha512-I1LQ7Hz5zgwR4QquilLNZwbhPw0Apx7i7X9kGMBTsqPdml/03Q9NBtD9nt/19ahjlphktQImrnderxqpzeVDjw==", + "requires": { + "buffer": "^5.6.0" + }, + "dependencies": { + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + } + } + }, "buffer": { "version": "4.9.2", "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", @@ -5812,9 +6977,9 @@ "integrity": "sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=" }, "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" }, "buffer-indexof-polyfill": { "version": "1.0.2", @@ -5826,6 +6991,12 @@ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=" }, + "buildcheck": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.3.tgz", + "integrity": "sha512-pziaA+p/wdVImfcbsZLNF32EiWyujlQLwolMqUQE8xpKNOH7KmZQaY8sXN7DGOEzPAElo9QTaeNRfGnf3iOJbA==", + "optional": true + }, "bunyan": { "version": "1.8.15", "resolved": "https://registry.npmjs.org/bunyan/-/bunyan-1.8.15.tgz", @@ -5886,11 +7057,16 @@ "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=" }, "chai": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-1.7.2.tgz", - "integrity": "sha1-ugfr1OGsE4opbN9pB3znS39KExc=", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.4.tgz", + "integrity": "sha512-yS5H68VYOCtN1cjfwumDSuzn/9c+yza4f3reKXlE5rUg7SFcCEy90gJvydNgOYtblyf4Zi6jIWRnXOgErta0KA==", "requires": { - "assertion-error": "1.0.0" + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" } }, "chainsaw": { @@ -5901,6 +7077,16 @@ "traverse": ">=0.3.0 <0.4" } }, + "charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=" + }, "class-utils": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", @@ -6023,20 +7209,38 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==" }, - "component-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" + "commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "requires": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } }, "component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" }, - "component-inherit": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" + "compute-array-constructors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-array-constructors/-/compute-array-constructors-1.0.1.tgz", + "integrity": "sha1-h+16ZZZo8yv0cM4hybsiSWSJJmU=" + }, + "compute-array-dtype": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-array-dtype/-/compute-array-dtype-1.0.1.tgz", + "integrity": "sha1-0EkmtoSQIkWQU5pK6BLxikmY2/M=" + }, + "compute-dtype": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/compute-dtype/-/compute-dtype-1.0.0.tgz", + "integrity": "sha1-90ZfwmgWbitaK4pktXzsaRGNNXQ=", + "requires": { + "compute-array-dtype": "^1.0.0", + "type-name": "^1.0.1" + } }, "compute-incrmmean": { "version": "1.0.2", @@ -6046,10 +7250,29 @@ "validate.io-positive-integer": "^1.0.0" } }, + "compute-indexspace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-indexspace/-/compute-indexspace-1.0.1.tgz", + "integrity": "sha1-2B0KWCFFyW5Ls5k2rr2ubMV+hX8=", + "requires": { + "validate.io-nonnegative-integer": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } + }, "compute-qmean": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/compute-qmean/-/compute-qmean-1.0.0.tgz", - "integrity": "sha1-e4UvolbK3QTKsqyFNpp2nkfz7S4=" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compute-qmean/-/compute-qmean-2.0.0.tgz", + "integrity": "sha512-RfK2okcvka8kdc3ePKShaw2DRc5CZZwUFxM4bZA8n/Lb3/zb3H5PtUy46IFApk8HZ3TcFhhgj0nId2ueGwNzwQ==", + "requires": { + "compute-array-constructors": "^1.0.0", + "dstructs-matrix": "^2.0.0", + "validate.io-array-like": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-matrix-like": "^1.0.2", + "validate.io-object": "^1.0.4", + "validate.io-positive-integer": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } }, "concat-map": { "version": "0.0.1", @@ -6057,13 +7280,13 @@ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" }, "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", - "readable-stream": "^2.2.2", + "readable-stream": "^3.0.2", "typedarray": "^0.0.6" } }, @@ -6072,6 +7295,16 @@ "resolved": "https://registry.npmjs.org/connection-tester/-/connection-tester-0.2.1.tgz", "integrity": "sha512-EPARP2G3rGQQFhNozGA/bqq8WBgbZlv7tSJJTWX0EJcBzj5EgXTFBVO5of2JZlYNd3Z1i9sO2GANWWVf4GB21g==" }, + "const-max-uint32": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/const-max-uint32/-/const-max-uint32-1.0.2.tgz", + "integrity": "sha1-8Am7YjDmeO2HTdLWqc2ePL+rtnY=" + }, + "const-pinf-float64": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/const-pinf-float64/-/const-pinf-float64-1.0.0.tgz", + "integrity": "sha1-9u+w15+cCYbT558pI6v5twtj1yY=" + }, "content-disposition": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", @@ -6105,12 +7338,36 @@ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, - "cws": { - "version": "1.2.15", - "resolved": "https://registry.npmjs.org/cws/-/cws-1.2.15.tgz", - "integrity": "sha512-H+KHHxkRyAHgJtoEmqjD4owlj/y6u1JCkhxNp7m+XA1Nhd9w56HOquhj8XaGEWjHdXQCk/vND0/NLthLBQr4Sg==", + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", "requires": { - "ws": "^7.4.6" + "object-assign": "^4", + "vary": "^1" + } + }, + "cpu-features": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.3.tgz", + "integrity": "sha512-p6C/uud4F4bDyCz9+BNU22KdV1AGxPK6L9rQG9x3x4SSzdMPyBPErP7Rxn8avT2ex1M2g5Rpjz5Us/ri/766Qg==", + "optional": true, + "requires": { + "buildcheck": "0.0.3", + "nan": "^2.15.0" + } + }, + "crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + }, + "cws": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/cws/-/cws-2.0.0.tgz", + "integrity": "sha512-kKI08jiYEE9td+506Q3mqbRGI5L5gRpEzZYMNnKwtHVKC46E1Y5ZZHE2LeH46FD7+t2+l/DB1u7bGMujNaDeFw==", + "requires": { + "ws": "^8.2.2" } }, "dashdash": { @@ -6121,11 +7378,6 @@ "assert-plus": "^1.0.0" } }, - "data-uri-to-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz", - "integrity": "sha512-WboRycPNsVw3B3TL559F7kuBUM4d8CgMEvk6xEJlOp7OBPjt6G7z8WMWlD2rOFZLk6OYfFIUGsCOWzcQH9K2og==" - }, "debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -6144,6 +7396,14 @@ "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=" }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, "define-properties": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", @@ -6189,28 +7449,86 @@ "streamsearch": "0.1.2" } }, - "discord.js": { - "version": "12.5.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", - "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", + "digest-fetch": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.2.1.tgz", + "integrity": "sha512-Do130fdya/9DPUl80PaltD7R0WIOKkROnLjrdh0CxvS+s3WGGWFMPZvB2eH48BRnVxlSKrLdp/wBH19f0Kag6Q==", "requires": { - "@discordjs/collection": "^0.1.6", - "@discordjs/form-data": "^3.0.1", - "abort-controller": "^3.0.0", - "node-fetch": "^2.6.1", - "prism-media": "^1.2.9", - "setimmediate": "^1.0.5", - "tweetnacl": "^1.0.3", - "ws": "^7.4.4" + "base-64": "^0.1.0", + "md5": "^2.3.0" }, "dependencies": { - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" + "base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs=" } } }, + "discord.js": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.2.0.tgz", + "integrity": "sha512-Ueb/0SOsxXyqwvwFYFe0msMrGqH1OMqpp2Dpbplnlr4MzcRrFWwsBM9gKNZXPVBHWUKiQkwU8AihXBXIvTTSvg==", + "requires": { + "@discordjs/collection": "^0.1.5", + "@discordjs/form-data": "^3.0.1", + "abort-controller": "^3.0.0", + "node-fetch": "^2.6.0", + "prism-media": "^1.2.0", + "setimmediate": "^1.0.5", + "tweetnacl": "^1.0.3", + "ws": "^7.2.1" + }, + "dependencies": { + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + } + } + }, + "dstructs-array-constructors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dstructs-array-constructors/-/dstructs-array-constructors-1.0.2.tgz", + "integrity": "sha1-DwaK1bN9AUpEAhpPnJ3Q2JpSZFo=" + }, + "dstructs-array-dtype": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/dstructs-array-dtype/-/dstructs-array-dtype-1.0.2.tgz", + "integrity": "sha1-w2j7bkYinfokCfcAzEOiCbSA524=" + }, + "dstructs-cast-arrays": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dstructs-cast-arrays/-/dstructs-cast-arrays-1.0.3.tgz", + "integrity": "sha1-8sugaep0MdrV13lgGlHVmXbCvCU=", + "requires": { + "dstructs-array-constructors": "^1.0.2", + "dstructs-array-dtype": "^1.0.2", + "type-name": "^1.0.1", + "validate.io-array-like": "^1.0.1" + } + }, + "dstructs-matrix": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/dstructs-matrix/-/dstructs-matrix-2.1.2.tgz", + "integrity": "sha1-tJYlYAogtCHGCdGRG7HYOCkHR9o=", + "requires": { + "compute-dtype": "^1.0.0", + "compute-indexspace": "^1.0.1", + "dstructs-cast-arrays": "^1.0.2", + "utils-copy": "^1.0.0", + "validate.io-array": "^1.0.6", + "validate.io-contains": "^1.0.0", + "validate.io-function": "^1.0.2", + "validate.io-integer-primitive": "^1.0.0", + "validate.io-nan": "^1.0.3", + "validate.io-nonnegative-integer": "^1.0.0", + "validate.io-nonnegative-integer-array": "^1.0.1", + "validate.io-number-primitive": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } + }, "dtrace-provider": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", @@ -6226,6 +7544,41 @@ "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", "requires": { "readable-stream": "^2.0.2" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" } }, "ecc-jsbn": { @@ -6274,94 +7627,113 @@ } }, "engine.io": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-3.5.0.tgz", - "integrity": "sha512-21HlvPUKaitDGE4GXNtQ7PLP0Sz4aWLddMPw2VTyFz1FVZqu/kZsJUO8WNpKuE/OCL7nkfRaOui2ZCJloGznGA==", + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.1.3.tgz", + "integrity": "sha512-rqs60YwkvWTLLnfazqgZqLa/aKo+9cueVfEi/dZ8PyGyaf8TLOxj++4QMIgeG3Gn0AhrWiFXvghsoY9L9h25GA==", "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", - "debug": "~4.1.0", - "engine.io-parser": "~2.2.0", - "ws": "~7.4.2" + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.3", + "ws": "~8.2.3" }, "dependencies": { "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==" }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "requires": {} } } }, "engine.io-client": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", - "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.1.1.tgz", + "integrity": "sha512-V05mmDo4gjimYW+FGujoGmmmxRaDsrVr7AXA3ZIfa04MWM1jOfZfUwou0oNqhNwy/votUDvGDt4JA4QF4e0b4g==", "requires": { - "component-emitter": "~1.3.0", - "component-inherit": "0.0.3", - "debug": "~3.1.0", - "engine.io-parser": "~2.2.0", + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.0.0", "has-cors": "1.1.0", - "indexof": "0.0.1", "parseqs": "0.0.6", "parseuri": "0.0.6", - "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.6.2", + "ws": "~8.2.3", + "xmlhttprequest-ssl": "~2.0.0", "yeast": "0.1.2" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "8.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz", + "integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==", + "requires": {} } } }, "engine.io-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-2.2.1.tgz", - "integrity": "sha512-x+dN/fBH8Ro8TFwJ+rkB2AmuVw9Yu2mockR/p3W8f8YtExwFgDvBDi0GWyb4ZLkpahtDGZgtr3zLovanJghPqg==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.0.3.tgz", + "integrity": "sha512-BtQxwF27XUNnSafQLvDi0dQ8s3i6VgzSoQMJacpIcGNrlUdfHSKbgm3jmjCVvQluGzqwujQMPAoMai3oYSTurg==", "requires": { - "after": "0.8.2", - "arraybuffer.slice": "~0.0.7", - "base64-arraybuffer": "0.1.4", - "blob": "0.0.5", - "has-binary2": "~1.0.2" + "@socket.io/base64-arraybuffer": "~1.0.2" } }, "es-abstract": { - "version": "1.18.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.3.tgz", - "integrity": "sha512-nQIr12dxV7SSxE6r6f1l3DtAeEYdsGpps13dR0TwJg1S8gyp4ZPgy3FZcHBgbiQqnoqSTb+oC+kO4UQ0C/J8vw==", + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.19.1.tgz", + "integrity": "sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w==", "requires": { "call-bind": "^1.0.2", "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", "get-intrinsic": "^1.1.1", + "get-symbol-description": "^1.0.0", "has": "^1.0.3", "has-symbols": "^1.0.2", - "is-callable": "^1.2.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.4", "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.10.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.1", + "is-string": "^1.0.7", + "is-weakref": "^1.0.1", + "object-inspect": "^1.11.0", "object-keys": "^1.1.1", "object.assign": "^4.1.2", "string.prototype.trimend": "^1.0.4", @@ -6623,14 +7995,12 @@ "integrity": "sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==" }, "fast-xml-parser": { - "version": "3.19.0", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.19.0.tgz", - "integrity": "sha512-4pXwmBplsCPv8FOY1WRakF970TjNGnGnfbOnLqjlYvMiF1SR3yOHyxMR/YCXpPTOspNF5gwudqktIP4VsWkvBg==" - }, - "fetch-blob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz", - "integrity": "sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==" + "version": "3.21.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-3.21.1.tgz", + "integrity": "sha512-FTFVjYoBOZTJekiUsawGsSYV9QL0A+zDYCRj7y34IO6Jg+2IMYEtQa+bbictpdpV8dHxXywqU7C0gRDEOFtBFg==", + "requires": { + "strnum": "^1.0.4" + } }, "file-type": { "version": "3.9.0", @@ -6715,9 +8085,9 @@ "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==" }, "follow-redirects": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.1.tgz", - "integrity": "sha512-HWqDgT7ZEkqRzBvc2s64vSZ/hfOceEol3ac/7tKwzuvEyWx3/4UegXh5oBOIotkGsObyk3xznnSRVADBgWSQVg==" + "version": "1.14.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.9.tgz", + "integrity": "sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w==" }, "for-in": { "version": "1.0.2", @@ -6794,6 +8164,11 @@ } } }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, "fstream": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", @@ -6806,9 +8181,9 @@ } }, "ftp-srv": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/ftp-srv/-/ftp-srv-4.4.0.tgz", - "integrity": "sha512-n/a9Z62VPvgdb/ltPSilSvH+hffGgwCJCVaHmGCVYgYxvh+GlyStapg3TNBXrKstBINRDe5wQGfl03RQ4+b1/w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/ftp-srv/-/ftp-srv-4.5.0.tgz", + "integrity": "sha512-AzUJwQqLGLndMPpVfGq54CzFZhd1ebFLNfiOatLtRSoNfmLpxQFnmpxwjdOsWChyDk7oWs0Pf5cDarlcFdgPIw==", "requires": { "bluebird": "^3.5.1", "bunyan": "^1.8.12", @@ -6825,28 +8200,21 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "gaxios": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.0.tgz", - "integrity": "sha512-pHplNbslpwCLMyII/lHPWFQbJWOX0B3R1hwBEOvzYi1GmdKZruuEHK4N9V6f7tf1EaPYyF80mui1+344p6SmLg==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-4.3.2.tgz", + "integrity": "sha512-T+ap6GM6UZ0c4E6yb1y/hy2UB6hTrqhglp3XfmU9qbLCGRYhLVV5aRPpC4EmoG8N8zOnkYCgoBz+ScvGAARY6Q==", "requires": { "abort-controller": "^3.0.0", "extend": "^3.0.2", "https-proxy-agent": "^5.0.0", "is-stream": "^2.0.0", - "node-fetch": "^2.3.0" - }, - "dependencies": { - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==" - } + "node-fetch": "^2.6.1" } }, "gcp-metadata": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.2.1.tgz", - "integrity": "sha512-tSk+REe5iq/N+K+SK1XjZJUrFPuDqGZVzCy2vocIHIGmPlTGsa8owXMJwGkrXr73NO0AzhPW4MF2DEHz7P2AVw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-4.3.1.tgz", + "integrity": "sha512-x850LS5N7V1F3UcV7PoupzGsyD6iVwTVvsh3tbXfkctZnBnjW5yu5z1/3k3SehF7TyoTIe78rJs02GMMy+LF+A==", "requires": { "gaxios": "^4.0.0", "json-bigint": "^1.0.0" @@ -6857,6 +8225,11 @@ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha1-6td0q+5y4gQJQzoGY2YCPdaIekE=" + }, "get-intrinsic": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", @@ -6867,6 +8240,15 @@ "has-symbols": "^1.0.1" } }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, "get-value": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", @@ -6886,13 +8268,14 @@ } }, "glob": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", - "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "requires": { + "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "2 || 3", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } @@ -6920,9 +8303,9 @@ } }, "google-auth-library": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.1.1.tgz", - "integrity": "sha512-+Q1linq/To3DYLyPz4UTEkQ0v5EOXadMM/S+taLV3W9611hq9zqg8kgGApqbTQnggtwdO9yU1y2YT7+83wdTRg==", + "version": "7.10.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-7.10.2.tgz", + "integrity": "sha512-M37o9Kxa/TLvOLgF71SXvLeVEP5sbSTmKl1zlIgl72SFy5PtsU3pOdu8G8MIHHpQ3/NZabDI8rQkA9DvQVKkPA==", "requires": { "arrify": "^2.0.0", "base64-js": "^1.3.0", @@ -6936,26 +8319,26 @@ } }, "google-p12-pem": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.0.3.tgz", - "integrity": "sha512-wS0ek4ZtFx/ACKYF3JhyGe5kzH7pgiQ7J5otlumqR9psmWMYc+U9cErKlCYVYHoUaidXHdZ2xbo34kB+S+24hA==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/google-p12-pem/-/google-p12-pem-3.1.3.tgz", + "integrity": "sha512-MC0jISvzymxePDVembypNefkAQp+DRP7dBE+zNUPaIjEspIlYg0++OrsNr248V9tPbz6iqtZ7rX1hxWA5B8qBQ==", "requires": { - "node-forge": "^0.10.0" + "node-forge": "^1.0.0" } }, "googleapis": { - "version": "71.0.0", - "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-71.0.0.tgz", - "integrity": "sha512-pi3aWckvsEJbZmJ/+MeO8eIWOK2wmSKRRLvCQbgqTLrFce/jEHLusPly+H3FkpmbO3oTtYRUMJJ07Y4F3DeOgA==", + "version": "100.0.0", + "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-100.0.0.tgz", + "integrity": "sha512-RToFQGY54B756IDbjdyjb1vWFmn03bYpXHB2lIf0eq2UBYsIbYOLZ0kqSomfJnpclEukwEmMF7Jn6Wsev871ew==", "requires": { "google-auth-library": "^7.0.2", "googleapis-common": "^5.0.2" } }, "googleapis-common": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.0.2.tgz", - "integrity": "sha512-TL7qronKNZwE/XBvqshwzCPmZGq2gz/beXzANF7EVoO7FsQjOd7dk40DYrXkoCpvbnJHCQKWESq6NansiIPFqA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/googleapis-common/-/googleapis-common-5.0.5.tgz", + "integrity": "sha512-o2dgoW4x4fLIAN+IVAOccz3mEH8Lj1LP9c9BSSvkNJEn+U7UZh0WSr4fdH08x5VH7+sstIpd1lOYFZD0g7j4pw==", "requires": { "extend": "^3.0.2", "gaxios": "^4.0.0", @@ -6973,14 +8356,14 @@ } }, "graceful-fs": { - "version": "4.2.6", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", - "integrity": "sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ==" + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.8.tgz", + "integrity": "sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==" }, "gtoken": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.2.1.tgz", - "integrity": "sha512-OY0BfPKe3QnMsY9MzTHTSKn+Vl2l1CcLe6BwDEQj00mbbkl5nyQ/7EUREstg4fQNZ8iYE7br4JJ7TdKeDOPWmw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-5.3.1.tgz", + "integrity": "sha512-yqOREjzLHcbzz1UrQoxhBtpk8KjrVhuqPE7od1K2uhyxG2BHjKZetlbLw/SPZak/QqTIQW+addS+EcjqQsZbwQ==", "requires": { "gaxios": "^4.0.0", "google-p12-pem": "^3.0.3", @@ -7014,21 +8397,6 @@ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" }, - "has-binary2": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", - "requires": { - "isarray": "2.0.1" - }, - "dependencies": { - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - } - } - }, "has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", @@ -7039,6 +8407,14 @@ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "requires": { + "has-symbols": "^1.0.2" + } + }, "has-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", @@ -7073,6 +8449,15 @@ "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==" }, + "help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "requires": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, "homedir-polyfill": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", @@ -7092,6 +8477,41 @@ "integrity": "sha1-pUT6nqVJK/s6LMqCEKEL57WvH2E=", "requires": { "concat-stream": "^1.4.7" + }, + "dependencies": { + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } + } } }, "http-errors": { @@ -7136,9 +8556,9 @@ }, "dependencies": { "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", + "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", "requires": { "ms": "2.1.2" } @@ -7163,11 +8583,6 @@ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" }, - "indexof": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -7187,6 +8602,16 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, "interpret": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", @@ -7225,16 +8650,20 @@ } }, "is-bigint": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.2.tgz", - "integrity": "sha512-0JV5+SOCQkIdzjBK9buARcV804Ddu7A0Qet6sHi3FimE9ne6m4BGQZfRn+NZiXbBk4F4XmHfDZIipLj9pX8dSA==" + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "requires": { + "has-bigints": "^1.0.1" + } }, "is-boolean-object": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.1.tgz", - "integrity": "sha512-bXdQWkECBUIAcCkeH1unwJLIpZYaa5VvuygSyS/c2lf719mTKZDU5UdDRlpd01UjADgmW8RfqaP+mRaVPdr/Ng==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "requires": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" } }, "is-buffer": { @@ -7243,14 +8672,14 @@ "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.3.tgz", - "integrity": "sha512-J1DcMe8UYTBSrKezuIUTUwjXsho29693unXM2YhJUTR2txK/eG47bvNa/wipPFmZFgr/N6f1GA66dv0mEyTIyQ==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", + "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" }, "is-core-module": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz", - "integrity": "sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", "requires": { "has": "^1.0.3" } @@ -7264,9 +8693,12 @@ } }, "is-date-object": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.4.tgz", - "integrity": "sha512-/b4ZVsG7Z5XVtIxs/h9W8nvfLgSAyKYdtGWQLbqy6jA1icmgjf8WCoTKgeS4wy5tYaPePouzFMANbnj94c2Z+A==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-descriptor": { "version": "1.0.2", @@ -7297,9 +8729,9 @@ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "requires": { "is-extglob": "^2.1.1" } @@ -7328,9 +8760,12 @@ } }, "is-number-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.5.tgz", - "integrity": "sha512-RU0lI/n95pMoUKu9v1BZP5MBcZuNSVJkMkAG2dJqC4z2GlkGUNeH68SuHuBKBD/XFe+LHZ+f9BKkLET60Niedw==" + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", + "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-plain-object": { "version": "2.0.4", @@ -7341,12 +8776,12 @@ } }, "is-regex": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.3.tgz", - "integrity": "sha512-qSVXFz28HM7y+IWX6vLCsexdlvzT1PJNFSBuaQLQ5o0IEw8UDYW6/2+eCMVyIsbM8CNLX2a/QWmSpyxYEHY7CQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "requires": { "call-bind": "^1.0.2", - "has-symbols": "^1.0.2" + "has-tostringtag": "^1.0.0" } }, "is-relative": { @@ -7357,15 +8792,23 @@ "is-unc-path": "^1.0.0" } }, + "is-shared-array-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz", + "integrity": "sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==" + }, "is-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", - "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "is-string": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.6.tgz", - "integrity": "sha512-2gdzbKUuqtQ3lYNrUTQYoClPhm7oQu4UdpSZMp1/DGgkHBT8E2Z1l0yMdb6D4zNAxwDiMv8MdulKROJGNl0Q0w==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "requires": { + "has-tostringtag": "^1.0.0" + } }, "is-symbol": { "version": "1.0.4", @@ -7388,6 +8831,14 @@ "unc-path-regex": "^0.1.2" } }, + "is-weakref": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.1.tgz", + "integrity": "sha512-b2jKc2pQZjaeFYWEf7ScFj+Be1I+PXmlu572Q8coTXZ+LD/QQZ7ShPMst8h16riVgyXTQwUsFEl74mDvc/3MHQ==", + "requires": { + "call-bind": "^1.0.0" + } + }, "is-windows": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", @@ -7418,6 +8869,11 @@ "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz", "integrity": "sha1-o/Iiqarp+Wb10nx5ZRDigJF2Qhc=" }, + "js-sdsl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-2.1.4.tgz", + "integrity": "sha512-/Ew+CJWHNddr7sjwgxaVeIORIH4AMVC9dy0hPf540ZGMVgS9d3ajwuVdyhDt6/QUvT8ATjR3yuYBKsS79F+H4A==" + }, "jsbn": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", @@ -7432,9 +8888,9 @@ } }, "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=" + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" }, "json-schema-traverse": { "version": "0.4.1", @@ -7455,14 +8911,26 @@ } }, "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", "requires": { "assert-plus": "1.0.0", "extsprintf": "1.3.0", - "json-schema": "0.2.3", + "json-schema": "0.4.0", "verror": "1.10.0" + }, + "dependencies": { + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } } }, "jwa": { @@ -7490,9 +8958,9 @@ "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, "knex": { - "version": "0.21.19", - "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.19.tgz", - "integrity": "sha512-6etvrq9XI1Ck6mEc/XiXFGVpD1Lmj6v9XWojqZgEbOvyMbW7XRvgZ99yIhN/kaBH+43FEy3xv/AcbRaH+1pJtw==", + "version": "0.21.21", + "resolved": "https://registry.npmjs.org/knex/-/knex-0.21.21.tgz", + "integrity": "sha512-cjw5qO1EzVKjbywcVa61IQJMLt7PfYBRI/2NwCA/B9beXgbw652wDNLz+JM+UKKNsfwprq0ugYqBYc9q4JN36A==", "requires": { "colorette": "1.2.1", "commander": "^6.2.0", @@ -7523,6 +8991,11 @@ } } }, + "layerr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/layerr/-/layerr-0.1.2.tgz", + "integrity": "sha512-ob5kTd9H3S4GOG2nVXyQhOu9O8nBgP555XxWPkJI0tR0JeRilfyTp8WtPdIJHLXBmHMSdEq5+KMxiYABeScsIQ==" + }, "ldap-filter": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/ldap-filter/-/ldap-filter-0.3.3.tgz", @@ -7532,20 +9005,20 @@ } }, "ldapauth-fork": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.1.tgz", - "integrity": "sha512-EdELQz8zgPruqV2y88PAuAiZCgTaMjex/kEA2PIcOlPYFt75C9QFt5HGZKVQo8Sf/3Mwnr1AtiThHKcq+pRtEg==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/ldapauth-fork/-/ldapauth-fork-5.0.2.tgz", + "integrity": "sha512-fWrrBwJ162rzQIIqfPsfCHy/861kEalQNIu16gmwOMr5cmdfjNkw7XfTlzCTJHybnFg9oW9WaX4DGXa0xiGPmA==", "requires": { - "@types/ldapjs": "^1.0.9", + "@types/ldapjs": "^2.2.2", "bcryptjs": "^2.4.0", "ldapjs": "^2.2.1", "lru-cache": "^6.0.0" } }, "ldapjs": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.0.tgz", - "integrity": "sha512-3Rbm3CS7vzTccpP1QnzKCEPok60L/b3BFlWU8r93P5oadCAaqCWEH9Td08crPnw4Ti20W8y0+ZKtFFNzxVu4kA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/ldapjs/-/ldapjs-2.3.1.tgz", + "integrity": "sha512-kf0tHHLrpwKaBAQOhYHXgdeh2PkFuCCxWgLb1MRn67ZQVo787D2pij3mmHVZx193GIdM8xcfi8HF6AIYYnj0fQ==", "requires": { "abstract-logging": "^2.0.0", "asn1": "^0.2.4", @@ -7557,6 +9030,11 @@ "verror": "^1.8.1" } }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==" + }, "liftoff": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", @@ -7590,6 +9068,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==" + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -7627,6 +9110,16 @@ "object-visit": "^1.0.0" } }, + "md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "requires": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -7668,30 +9161,30 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" }, "mime-db": { - "version": "1.48.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.48.0.tgz", - "integrity": "sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==" + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.51.0.tgz", + "integrity": "sha512-5y8A56jg7XVQx2mbv1lu49NR4dokRnhZYTtL+KGfaa27uq4pSTXkwQkFJl4pkRMyNFz/EtYDSkiiEHx3F7UN6g==" }, "mime-types": { - "version": "2.1.31", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.31.tgz", - "integrity": "sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==", + "version": "2.1.34", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.34.tgz", + "integrity": "sha512-6cP692WwGIs9XXdOO4++N+7qjqv0rqxxVvJ3VHPh/Sc9mVZcQP+ZGhkKiTvWMQRr2tbHkJP/Yn7Y0npb3ZBs4A==", "requires": { - "mime-db": "1.48.0" + "mime-db": "1.51.0" } }, "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" } }, "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" }, "mixin-deep": { "version": "1.3.2", @@ -7720,6 +9213,76 @@ "resolved": "https://registry.npmjs.org/mp4frag/-/mp4frag-0.2.0.tgz", "integrity": "sha512-zrLws5vFuUvaivVXu4ZPg7fdJynSbcIT6kI00okZ+jCvxqMIs6zhhh7sw16BE+lL1OD6RyCsFgJEdzxZaeb5fQ==" }, + "mqtt": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", + "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "requires": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "ws": { + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.8.tgz", + "integrity": "sha512-ri1Id1WinAX5Jqn9HejiGb8crfRio0Qgu8+MtL36rlTA6RLsMdWt1Az/19A2Qij6uSHUMphEFaTKa4WG+UNHNw==", + "requires": {} + } + } + }, + "mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "requires": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -7751,13 +9314,35 @@ "version": "9.0.0", "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz", "integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } } } }, "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", + "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==", "optional": true }, "nanomatch": { @@ -7794,29 +9379,31 @@ "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==" }, - "node-addon-api": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", - "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==" + "node-abort-controller": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.0.1.tgz", + "integrity": "sha512-/ujIVxthRs+7q6hsdjHMaj8hRG9NuWmwrz+JdRwZ14jdFoKSkm+vDsCbF9PLpnSqjaWQJuTmVtcWHNLr+vrOFw==" }, "node-fetch": { - "version": "3.0.0-beta.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.0.0-beta.9.tgz", - "integrity": "sha512-RdbZCEynH2tH46+tj0ua9caUHVWrd/RHnRfvly2EVdqGmI3ndS1Vn/xjm5KuGejDt2RNDQsVRLPNd2QPwcewVg==", + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", "requires": { - "data-uri-to-buffer": "^3.0.1", - "fetch-blob": "^2.1.1" + "whatwg-url": "^5.0.0" } }, "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==" + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==" }, - "node-gyp-build": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-3.9.0.tgz", - "integrity": "sha512-zLcTg6P4AbcHPq465ZMFNXx7XpKKJh+7kkN699NiQWisR2uWYOWNWqRHAmbnmKiL4e9aLSlmy5U7rEMUXV59+A==" + "node-onvif-events": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/node-onvif-events/-/node-onvif-events-2.0.5.tgz", + "integrity": "sha512-rZOHirBe/O47qD0zdjUBQn9dPFO9VdCw6ZSQGUN/Grjxh9p+LOZImN3kLuDdfNfczI8gIfg4JUV0LnSUSxzIeA==", + "requires": { + "onvif": "git+https://github.com/agsh/onvif.git" + } }, "node-sha1": { "version": "1.0.1", @@ -7824,21 +9411,22 @@ "integrity": "sha1-Mu2EfYUTFXuW3sa3noxHvK63jhw=" }, "node-ssh": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-11.1.1.tgz", - "integrity": "sha512-B3Tb3t54nCj2PyA8vnUMeH19Z2hybJzg5n4t9mRCOTfVGwGlJrv0frDjhPjisTAg3JplJiSxzfImOTMvFPkraQ==", + "version": "12.0.4", + "resolved": "https://registry.npmjs.org/node-ssh/-/node-ssh-12.0.4.tgz", + "integrity": "sha512-5M3FBeAWjEpAQvVakQde6CeviEoEiYb6IjJL9mrMen9at63GAv0Q5vOFHFP+SM1Y7pTN3EBvJ/I+oxn2Lpydbw==", "requires": { + "is-stream": "^2.0.0", "make-dir": "^3.1.0", "sb-promise-queue": "^2.1.0", "sb-scandir": "^3.1.0", "shell-escape": "^0.2.0", - "ssh2": "^0.8.9" + "ssh2": "^1.5.0" } }, "node-telegram-bot-api": { - "version": "0.52.0", - "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.52.0.tgz", - "integrity": "sha512-HOOHJ14OcuAWcVZQb5kth2lrWJeeOdaO7XFdYXcJT9Dxpznm7iZDHBq9ODLknDTE4dhDMDL6TsjjgYV468gtDQ==", + "version": "0.58.0", + "resolved": "https://registry.npmjs.org/node-telegram-bot-api/-/node-telegram-bot-api-0.58.0.tgz", + "integrity": "sha512-DmP5wBON9stOiunvUw/NvTb1clMYvj+c3NnSqbPZdVd6hNkNRnM97eqPZIH4UsBJ+4n+XFGpU33dCzjqD1sv3A==", "requires": { "array.prototype.findindex": "^2.0.2", "bl": "^1.2.3", @@ -7853,6 +9441,15 @@ "request-promise": "^4.2.2" }, "dependencies": { + "bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "requires": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } + }, "debug": { "version": "3.2.7", "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", @@ -7870,19 +9467,79 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } } } }, "nodemailer": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.1.tgz", - "integrity": "sha512-1xzFN3gqv+/qJ6YRyxBxfTYstLNt0FCtZaFRvf4Sg9wxNGWbwFmGXVpfSi6ThGK6aRxAo+KjHtYSW8NvCsNSAg==" + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.1.tgz", + "integrity": "sha512-E1C8G3rnXrGjznwGP1k+OrW5k4rl0XtqTEB19f7vtJAMYwfxZVSsAu2iY5xJkrZsbVYr6PwwAwRmFlakPoFC0A==" + }, + "number-allocator": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.10.tgz", + "integrity": "sha512-K4AvNGKo9lP6HqsZyfSr9KDaqnwFzW203inhQEOwFrmFaYevpdX4VNwdOLk197aHujzbT//z6pCBrCOUYSM5iw==", + "requires": { + "debug": "^4.3.1", + "js-sdsl": "^2.1.2" + }, + "dependencies": { + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + } + } }, "oauth-sign": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==" }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + }, "object-copy": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", @@ -7945,9 +9602,9 @@ } }, "object-inspect": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.10.3.tgz", - "integrity": "sha512-e5mCJlSH7poANfC8z8S9s9S2IN5/4Zb3aZ33f5s8YqoazCFzNLloLU8r5VCG+G7WoqLvAAZoVMcy3tp/3X0Plw==" + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", + "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" }, "object-keys": { "version": "1.1.1", @@ -8017,6 +9674,30 @@ "wrappy": "1" } }, + "onvif": { + "version": "git+ssh://git@github.com/agsh/onvif.git#9742ea117b2d19eb31deb54cfb9e2b98f21f9d1f", + "from": "onvif@git+https://github.com/agsh/onvif.git", + "requires": { + "lodash.get": "^4.4.2", + "xml2js": "^0.4.23" + }, + "dependencies": { + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" + } + } + }, "p-limit": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", @@ -8039,11 +9720,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" }, "pam-diff": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pam-diff/-/pam-diff-1.0.0.tgz", - "integrity": "sha512-ihMxKsNC4EDLIqtQbWzWZvvamAvxF0NVLG6p4LPurkYRwBPXJlVjNcvkSMrEy6eIrerhO3V0LVKTxGZYS4aPbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pam-diff/-/pam-diff-1.1.0.tgz", + "integrity": "sha512-4Xo6u4amQzhMcff372t7UfZBqmXd06av/GDVD6dQWyND7a4nW42ScJf5yr2WYf6JHTdPdVG82cDquuJkGI1FYA==", "requires": { - "pixel-change": "^1.0.0", + "pixel-change": "1.1.0", "polygon-points": "^0.6.0" } }, @@ -8129,27 +9810,10 @@ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=" }, - "pcm-boilerplate": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pcm-boilerplate/-/pcm-boilerplate-0.1.1.tgz", - "integrity": "sha1-y4O0mf4lCMD2wYM4AD/OI54VCTE=", - "requires": { - "async": "0.2.x", - "chai": "1.7.x", - "underscore": "1.4.x" - }, - "dependencies": { - "async": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", - "integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E=" - }, - "underscore": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.4.4.tgz", - "integrity": "sha1-YaajIBBiKvoHljvzJSA88SI51gQ=" - } - } + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" }, "performance-now": { "version": "2.1.0", @@ -8167,12 +9831,24 @@ "integrity": "sha512-gUWldPYgNjCp1q8qKpTsSalDqXWaLlaXVO+la1jgiJMbMlokMdOhzNyVAsRKJR23FVyPOAUHdi2YpDfneSOcbw==" }, "pixel-change": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/pixel-change/-/pixel-change-1.0.0.tgz", - "integrity": "sha512-wSMUgbWPUWaFqR+rJH3GOrSpRe6MaoHxPaY4NhchhSVeQmKai+8a+zRBn49RJkgRmHx9RzLmQyl1A+qzf5LTWw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pixel-change/-/pixel-change-1.1.0.tgz", + "integrity": "sha512-p0J+CXVpeULyzlQTFzRnNcvQnbSn5kOw6qlMWPE09JNybicy/rr6ZC3AS6Z2gKhHINmo62KzynxQNlRIk6YJNQ==", "requires": { - "node-addon-api": "^1.6.3", - "node-gyp-build": "^3.8.0" + "node-addon-api": "^4.2.0", + "node-gyp-build": "^4.3.0" + }, + "dependencies": { + "node-addon-api": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.2.0.tgz", + "integrity": "sha512-eazsqzwG2lskuzBqCGPi7Ac2UgOoMz8JVOXVhTvvPDYhthvNpefx8jWD8Np7Gv+2Sz0FlPWZk0nJV0z598Wn8Q==" + }, + "node-gyp-build": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.3.0.tgz", + "integrity": "sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q==" + } } }, "polygon-points": { @@ -8191,9 +9867,9 @@ "integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw=" }, "prism-media": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.9.tgz", - "integrity": "sha512-UHCYuqHipbTR1ZsXr5eg4JUmHER8Ss4YEb9Azn+9zzJ7/jlTtD1h0lc4g6tNx3eMlB8Mp6bfll0LPMAV4R6r3Q==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.4.tgz", + "integrity": "sha512-eW7LXORkTCQznZs+eqe9VjGOrLBxcBPXgNyHXMTSRVhphvd/RrxgIR7WaWt4fkLuhshcdT5KHL88LAfcvS3f5g==", "requires": {} }, "process": { @@ -8221,9 +9897,9 @@ "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==" }, "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "requires": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -8234,6 +9910,11 @@ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, + "pushover-notifications": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/pushover-notifications/-/pushover-notifications-1.2.2.tgz", + "integrity": "sha512-+3Xcj+kiMiouZK1Ws8yGBTyl8WMPZZdELgl/iVxYqNwDdlaObBHMhEGPRC6Zb9t0BE27ikOoOqSIO1cKZOtsDA==" + }, "q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", @@ -8271,17 +9952,13 @@ } }, "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } }, "rechoir": { @@ -8301,6 +9978,16 @@ "safe-regex": "^1.1.0" } }, + "regex-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/regex-regex/-/regex-regex-1.0.0.tgz", + "integrity": "sha1-kEih6uuHD01IDavHb8Qs3MC8OnI=" + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha1-M2Hs+jymwYKDOA3Qu5VG85D17Oc=" + }, "repeat-element": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", @@ -8415,12 +10102,31 @@ "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==" }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, "rimraf": { "version": "2.4.5", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", "integrity": "sha1-7nEM5dk6j9uFb7Xqj/Di11k0sto=", "requires": { "glob": "^6.0.1" + }, + "dependencies": { + "glob": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "integrity": "sha1-DwiGD2oVUSey+t1PnOJLGqtuTSI=", + "requires": { + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "2 || 3", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "safe-buffer": { @@ -8569,14 +10275,40 @@ } }, "shinobi-sound-detection": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/shinobi-sound-detection/-/shinobi-sound-detection-0.1.8.tgz", - "integrity": "sha512-rZzLQ+pbJNb4IDV7fljzuGRKrEJcAw01kzB1E0R9F7lwrKXZhLXSD2H32dgQS71+V75308j1uEhv+SYTsfdq7g==", + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/shinobi-sound-detection/-/shinobi-sound-detection-0.1.13.tgz", + "integrity": "sha512-QWGzkGENV5y+3Bc7DTAAggXaTWVr7UT+yRX65uKjtDEE9aqU5sW9sck0k/D74PaRS+p/vBaCYMCbyJH+q21FZA==", "requires": { + "async": "3.2.0", + "chai": "4.3.4", "compute-incrmmean": "^1.0.2", - "compute-qmean": "^1.0.0", - "pcm-boilerplate": "^0.1.1", - "underscore": "^1.8.3" + "compute-qmean": "^2.0.0", + "underscore": "^1.13.1" + }, + "dependencies": { + "async": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", + "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" + } + } + }, + "shinobi-zwave": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/shinobi-zwave/-/shinobi-zwave-1.0.11.tgz", + "integrity": "sha512-3torRdlIGkpM+TBHOqgCY0VcBe7FmNptLP8TAAq5YHZBmvHtIu4kjU28dwhi3+a7MjEPtP9T9/OANyebMxqg4A==", + "requires": { + "request": "^2.88.2" + } + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" } }, "smtp-server": { @@ -8587,6 +10319,13 @@ "base32.js": "0.1.0", "ipv6-normalize": "1.0.1", "nodemailer": "6.6.1" + }, + "dependencies": { + "nodemailer": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.6.1.tgz", + "integrity": "sha512-1xzFN3gqv+/qJ6YRyxBxfTYstLNt0FCtZaFRvf4Sg9wxNGWbwFmGXVpfSi6ThGK6aRxAo+KjHtYSW8NvCsNSAg==" + } } }, "snapdragon": { @@ -8717,113 +10456,97 @@ } }, "socket.io": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-2.4.1.tgz", - "integrity": "sha512-Si18v0mMXGAqLqCVpTxBa8MGqriHGQh8ccEOhmsmNS3thNCGBwO8WGrwMibANsWtQQ5NStdZwHqZR3naJVFc3w==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.4.1.tgz", + "integrity": "sha512-s04vrBswdQBUmuWJuuNTmXUVJhP0cVky8bBDhdkf8y0Ptsu7fKU2LuLbts9g+pdmAdyMMn8F/9Mf1/wbtUN0fg==", "requires": { - "debug": "~4.1.0", - "engine.io": "~3.5.0", - "has-binary2": "~1.0.2", - "socket.io-adapter": "~1.1.0", - "socket.io-client": "2.4.0", - "socket.io-parser": "~3.4.0" + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "debug": "~4.3.2", + "engine.io": "~6.1.0", + "socket.io-adapter": "~2.3.3", + "socket.io-parser": "~4.0.4" }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, "socket.io-adapter": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-1.1.2.tgz", - "integrity": "sha512-WzZRUj1kUjrTIrUKpZLEzFZ1OLj5FwLlAFQs9kuZJzJi5DKdU7FsWc36SNmA8iDOtwBQyT8FkrriRM8vXLYz8g==" + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.3.3.tgz", + "integrity": "sha512-Qd/iwn3VskrpNO60BeRyCyr8ZWw9CPZyitW4AQwmRZ8zCiyDiL+znRnWX6tDHXnWn1sJrM1+b6Mn6wEDJJ4aYQ==" }, "socket.io-client": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-2.4.0.tgz", - "integrity": "sha512-M6xhnKQHuuZd4Ba9vltCLT9oa+YvTsP8j9NcEiLElfIg8KeYPyhWOes6x4t+LTAC8enQbE/995AdTem2uNyKKQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.4.1.tgz", + "integrity": "sha512-N5C/L5fLNha5Ojd7Yeb/puKcPWWcoB/A09fEjjNsg91EDVr5twk/OEyO6VT9dlLSUNY85NpW6KBhVMvaLKQ3vQ==", "requires": { - "backo2": "1.0.2", - "component-bind": "1.0.0", - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "engine.io-client": "~3.5.0", - "has-binary2": "~1.0.2", - "indexof": "0.0.1", - "parseqs": "0.0.6", + "@socket.io/component-emitter": "~3.0.0", + "backo2": "~1.0.2", + "debug": "~4.3.2", + "engine.io-client": "~6.1.1", "parseuri": "0.0.6", - "socket.io-parser": "~3.3.0", - "to-array": "0.1.4" + "socket.io-parser": "~4.1.1" }, "dependencies": { "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "ms": "2.0.0" + "ms": "2.1.2" } }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "socket.io-parser": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.3.2.tgz", - "integrity": "sha512-FJvDBuOALxdCI9qwRrO/Rfp9yfndRtc1jSgVgV8FDraihmSP/MLGD5PEuJrNfjALvcQ+vMDM/33AWOYP/JSjDg==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.1.2.tgz", + "integrity": "sha512-j3kk71QLJuyQ/hh5F/L2t1goqzdTL0gvDzuhTuNSwihfuFUrcSji0qFZmJJPtG6Rmug153eOPsUizeirf1IIog==", "requires": { - "component-emitter": "~1.3.0", - "debug": "~3.1.0", - "isarray": "2.0.1" + "@socket.io/component-emitter": "~3.0.0", + "debug": "~4.3.1" } } } }, "socket.io-parser": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-3.4.1.tgz", - "integrity": "sha512-11hMgzL+WCLWf1uFtHSNvliI++tcRUWdoeYuwIl+Axvwy9z2gQM+7nJyN3STj1tLj5JyIUH8/gpDGxzAlDdi0A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", + "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", "requires": { - "component-emitter": "1.2.1", - "debug": "~4.1.0", - "isarray": "2.0.1" + "@types/component-emitter": "^1.2.10", + "component-emitter": "~1.3.0", + "debug": "~4.3.1" }, "dependencies": { - "component-emitter": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", - "integrity": "sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=" - }, "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, - "isarray": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" - }, "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" } } }, @@ -8857,27 +10580,28 @@ "extend-shallow": "^3.0.0" } }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + } + }, "sqlstring": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz", "integrity": "sha1-R1OT/56RR5rqYtyvDKPRSYOn+0A=" }, "ssh2": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-0.8.9.tgz", - "integrity": "sha512-GmoNPxWDMkVpMFa9LVVzQZHF6EW3WKmBwL+4/GeILf2hFmix5Isxm7Amamo8o7bHiU0tC+wXsGcUXOxp8ChPaw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.8.0.tgz", + "integrity": "sha512-NVIRkIwJvWl+mcRozp+EBzHMVCcbDKBea64ToPdZEk43yAVGwmfqYZRPFRnnvGjsKC34wYCmiupTcKgCVNVNNg==", "requires": { - "ssh2-streams": "~0.4.10" - } - }, - "ssh2-streams": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz", - "integrity": "sha512-8pnlMjvnIZJvmTzUIIA5nT4jr2ZWNNVHwyXfMGdRJbug9TpI3kd99ffglgfSWqujVv/0gxwMsDn9j9RVst8yhQ==", - "requires": { - "asn1": "~0.2.0", + "asn1": "^0.2.4", "bcrypt-pbkdf": "^1.0.2", - "streamsearch": "~0.1.2" + "cpu-features": "0.0.3", + "nan": "^2.15.0" } }, "sshpk": { @@ -8983,27 +10707,39 @@ "resolved": "https://registry.npmjs.org/stealthy-require/-/stealthy-require-1.1.1.tgz", "integrity": "sha1-NbCYdbT/SfJqd35QmzCQoyJr8ks=" }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, "streamsearch": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" }, "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + } } }, "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "string.prototype.trimend": { @@ -9025,13 +10761,18 @@ } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } }, + "strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==" + }, "tarn": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz", @@ -9047,11 +10788,6 @@ "resolved": "https://registry.npmjs.org/tildify/-/tildify-2.0.0.tgz", "integrity": "sha512-Cc+OraorugtXNfs50hU9KS369rFXCfgGLpfCfvlc+Ud5u6VWmUQsOAa9HbTvheQdYnrdJqqv1e5oIqXppMYnSw==" }, - "to-array": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" - }, "to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", @@ -9104,6 +10840,11 @@ "punycode": "^2.1.1" } }, + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o=" + }, "traverse": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", @@ -9127,6 +10868,11 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -9136,6 +10882,11 @@ "mime-types": "~2.1.24" } }, + "type-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-1.1.0.tgz", + "integrity": "sha1-rZw/fDMPWy8I3n159W0rlFHkKw4=" + }, "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -9247,6 +10998,28 @@ "version": "3.4.7", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM=" + }, + "readable-stream": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", + "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + } } } }, @@ -9285,9 +11058,9 @@ "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==" }, "url-parse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", - "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", "requires": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" @@ -9316,11 +11089,61 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "utils-copy": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/utils-copy/-/utils-copy-1.1.1.tgz", + "integrity": "sha1-biuXmCqozXPhGCo+b4vsPA9AWKc=", + "requires": { + "const-pinf-float64": "^1.0.0", + "object-keys": "^1.0.9", + "type-name": "^2.0.0", + "utils-copy-error": "^1.0.0", + "utils-indexof": "^1.0.0", + "utils-regex-from-string": "^1.0.0", + "validate.io-array": "^1.0.3", + "validate.io-buffer": "^1.0.1", + "validate.io-nonnegative-integer": "^1.0.0" + }, + "dependencies": { + "type-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/type-name/-/type-name-2.0.2.tgz", + "integrity": "sha1-7+fUEj2KxSr/9/QMfk3sUmYAj7Q=" + } + } + }, + "utils-copy-error": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-copy-error/-/utils-copy-error-1.0.1.tgz", + "integrity": "sha1-eR3jk8DwmJCv1Z88vqY18HmpT6U=", + "requires": { + "object-keys": "^1.0.9", + "utils-copy": "^1.1.0" + } + }, + "utils-indexof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-indexof/-/utils-indexof-1.0.0.tgz", + "integrity": "sha1-IP6r8J7xAYtSNkPoOA57yD7GG1w=", + "requires": { + "validate.io-array-like": "^1.0.1", + "validate.io-integer-primitive": "^1.0.0" + } + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" }, + "utils-regex-from-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/utils-regex-from-string/-/utils-regex-from-string-1.0.0.tgz", + "integrity": "sha1-/hopCfjeD/DVGCyA+8ZU1qaH0Yk=", + "requires": { + "regex-regex": "^1.0.0", + "validate.io-string-primitive": "^1.0.0" + } + }, "uuid": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", @@ -9334,6 +11157,39 @@ "homedir-polyfill": "^1.0.1" } }, + "validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha1-W1osr9j4uFq7L4hroVPy2Tond00=" + }, + "validate.io-array-like": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-array-like/-/validate.io-array-like-1.0.2.tgz", + "integrity": "sha1-evn363tRcVvrIhVmjsXM5U+t21o=", + "requires": { + "const-max-uint32": "^1.0.2", + "validate.io-integer-primitive": "^1.0.0" + } + }, + "validate.io-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-buffer/-/validate.io-buffer-1.0.2.tgz", + "integrity": "sha1-hS1nNAIZFNXROvwyUxdh43IO1E4=" + }, + "validate.io-contains": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-contains/-/validate.io-contains-1.0.0.tgz", + "integrity": "sha1-vwm6TyfGQlB7CQXbs6dKUncInP4=", + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-nan-primitive": "^1.0.0" + } + }, + "validate.io-function": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-function/-/validate.io-function-1.0.2.tgz", + "integrity": "sha1-NDoZgC7TsZaCaceA5VjpNBHAutc=" + }, "validate.io-integer": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/validate.io-integer/-/validate.io-integer-1.0.5.tgz", @@ -9342,11 +11198,64 @@ "validate.io-number": "^1.0.3" } }, + "validate.io-integer-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-integer-primitive/-/validate.io-integer-primitive-1.0.0.tgz", + "integrity": "sha1-qaoBA1X+hoHA/qbBp0rSQZyt3cY=", + "requires": { + "validate.io-number-primitive": "^1.0.0" + } + }, + "validate.io-matrix-like": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/validate.io-matrix-like/-/validate.io-matrix-like-1.0.2.tgz", + "integrity": "sha1-XsMqddCInaxzbepovdYUWxVe38M=" + }, + "validate.io-nan": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/validate.io-nan/-/validate.io-nan-1.0.3.tgz", + "integrity": "sha1-1DjhOGjJy9N/26EllN5Nj+FEMKQ=" + }, + "validate.io-nan-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-nan-primitive/-/validate.io-nan-primitive-1.0.0.tgz", + "integrity": "sha1-R1zC0DXQuvLQCRItg+opGjLX+ww=" + }, + "validate.io-nonnegative-integer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-nonnegative-integer/-/validate.io-nonnegative-integer-1.0.0.tgz", + "integrity": "sha1-gGkkOgjF+Y6VQTySnf17GPP28p8=", + "requires": { + "validate.io-integer": "^1.0.5" + } + }, + "validate.io-nonnegative-integer-array": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/validate.io-nonnegative-integer-array/-/validate.io-nonnegative-integer-array-1.0.1.tgz", + "integrity": "sha1-ZjMKZl9VmLlvJfaQgfgfYy6k208=", + "requires": { + "validate.io-array": "^1.0.3", + "validate.io-nonnegative-integer": "^1.0.0" + } + }, "validate.io-number": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz", "integrity": "sha1-9j/+2iSL8opnqNSODjtGGhZluvg=" }, + "validate.io-number-primitive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/validate.io-number-primitive/-/validate.io-number-primitive-1.0.0.tgz", + "integrity": "sha1-0uAfICmJNp3PEVVElWQgOv5YTlU=" + }, + "validate.io-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/validate.io-object/-/validate.io-object-1.0.4.tgz", + "integrity": "sha1-3KAezu45DhENvCr4Q8gfe/M6Qas=", + "requires": { + "validate.io-array": "^1.0.1" + } + }, "validate.io-positive-integer": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/validate.io-positive-integer/-/validate.io-positive-integer-1.0.0.tgz", @@ -9355,23 +11264,40 @@ "validate.io-integer": "^1.0.5" } }, + "validate.io-string-primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/validate.io-string-primitive/-/validate.io-string-primitive-1.0.1.tgz", + "integrity": "sha1-uBNbn7E3K94C/dU60dDM1t55j+4=" + }, "vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" }, "vasync": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.0.tgz", - "integrity": "sha1-z951GGChWCLbOxMrxZsRakra8Bs=", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/vasync/-/vasync-2.2.1.tgz", + "integrity": "sha512-Hq72JaTpcTFdWiNA4Y22Amej2GH3BFmBaKPPlDZ4/oC8HNn2ISHLkFrJU4Ds8R3jcUi7oo5Y9jcMHKjES+N9wQ==", "requires": { "verror": "1.10.0" + }, + "dependencies": { + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "requires": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + } } }, "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.1.tgz", + "integrity": "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg==", "requires": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", @@ -9379,28 +11305,44 @@ } }, "webdav": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/webdav/-/webdav-3.6.2.tgz", - "integrity": "sha512-HFRiI1jluMSPQMVgxVD6VVYNtaglO53vHG0uf7Zec+wl0A1Mei2z8/IFgAAAJMUuEWAx2AfBD5lcWhAiYA9LUw==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/webdav/-/webdav-4.8.0.tgz", + "integrity": "sha512-CVJvxu0attEfoQUKraDiNh3uMjNPNl+BY0pbcKbyc/X+8IXDnqAT4tT4Ge12w+j49fYuVpFVkpEGwBZabv7Uhw==", "requires": { - "axios": "^0.21.1", - "base-64": "^0.1.0", - "fast-xml-parser": "^3.17.4", + "axios": "^0.24.0", + "base-64": "^1.0.0", + "fast-xml-parser": "^3.19.0", "he": "^1.2.0", "hot-patcher": "^0.5.0", + "layerr": "^0.1.2", + "md5": "^2.3.0", "minimatch": "^3.0.4", "nested-property": "^4.0.0", "path-posix": "^1.0.0", "url-join": "^4.0.1", - "url-parse": "^1.4.7" + "url-parse": "^1.5.3" } }, "webdav-fs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/webdav-fs/-/webdav-fs-3.0.0.tgz", - "integrity": "sha512-/rwiAPiPymjHXSgobrze/jMIvap+9cF75A7iKvUCUDrU58MIEEp4tsBtt4gQlOQWHkqAeMbcUBrW6xMNf8JYhg==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/webdav-fs/-/webdav-fs-4.0.1.tgz", + "integrity": "sha512-2W3UCjvFQ+V8vHFiRPuM/TR8cPjhvTeFq3wG/xZKJgjxhnh4trJvxoG9RczUWBPFtkVJD/rDuuCwtD8jNmYtdw==", "requires": { - "webdav": "^3.4.0" + "webdav": "^4.7.0" + } + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE=" + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha1-lmRU6HZUYuN2RNNib2dCzotwll0=", + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, "which": { @@ -9444,9 +11386,9 @@ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==", + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", "requires": {} }, "xml2js": { @@ -9464,9 +11406,14 @@ "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" }, "xmlhttprequest-ssl": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.3.tgz", - "integrity": "sha512-3XfeQE/wNkvrIktn2Kf0869fC0BN6UpydVasGIeSm2B1Llihf7/0UfZM+eCkOw3P7bP4+qPgqhm7ZoxuJtFU0Q==" + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "4.0.3", diff --git a/package.json b/package.json index d830627d..6e60fb1a 100644 --- a/package.json +++ b/package.json @@ -18,40 +18,46 @@ "aws-sdk": "^2.731.0", "backblaze-b2": "^0.9.12", "body-parser": "^1.19.0", + "bson": "^4.6.1", "connection-tester": "^0.2.0", - "cws": "^1.2.11", + "cws": "^2.0.0", + "digest-fetch": "^1.2.1", "discord.js": "^12.2.0", - "ejs": "^2.5.5", + "ejs": "^2.7.4", "express": "^4.16.4", - "express-fileupload": "^1.1.6-alpha.6", + "express-fileupload": "^1.4.0", "fs-extra": "9.0.1", "ftp-srv": "^4.4.0", - "googleapis": "^71.0.0", + "googleapis": "^100.0.0", "http-proxy": "^1.17.0", "jsonfile": "^3.0.1", "knex": "^0.21.4", - "ldapauth-fork": "^5.0.1", - "moment": "^2.27.0", + "ldapauth-fork": "^5.0.2", + "moment": "^2.29.4", "mp4frag": "^0.2.0", + "mqtt": "^4.3.7", "mysql": "^2.18.1", - "node-fetch": "3.0.0-beta.9", - "node-ssh": "^11.1.1", - "node-telegram-bot-api": "^0.52.0", + "node-abort-controller": "^3.0.1", + "node-fetch": "^2.6.7", + "node-onvif-events": "^2.0.5", + "node-ssh": "^12.0.4", + "node-telegram-bot-api": "^0.58.0", "nodemailer": "^6.4.11", - "node-pushover": "^1.0.0", - "pam-diff": "^1.0.0", + "pam-diff": "^1.1.0", "path": "^0.12.7", "pipe2pam": "^0.6.2", - "request": "^2.88.0", + "pixel-change": "^1.1.0", + "pushover-notifications": "^1.2.2", "sat": "^0.7.1", "shinobi-onvif": "0.1.9", - "shinobi-sound-detection": "^0.1.8", + "shinobi-sound-detection": "^0.1.13", + "shinobi-zwave": "^1.0.11", "smtp-server": "^3.5.0", - "socket.io": "^2.3.0", - "socket.io-client": "^2.3.0", + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1", "tree-kill": "1.2.2", "unzipper": "0.10.11", - "webdav-fs": "^3.0.0" + "webdav-fs": "^4.0.1" }, "bin": "camera.js", "scripts": { @@ -66,12 +72,12 @@ }, "pkg": { "targets": [ - "node12" + "node16" ], "scripts": [ - "libs/cameraThread/detector.js", - "libs/cameraThread/singleCamera.js", - "libs/cameraThread/snapshot.js" + "libs/cameraThread/detector.js", + "libs/cameraThread/singleCamera.js", + "libs/cameraThread/snapshot.js" ], "assets": [ "definitions/**/*", diff --git a/plugins/deepstack-object/package.json b/plugins/deepstack-object/package.json index a9d413c4..09fb62b3 100644 --- a/plugins/deepstack-object/package.json +++ b/plugins/deepstack-object/package.json @@ -8,8 +8,8 @@ "request": "^2.88.0", "express": "^4.16.2", "moment": "^2.19.2", - "socket.io": "^2.3.0", - "socket.io-client": "^2.3.0" + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1" }, "devDependencies": {}, "bin": "shinobi-deepstack-object.js", diff --git a/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/js/super.faceManager.js b/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/js/super.faceManager.js index a25c064f..4e6d84d9 100644 --- a/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/js/super.faceManager.js +++ b/plugins/face/faceManagerCustomAutoLoadLibrary/web/libs/js/super.faceManager.js @@ -28,7 +28,7 @@ $(document).ready(function(){
- +
${name} diff --git a/plugins/face/package.json b/plugins/face/package.json index 8dd8024c..395de9cd 100644 --- a/plugins/face/package.json +++ b/plugins/face/package.json @@ -11,7 +11,7 @@ "socket.io-client": "^1.7.4", "express": "^4.16.2", "moment": "^2.19.2", - "socket.io": "^2.0.4", + "socket.io": "^4.4.1", "face-api.js": "^0.22.2", "canvas": "^2.1.0", "dotenv": "^8.2.0" diff --git a/plugins/openalpr/package.json b/plugins/openalpr/package.json index 2547595c..f353b006 100644 --- a/plugins/openalpr/package.json +++ b/plugins/openalpr/package.json @@ -6,7 +6,7 @@ "dependencies": { "express": "^4.16.2", "moment": "^2.19.2", - "socket.io": "^2.0.4", + "socket.io": "^4.4.1", "node-openalpr-shinobi": "1.1.4" }, "devDependencies": {}, diff --git a/plugins/platerecognizer/package.json b/plugins/platerecognizer/package.json index 4b5298d2..faeb6e39 100644 --- a/plugins/platerecognizer/package.json +++ b/plugins/platerecognizer/package.json @@ -8,8 +8,8 @@ "request": "^2.88.0", "express": "^4.16.2", "moment": "^2.19.2", - "socket.io": "^2.3.0", - "socket.io-client": "^2.3.0" + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1" }, "devDependencies": {}, "bin": "shinobi-platerecognizer.js", diff --git a/plugins/pluginBase.js b/plugins/pluginBase.js index 800b45cd..b027529c 100644 --- a/plugins/pluginBase.js +++ b/plugins/pluginBase.js @@ -304,7 +304,7 @@ module.exports = function(__dirname, config){ if(config.mode === 'host'){ plugLog('Plugin started as Host') //start plugin as host - var io = require('socket.io')(server,{ + const io = new (require('socket.io').Server)(server,{ transports: ['websocket'] }) io.engine.ws = new (require('cws').Server)({ diff --git a/plugins/tensorflow-coral/package.json b/plugins/tensorflow-coral/package.json index ccb28c80..e02d5be4 100644 --- a/plugins/tensorflow-coral/package.json +++ b/plugins/tensorflow-coral/package.json @@ -8,8 +8,8 @@ "dotenv": "^8.2.0", "express": "^4.16.2", "moment": "^2.19.2", - "socket.io": "^2.0.4", - "socket.io-client": "^1.7.4" + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1" }, "devDependencies": {}, "bin": "shinobi-tensorflow-coral.js", diff --git a/plugins/tensorflow/INSTALL-2-3-0.sh b/plugins/tensorflow/INSTALL-2-3-0.sh index 4b3f7ac3..4761e85c 100644 --- a/plugins/tensorflow/INSTALL-2-3-0.sh +++ b/plugins/tensorflow/INSTALL-2-3-0.sh @@ -1,171 +1,52 @@ -#!/bin/bash -echo "ARM CPU Installation is currently NOT supported! Jetson Nano with GPU enabled is currently only supported." -echo "Jetson Nano may experience \"Unsupported Errors\", you may ignore them. Patches will be applied." -if [[ ! $(head -1 /etc/nv_tegra_release) =~ R32.*4\.[34] ]] ; then - echo "ERROR: not JetPack-4.4" - exit 1 -fi +DIR=$(dirname "${0}") -cudaCompute=$(cat /sys/module/tegra_fuse/parameters/tegra_chip_id) -# 33 : Nano, TX1 -# 24 : TX2 -# 25 : Xavier NX and AGX Xavier - -DIR=$(dirname $0) -echo $DIR echo "Replacing package.json for tfjs 2.3.0..." -wget -O $DIR/package.json https://cdn.shinobi.video/binaries/tensorflow/2.3.0/package.json +cp "${DIR}/package-jetson.json" "${DIR}/package.json" + echo "Removing existing Tensorflow Node.js modules..." -rm -rf $DIR/node_modules +rm -rf "${DIR}/node_modules" + +echo "Installing Yarn package manager" npm install yarn -g --unsafe-perm --force -installJetsonFlag=false -installArmFlag=false -installGpuFlag=false -dontCreateKeyFlag=false +[ -d "${DIR}/tfjs-tfjs-v2.3.0" ] && echo "Removing existing Tensorflow source directory" && rm -rf "${DIR}/tfjs-tfjs-v2.3.0" -while [ ! $# -eq 0 ] -do - case "$1" in - --jetson) - installJetsonFlag=true - exit - ;; - --arm) - installArmFlag=true - exit - ;; - --gpu) - installGpuFlag=true - exit - ;; - --dont-create-key) - dontCreateKeyFlag=true - exit - ;; - esac - shift -done +echo "Downloading Tensorflow source tarball" +wget -O "${DIR}/tfjs-v2.3.0.tar.gz" https://github.com/tensorflow/tfjs/archive/refs/tags/tfjs-v2.3.0.tar.gz -if [ "$installJetsonFlag" = true ] && [ "$installArmFlag" = true ]; then - echo "--jetson and --arm cannot both be set. Exiting..." - exit -1 -fi +echo "Extracting and preparing Tensorflow source" +tar -xf tfjs-v2.3.0.tar.gz -C "${DIR}" +(cd "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu" || exit ; ./prep-gpu.sh) -if ([ "$installJetsonFlag" = true ] || [ "$installArmFlag" = true ]) && [ "$installGpuFlag" = true ]; then - echo "--gpu flag cannot be set with --jetson or --arm. Exiting..." - exit -2 -fi +echo "Building Tensorflow Node GPU package" +(cd "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu" || exit ; yarn && yarn build) -nonInteractiveFlag=false -if [ "$installJetsonFlag" = true ] || [ "$installArmFlag" = true ] || [ "$installGpuFlag" = true ]; then - nonInteractiveFlag=true -fi -manualInstallRequirements() { - npm install --unsafe-perm - npm install @tensorflow/tfjs-backend-cpu@2.3.0 @tensorflow/tfjs-backend-webgl@2.3.0 @tensorflow/tfjs-converter@2.3.0 @tensorflow/tfjs-core@2.3.0 @tensorflow/tfjs-layers@2.3.0 @tensorflow/tfjs-node@2.3.0 --unsafe-perm -} -runRebuildCpu() { - npm rebuild @tensorflow/tfjs-node --build-addon-from-source --unsafe-perm -} +echo "Removing Tensorflow source tarball" +rm -f "${DIR}/tfjs-v2.3.0.tar.gz" -runRebuildGpu() { - npm rebuild @tensorflow/tfjs-node-gpu --build-addon-from-source --unsafe-perm -} +echo "Installing Tensorflow addon" +yarn -installJetson() { - installGpuFlag=true - npm install @tensorflow/tfjs-node-gpu@2.3.0 --unsafe-perm - customBinaryLocation="node_modules/@tensorflow/tfjs-node-gpu/scripts/custom-binary.json" - case cudaCompute in - "33" ) # Nano and TX1 - echo '{"tf-lib": "https://cdn.shinobi.video/binaries/tensorflow/2.3.0/libtensorflow.tar.gz"}' > "$customBinaryLocation" - ;; - "25" ) # Xavier NX and AGX Xavier - echo '{"tf-lib": "https://cdn.shinobi.video/binaries/tensorflow/2.3.0-xavier/libtensorflow.tar.gz"}' > "$customBinaryLocation" - ;; - * ) # default - echo '{"tf-lib": "https://cdn.shinobi.video/binaries/tensorflow/2.3.0/libtensorflow.tar.gz"}' > "$customBinaryLocation" - ;; - esac - manualInstallRequirements - chmod -R 777 . - runRebuildGpu -} +echo "Clean Yarn cache" +yarn cache clean --all -installGpuRoute() { - installGpuFlag=true - manualInstallRequirements - npm install @tensorflow/tfjs-node-gpu@2.3.0 --unsafe-perm -} - -installNonGpuRoute() { - manualInstallRequirements - npm install @tensorflow/tfjs-node@2.3.0 --unsafe-perm - runRebuildCpu -} - - -if [ "$nonInteractiveFlag" = false ]; then - echo "Shinobi - Are you installing on Jetson Nano or Xavier?" - echo "You must be on JetPack 4.4 for this plugin to install!" - echo "(y)es or (N)o" - read armCpu - if [ "$armCpu" = "y" ] || [ "$armCpu" = "Y" ]; then - # echo "Shinobi - Is it a Jetson Nano?" - # echo "You must be on JetPack 4.4 for this plugin to install!" - # echo "(y)es or (N)o" - # read isItJetsonNano - # echo "Shinobi - You may see Unsupported Errors, please wait while patches are applied." - # if [ "$isItJetsonNano" = "y" ] || [ "$isItJetsonNano" = "Y" ]; then - installJetson - # else - # installArm - # fi - else - echo "Shinobi - Do you want to install TensorFlow.js with GPU support? " - echo "You can run this installer again to change it." - echo "(y)es or (N)o" - read nodejsinstall - if [ "$nodejsinstall" = "y" ] || [ "$nodejsinstall" = "Y" ]; then - installGpuRoute - else - installNonGpuRoute - fi - fi -else - if [ "$installJetsonFlag" = true ]; then - installJetson - fi - # - # if [ "$installArmFlag" = true ]; then - # installArm - # fi - - if [ "$installGpuFlag" = true ]; then - installGpuRoute - else - installNonGpuRoute - fi -fi # # npm audit fix --force -if [ ! -e "$DIR/conf.json" ]; then - dontCreateKeyFlag=false +if [ ! -e "${DIR}/conf.json" ]; then echo "Creating conf.json" - sudo cp $DIR/conf.sample.json $DIR/conf.json + sudo cp "${DIR}/conf.sample.json" "${DIR}/conf.json" else echo "conf.json already exists..." fi -if [ "$dontCreateKeyFlag" = false ]; then - tfjsBuildVal="cpu" - if [ "$installGpuFlag" = true ]; then - tfjsBuildVal="gpu" - fi +tfjsBuildVal="gpu" - echo "Adding Random Plugin Key to Main Configuration" - node $DIR/../../tools/modifyConfigurationForPlugin.js tensorflow key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}') tfjsBuild=$tfjsBuildVal -fi +echo "Adding Random Plugin Key to Main Configuration" +node "${DIR}/../../tools/modifyConfigurationForPlugin.js" tensorflow key="$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}')" tfjsBuild="${tfjsBuildVal}" -echo "TF_FORCE_GPU_ALLOW_GROWTH=true" > "$DIR/.env" -echo "#CUDA_VISIBLE_DEVICES=0,2" >> "$DIR/.env" +echo "TF_FORCE_GPU_ALLOW_GROWTH=true" > "${DIR}/.env" +echo "#CUDA_VISIBLE_DEVICES=0,2" >> "${DIR}/.env" + +echo "Instalation finished" +echo "For plugin automatic start run: pm2 start shinobi-tensorflow.js && pm2 save" +echo "Or run manualy: node shinobi-tensorflow.js" +echo "To make Shinobi avare of new plugin, restart it by: pm2 restart camera" diff --git a/plugins/tensorflow/INSTALL-jetson.sh b/plugins/tensorflow/INSTALL-jetson.sh index fbb65627..6c2867d3 100644 --- a/plugins/tensorflow/INSTALL-jetson.sh +++ b/plugins/tensorflow/INSTALL-jetson.sh @@ -1,37 +1,56 @@ #!/bin/bash echo "ARM CPU Installation is currently NOT supported! Jetson Nano with GPU enabled is currently only supported." -echo "Jetson Nano may experience \"Unsupported Errors\", you may ignore them. Patches will be applied." if [[ ! $(head -1 /etc/nv_tegra_release) =~ R32.*4\.[34] ]] ; then echo "ERROR: not JetPack-4.4" exit 1 fi cudaCompute=$(cat /sys/module/tegra_fuse/parameters/tegra_chip_id) +export cudaCompute # 33 : Nano, TX1 # 24 : TX2 # 25 : Xavier NX and AGX Xavier -DIR=$(dirname $0) -echo $DIR +DIR=$(dirname "${0}") + echo "Replacing package.json for tfjs 2.3.0..." -wget -O $DIR/package.json https://cdn.shinobi.video/binaries/tensorflow/2.3.0/package.json +cp "${DIR}/package-jetson.json" "${DIR}/package.json" + echo "Removing existing Tensorflow Node.js modules..." -rm -rf $DIR/node_modules +rm -rf "${DIR}/node_modules" + +echo "Installing Yarn package manager" npm install yarn -g --unsafe-perm --force -npm install dotenv +[ -d "${DIR}/tfjs-tfjs-v2.3.0" ] && echo "Removing existing Tensorflow source directory" && rm -rf "${DIR}/tfjs-tfjs-v2.3.0" -npm install @tensorflow/tfjs-backend-cpu@2.3.0 @tensorflow/tfjs-backend-webgl@2.3.0 @tensorflow/tfjs-converter@2.3.0 @tensorflow/tfjs-core@2.3.0 @tensorflow/tfjs-layers@2.3.0 @tensorflow/tfjs-node@2.3.0 --unsafe-perm --force --legacy-peer-deps -npm install @tensorflow/tfjs-node-gpu@2.3.0 --unsafe-perm -customBinaryLocation="node_modules/@tensorflow/tfjs-node-gpu/scripts/custom-binary.json" -echo '{"tf-lib": "https://cdn.shinobi.video/binaries/tensorflow/2.3.0/libtensorflow.tar.gz"}' > "$customBinaryLocation" -npm rebuild @tensorflow/tfjs-node-gpu --build-addon-from-source --unsafe-perm +echo "Downloading Tensorflow source tarball" +wget -O "${DIR}/tfjs-v2.3.0.tar.gz" https://github.com/tensorflow/tfjs/archive/refs/tags/tfjs-v2.3.0.tar.gz +echo "Extracting and preparing Tensorflow source" +tar -xf tfjs-v2.3.0.tar.gz -C "${DIR}" +(cd "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu" || exit ; ./prep-gpu.sh) + +# Little hack to make it run on jetson and download precompiled binaries +sed -i "s/os.arch() === 'arm'/os.arch() === 'arm64'/ig" "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu/scripts/install.js" +sed -i "s/https:\/\/storage.googleapis.com\/tf-builds\/libtensorflow_r1_14_linux_arm.tar.gz/https:\/\/cdn.shinobi.video\/binaries\/tensorflow\/2.3.0\/libtensorflow.tar.gz/ig" "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu/scripts/install.js" + +echo "Building Tensorflow Node GPU package" +(cd "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu" || exit ; yarn && yarn build) + +echo "Removing Tensorflow source tarball" +rm -f "${DIR}/tfjs-v2.3.0.tar.gz" + +echo "Installing Tensorflow addon" +yarn + +echo "Clean Yarn cache" +yarn cache clean --all # # npm audit fix --force -if [ ! -e "$DIR/conf.json" ]; then +if [ ! -e "${DIR}/conf.json" ]; then echo "Creating conf.json" - sudo cp $DIR/conf.sample.json $DIR/conf.json + sudo cp "${DIR}/conf.sample.json" "${DIR}/conf.json" else echo "conf.json already exists..." fi @@ -39,7 +58,12 @@ fi tfjsBuildVal="gpu" echo "Adding Random Plugin Key to Main Configuration" -node $DIR/../../tools/modifyConfigurationForPlugin.js tensorflow key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}') tfjsBuild=$tfjsBuildVal +node "${DIR}/../../tools/modifyConfigurationForPlugin.js" tensorflow key="$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}')" tfjsBuild="${tfjsBuildVal}" -echo "TF_FORCE_GPU_ALLOW_GROWTH=true" > "$DIR/.env" -echo "#CUDA_VISIBLE_DEVICES=0,2" >> "$DIR/.env" +echo "TF_FORCE_GPU_ALLOW_GROWTH=true" > "${DIR}/.env" +echo "#CUDA_VISIBLE_DEVICES=0,2" >> "${DIR}/.env" + +echo "Instalation finished" +echo "For plugin automatic start run: pm2 start shinobi-tensorflow.js && pm2 save" +echo "Or run manualy: node shinobi-tensorflow.js" +echo "To make Shinobi avare of new plugin, restart it by: pm2 restart camera" diff --git a/plugins/tensorflow/package-jetson.json b/plugins/tensorflow/package-jetson.json new file mode 100644 index 00000000..e9288b63 --- /dev/null +++ b/plugins/tensorflow/package-jetson.json @@ -0,0 +1,47 @@ +{ + "name": "shinobi-tensorflow", + "author": "Shinob Systems, Moinul Alam", + "version": "1.0.5", + "description": "Object Detection plugin based on @tensorflow/tfjs-node", + "main": "shinobi-tensorflow.js", + "dependencies": { + "@tensorflow-models/coco-ssd": "2.2.1", + "@tensorflow/tfjs": "2.3.0", + "@tensorflow/tfjs-backend-cpu": "2.3.0", + "@tensorflow/tfjs-backend-webgl": "2.3.0", + "@tensorflow/tfjs-converter": "2.3.0", + "@tensorflow/tfjs-core": "2.3.0", + "@tensorflow/tfjs-layers": "2.3.0", + "@tensorflow/tfjs-node-gpu": "file:tfjs-tfjs-v2.3.0/tfjs-node-gpu", + "alea": "^1.0.1", + "dotenv": "^8.6.0", + "express": "4.16.2", + "moment": "2.19.2", + "random-seedable": "^1.0.8", + "seedrandom": "2.4.3", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1", + "xor128": "^0.1.0" + }, + "bin": "shinobi-tensorflow.js", + "scripts": { + "package": "pkg package.json -t linux,macos,win --out-path dist", + "package-x64": "pkg package.json -t linux-x64,macos-x64,win-x64 --out-path dist/x64", + "package-x86": "pkg package.json -t linux-x86,macos-x86,win-x86 --out-path dist/x86", + "package-armv6": "pkg package.json -t linux-armv6,macos-armv6,win-armv6 --out-path dist/armv6", + "package-armv7": "pkg package.json -t linux-armv7,macos-armv7,win-armv7 --out-path dist/armv7", + "package-arm64": "pkg package.json -t linux-arm64,macos-arm64,win-arm64 --out-path dist/arm64", + "package-all": "npm run package && npm run package-x64 && npm run package-x86 && npm run package-armv6 && npm run package-armv7 && npm run package-arm64" + }, + "pkg": { + "targets": [ + "node16" + ], + "scripts": [ + "../pluginBase.js" + ], + "assets": [] + }, + "disabled": false +} diff --git a/plugins/tensorflow/package.json b/plugins/tensorflow/package.json index 61ce40dd..29e6160b 100644 --- a/plugins/tensorflow/package.json +++ b/plugins/tensorflow/package.json @@ -5,17 +5,20 @@ "description": "Object Detection plugin based on @tensorflow/tfjs-node", "main": "shinobi-tensorflow.js", "dependencies": { - "@tensorflow-models/coco-ssd": "^2.1.0", - "@tensorflow/tfjs-converter": "^2.7.0", - "@tensorflow/tfjs-core": "^2.7.0", - "@tensorflow/tfjs-layers": "^2.7.0", - "@tensorflow/tfjs-node": "^2.7.0", - "@tensorflow/tfjs-node-gpu": "^2.7.0", + "@tensorflow-models/coco-ssd": "2.2.1", + "@tensorflow/tfjs": "2.3.0", + "@tensorflow/tfjs-backend-cpu": "2.3.0", + "@tensorflow/tfjs-backend-webgl": "2.3.0", + "@tensorflow/tfjs-converter": "2.3.0", + "@tensorflow/tfjs-core": "2.3.0", + "@tensorflow/tfjs-layers": "2.3.0", + "@tensorflow/tfjs-node-gpu": "file:tfjs-tfjs-v2.3.0/tfjs-node-gpu", + "moment": "^2.29.1", "dotenv": "^8.2.0", + "cws": "^2.0.0", "express": "^4.16.2", - "moment": "^2.19.2", - "socket.io": "^2.3.0", - "socket.io-client": "^2.3.0" + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1" }, "devDependencies": {}, "bin": "shinobi-tensorflow.js", diff --git a/plugins/yolo/package.json b/plugins/yolo/package.json index 382d3e97..ea8d11ae 100644 --- a/plugins/yolo/package.json +++ b/plugins/yolo/package.json @@ -4,11 +4,13 @@ "description": "YoloV3 plugin for Shinobi that uses C++ functions for detection.", "main": "shinobi-yolo.js", "dependencies": { - "socket.io-client": "^1.7.4", - "express": "^4.16.2", - "moment": "^2.19.2", - "socket.io": "^2.0.4", "imagickal": "^4.0.0", + "moment": "^2.29.1", + "dotenv": "^8.2.0", + "cws": "^2.0.0", + "express": "^4.16.4", + "socket.io": "^4.4.1", + "socket.io-client": "^4.4.1", "node-yolo-shinobi": "^2.0.3" }, "devDependencies": {}, diff --git a/sql/postgresql/framework.pgsql b/sql/postgresql/framework.pgsql index ee79e698..686396bd 100644 --- a/sql/postgresql/framework.pgsql +++ b/sql/postgresql/framework.pgsql @@ -62,6 +62,16 @@ CREATE TRIGGER update_api_modtime BEFORE UPDATE ON "API" FOR EACH ROW EXECUTE PROCEDURE update_time_column(); +CREATE TABLE IF NOT EXISTS "Cloud Timelapse Frames" ( + "ke" varchar(50) NOT NULL, + "mid" varchar(50) NOT NULL, + "href" text NOT NULL, + "details" text, + "filename" varchar(50) NOT NULL, + "time" timestamp DEFAULT NULL, + "size" int NOT NULL +) ; + CREATE TABLE IF NOT EXISTS "Cloud Videos" ( "mid" varchar(50) NOT NULL, "ke" varchar(50) DEFAULT NULL, @@ -72,7 +82,6 @@ CREATE TABLE IF NOT EXISTS "Cloud Videos" ( "status" int DEFAULT '0', "details" text ) ; - /* For status above: COMMENT '0:Complete,1:Read,2:Archive' */ CREATE TABLE IF NOT EXISTS "Events" ( @@ -81,11 +90,49 @@ CREATE TABLE IF NOT EXISTS "Events" ( "details" text, "time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ) ; - CREATE TRIGGER update_events_modtime BEFORE UPDATE ON "Events" FOR EACH ROW EXECUTE PROCEDURE update_time_column(); +CREATE TABLE IF NOT EXISTS "Events Counts" ( + "ke" varchar(50) NOT NULL, + "mid" varchar(50) NOT NULL, + "details" text NOT NULL, + "time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "end" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + "count" int NOT NULL DEFAULT 1, + "tag" varchar(30) DEFAULT NULL +) ; +CREATE TRIGGER update_events_counts_modtime +BEFORE UPDATE ON "Events Counts" +FOR EACH ROW EXECUTE PROCEDURE upd_end_column(); + +CREATE TABLE IF NOT EXISTS "Files" ( + "ke" varchar(50) NOT NULL, + "mid" varchar(50) NOT NULL, + "name" text NOT NULL, + "size" float NOT NULL DEFAULT '0', + "details" text NOT NULL, + "status" int NOT NULL DEFAULT '0', + "time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP +) ; +CREATE TRIGGER update_files_modtime +BEFORE UPDATE ON "Files" +FOR EACH ROW EXECUTE PROCEDURE update_time_column(); + +CREATE TABLE IF NOT EXISTS "LoginTokens" ( + "loginId" varchar(255) DEFAULT '', + "type" varchar(25) DEFAULT '', + "ke" varchar(50) DEFAULT '', + "uid" varchar(50) DEFAULT '', + "name" varchar(50) DEFAULT 'Unknown', + "lastLogin" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE ("loginId") +) ; +CREATE TRIGGER update_logintokens_modtime +BEFORE UPDATE ON "LoginTokens" +FOR EACH ROW EXECUTE PROCEDURE update_time_column(); + CREATE TABLE IF NOT EXISTS "Logs" ( "ke" varchar(50) DEFAULT NULL, "mid" varchar(50) DEFAULT NULL, @@ -122,44 +169,6 @@ CREATE TABLE IF NOT EXISTS "Presets" ( "type" varchar(50) DEFAULT NULL ) ; -CREATE TABLE IF NOT EXISTS "Users" ( - "ke" varchar(50) DEFAULT NULL, - "uid" varchar(50) DEFAULT NULL, - "auth" varchar(50) DEFAULT NULL, - "mail" varchar(100) DEFAULT NULL, - "pass" varchar(100) DEFAULT NULL, - "details" text, - UNIQUE ("mail") -) ; - -CREATE TYPE vidtype AS ENUM('webm','mp4','null'); -CREATE TABLE IF NOT EXISTS "Videos" ( - "mid" varchar(50) DEFAULT NULL, - "ke" varchar(50) DEFAULT NULL, - "ext" vidtype DEFAULT NULL, - "time" timestamp NULL DEFAULT NULL, - "duration" float DEFAULT NULL, - "size" float DEFAULT NULL, - "frames" int DEFAULT NULL, - "end" timestamp NULL DEFAULT NULL, - "status" int DEFAULT '0', - "details" text -) ; -/* For status above, COMMENT '0:Building,1:Complete,2:Read,3:Archive' */ - -CREATE TABLE IF NOT EXISTS "Files" ( - "ke" varchar(50) NOT NULL, - "mid" varchar(50) NOT NULL, - "name" text NOT NULL, - "size" float NOT NULL DEFAULT '0', - "details" text NOT NULL, - "status" int NOT NULL DEFAULT '0', - "time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ; -CREATE TRIGGER update_files_modtime -BEFORE UPDATE ON "Files" -FOR EACH ROW EXECUTE PROCEDURE update_time_column(); - CREATE TABLE IF NOT EXISTS "Schedules" ( "ke" varchar(50) DEFAULT NULL, "name" text, @@ -169,6 +178,15 @@ CREATE TABLE IF NOT EXISTS "Schedules" ( "enabled" int NOT NULL DEFAULT '1' ) ; +CREATE TABLE IF NOT EXISTS "Timelapse Frames" ( + "ke" varchar(50) NOT NULL, + "mid" varchar(50) NOT NULL, + "details" text, + "filename" varchar(50) NOT NULL, + "time" timestamp NULL DEFAULT NULL, + "size" int NOT NULL +) ; + CREATE TABLE IF NOT EXISTS "Timelapses" ( "ke" varchar(50) NOT NULL, "mid" varchar(50) NOT NULL, @@ -182,37 +200,29 @@ CREATE TRIGGER update_timelapses_modtime BEFORE UPDATE ON "Timelapses" FOR EACH ROW EXECUTE PROCEDURE upd_end_column(); -CREATE TABLE IF NOT EXISTS "Timelapse Frames" ( - "ke" varchar(50) NOT NULL, - "mid" varchar(50) NOT NULL, +CREATE TABLE IF NOT EXISTS "Users" ( + "ke" varchar(50) DEFAULT NULL, + "uid" varchar(50) DEFAULT NULL, + "auth" varchar(50) DEFAULT NULL, + "mail" varchar(100) DEFAULT NULL, + "pass" varchar(100) DEFAULT NULL, + "accountType" int DEFAULT '0', "details" text, - "filename" varchar(50) NOT NULL, + UNIQUE ("mail") +) ; + +CREATE TYPE vidtype AS ENUM('webm','mp4','null'); +CREATE TABLE IF NOT EXISTS "Videos" ( + "mid" varchar(50) DEFAULT NULL, + "ke" varchar(50) DEFAULT NULL, + "ext" vidtype DEFAULT NULL, "time" timestamp NULL DEFAULT NULL, - "size" int NOT NULL + "duration" float DEFAULT NULL, + "size" float DEFAULT NULL, + "frames" int DEFAULT NULL, + "end" timestamp NULL DEFAULT NULL, + "status" int DEFAULT '0', + "archived" int DEFAULT '0', + "details" text ) ; - -CREATE TABLE IF NOT EXISTS "Cloud Timelapse Frames" ( - "ke" varchar(50) NOT NULL, - "mid" varchar(50) NOT NULL, - "href" text NOT NULL, - "details" text, - "filename" varchar(50) NOT NULL, - "time" timestamp DEFAULT NULL, - "size" int NOT NULL -) ; - -CREATE TABLE IF NOT EXISTS "Events Counts" ( - "ke" varchar(50) NOT NULL, - "mid" varchar(50) NOT NULL, - "details" text NOT NULL, - "time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - "end" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - "count" int NOT NULL DEFAULT 1, - "tag" varchar(30) DEFAULT NULL -) ; -CREATE TRIGGER update_events_counts_modtime -BEFORE UPDATE ON "Events Counts" -FOR EACH ROW EXECUTE PROCEDURE upd_end_column(); - - - +/* For status above, COMMENT '0:Building,1:Complete,2:Read,3:Archive' */ diff --git a/test/testEventBasedRecordingModule.js b/test/testEventBasedRecordingModule.js new file mode 100644 index 00000000..7c20b1c6 --- /dev/null +++ b/test/testEventBasedRecordingModule.js @@ -0,0 +1,29 @@ +module.exports = function(s,config,lang,getSnapshot){ + const { + getEventBasedRecordingUponCompletion, + } = require('../events/utils.js')(s,config,lang) + const onEventTrigger = async (d,filter) => { + console.log('CUSTOM COMMAND ON EVENT eventBasedRecording') + const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id] + let videoPath = null + let videoName = null + console.log('await eventBasedRecording') + const eventBasedRecording = await getEventBasedRecordingUponCompletion({ + ke: d.ke, + mid: d.mid + }) + console.log('complete eventBasedRecording') + console.log(eventBasedRecording) + if(eventBasedRecording.filePath){ + videoPath = eventBasedRecording.filePath + videoName = eventBasedRecording.filename + }else{ + const siftedVideoFileFromRam = await s.mergeDetectorBufferChunks(d) + console.log('siftedVideoFileFromRam') + console.log(siftedVideoFileFromRam) + videoPath = siftedVideoFileFromRam.filePath + videoName = siftedVideoFileFromRam.filename + } + } + s.onEventTrigger(onEventTrigger) +} diff --git a/tools/convertDefinitionsFieldInfosToLangParam.js b/tools/convertDefinitionsFieldInfosToLangParam.js index 1254b070..63577130 100644 --- a/tools/convertDefinitionsFieldInfosToLangParam.js +++ b/tools/convertDefinitionsFieldInfosToLangParam.js @@ -1,13 +1,173 @@ +const fs = require('fs') const definitionFile = process.cwd() + '/definitions/en_CA.js' +const newDefinitionFile = process.cwd() + '/tools/en_CA.js' const languagesFile = process.cwd() + '/languages/en_CA.json' +const newLanguagesFile = process.cwd() + '/tools/en_CA.json' const languagesData = require(languagesFile) +var definitonsRawData = fs.readFileSync(definitionFile).toString() const definitionData = require(definitionFile)({ gid: () => {return 'randomId'}, listOfStorage: [], }, - {}, + { + timeZones: [ + { + "text": "UTC−12:00, Y", + "value": -720 + }, + { + "text": "UTC−11:00, X", + "value": -660 + }, + { + "text": "UTC−10:00, W", + "value": -600 + }, + { + "text": "UTC−09:30, V†", + "value": -570 + }, + { + "text": "UTC−09:00, V", + "value": -540 + }, + { + "text": "UTC−08:00, U", + "value": -480 + }, + { + "text": "UTC−07:00, T", + "value": -420 + }, + { + "text": "UTC−06:00, S", + "value": -360 + }, + { + "text": "UTC−05:00, R", + "value": -300 + }, + { + "text": "UTC−04:00, Q", + "value": -240 + }, + { + "text": "UTC−03:30, P†", + "value": -210 + }, + { + "text": "UTC−03:00, P", + "value": -180 + }, + { + "text": "UTC−02:00, O", + "value": -120 + }, + { + "text": "UTC−01:00, N", + "value": -60 + }, + { + "text": "UTC±00:00, Z", + "value": 0, + "selected": true + }, + { + "text": "UTC+01:00, A", + "value": 60 + }, + { + "text": "UTC+02:00, B", + "value": 120 + }, + { + "text": "UTC+03:00, C", + "value": 180 + }, + { + "text": "UTC+03:30, C†", + "value": 210 + }, + { + "text": "UTC+04:00, D", + "value": 240 + }, + { + "text": "UTC+04:30, D†", + "value": 270 + }, + { + "text": "UTC+05:00, E", + "value": 300 + }, + { + "text": "UTC+05:30, E†", + "value": 330 + }, + { + "text": "UTC+05:45, E*", + "value": 345 + }, + { + "text": "UTC+06:00, F", + "value": 360 + }, + { + "text": "UTC+06:30, F†", + "value": 390 + }, + { + "text": "UTC+07:00, G", + "value": 420 + }, + { + "text": "UTC+08:00, H", + "value": 480 + }, + { + "text": "UTC+08:45, H*", + "value": 525 + }, + { + "text": "UTC+09:00, I", + "value": 540 + }, + { + "text": "UTC+09:30, I†", + "value": 570 + }, + { + "text": "UTC+10:00, K", + "value": 600 + }, + { + "text": "UTC+10:30, K†", + "value": 630 + }, + { + "text": "UTC+11:00, L", + "value": 660 + }, + { + "text": "UTC+12:00, M", + "value": 720 + }, + { + "text": "UTC+12:45, M*", + "value": 765 + }, + { + "text": "UTC+13:00, M†", + "value": 780 + }, + { + "text": "UTC+14:00, M†", + "value": 840 + } + ] + }, languagesData -) +); const capitalize = (s) => { if (typeof s !== 'string') return '' @@ -15,16 +175,34 @@ const capitalize = (s) => { } const capitalizeAllWords = (string) => { let firstPart = `` + let secondPart = `` + let thirdPart = `` let newString = `` - string.split(' ').forEach((part) => { + string + .replace(/"/g,'') + .split(' ').forEach((part) => { firstPart += capitalize(part) }) firstPart.split('_').forEach((part) => { + secondPart += capitalize(part) + }) + secondPart.split('-').forEach((part) => { + thirdPart += capitalize(part) + }) + thirdPart.split('=').forEach((part) => { newString += capitalize(part) }) return newString } - +function replaceTextWithLandParam(langText,langParam){ + if(definitonsRawData.indexOf(`"${langText}"`) > -1){ + definitonsRawData = definitonsRawData.replace(`"${langText}"`,`lang["${langParam}"]`) + console.log('Replacing : ',definitonsRawData.indexOf(`"${langText}"`),`"${langText}"`,langParam) + }else if(definitonsRawData.indexOf(`'${langText}'`) > -1){ + definitonsRawData = definitonsRawData.replace(`'${langText}'`,`lang["${langParam}"]`) + console.log('Replacing : ',definitonsRawData.indexOf(`'${langText}'`),`'${langText}'`,langParam) + } +} const processSection = (section) => { try{ if(section.info){ @@ -38,6 +216,7 @@ const processSection = (section) => { const langParam = `fieldText` + capitalizeAllWords(cleanName) const langText = field.description newLangParams[langParam] = langText + replaceTextWithLandParam(langText,langParam) } if(field.possible instanceof Array){ field.possible.forEach((possibility) => { @@ -45,6 +224,7 @@ const processSection = (section) => { const langParam = `fieldText` + capitalizeAllWords(cleanName) + capitalizeAllWords(possibility.name) const langText = possibility.info newLangParams[langParam] = langText + replaceTextWithLandParam(langText,langParam) } }) } @@ -53,6 +233,7 @@ const processSection = (section) => { }) } }catch(err){ + console.log(err) console.error(section) console.error(err) } @@ -69,7 +250,19 @@ pageKeys.forEach((pageKey) => { processSection(section) }) }else{ - console.log(page) + // console.log(page) } }) -console.log(newLangParams) +const newLanguageFile = Object.assign(languagesData,newLangParams) +// console.log(definitonsRawData) +console.log(newLanguageFile) +setTimeout(() => { + try{ + console.log('Writing New Definitions File, en_CA.js') + fs.writeFileSync(newDefinitionFile,definitonsRawData) + console.log('Writing New Language File, en_CA.json') + fs.writeFileSync(newLanguagesFile,JSON.stringify(newLanguageFile,null,3)) + }catch(err){ + console.log(err) + } +},2000) diff --git a/tools/makeIncludedNpmList.js b/tools/makeIncludedNpmList.js new file mode 100644 index 00000000..5da8c589 --- /dev/null +++ b/tools/makeIncludedNpmList.js @@ -0,0 +1,7 @@ +const package = require(`../package.json`) +const depKeys = Object.keys(package.dependencies) +let endText = `` +depKeys.forEach((key) => { + endText += `${key} - https://www.npmjs.com/package/${key}\n` +}) +console.log(endText) diff --git a/tools/translateLanguageFile.js b/tools/translateLanguageFile.js index 418de203..25aa30dd 100644 --- a/tools/translateLanguageFile.js +++ b/tools/translateLanguageFile.js @@ -1,84 +1,120 @@ -console.log('This translation tool uses Yandex.') +const fs = require('fs'); +console.log('This translation tool uses a Google Translate scraper. Use responsibly or your IP will be blocked by Google from using the service.') if(!process.argv[2]||!process.argv[3]||!process.argv[4]){ console.log('You must input arguments.') console.log('# node translateLanguageFile.js ') console.log('Example:') - console.log('# node translateLanguageFile.js en_US en ar') + console.log('# node translateLanguageFile.js en_CA en ar') return } -var langDir='../languages/' -var fs=require('fs'); -var https = require('https'); -var jsonfile=require('jsonfile'); -var source=require(langDir+process.argv[2]+'.json') -var list = Object.keys(source) +let translate; +try{ + translate = require('@vitalets/google-translate-api') +}catch(err){ + console.log(`You are missing a module to use this tool. Run "npm install @vitalets/google-translate-api" to install the required module.`) + return +} +const langDir = `${__dirname}/../languages/` +const sourceLangauge = process.argv[3] +const inputFileLangauge = process.argv[2] +const inputFileName = inputFileLangauge + '.json' +const source = require(langDir + inputFileName) +const list = Object.keys(source) console.log(list.length) var extra = '' var current = 1 var currentItem = list[0] -var chosenFile = langDir+process.argv[4]+'.json' +const outputFileLangauge = process.argv[4] +const outputFileName = outputFileLangauge + '.json' +const chosenFile = langDir + outputFileName +const generatedLanguageFilesPath = `${__dirname}/generatedLanguageFiles/` +const generatedFilePath = `${generatedLanguageFilesPath}${outputFileName}` +const throttleTime = parseInt(process.argv[5]) || 1000 +const usePendingFileForOutputSource = process.argv[6] === '1' +let newList try{ - newList=require(chosenFile) + const buildOutputSource = usePendingFileForOutputSource ? generatedFilePath : chosenFile + console.log(`Source Path : ${buildOutputSource}`) + eval(`newList = ${fs.readFileSync(buildOutputSource,'utf8')}`) + console.log(`The word "Save" in this language : `,newList['Save']) }catch(err){ - console.log(chosenFile) - var newList={} + console.log(`There was an error loading : ${chosenFile}`) + console.log(`Using blank base file. This will translate against all available terms!!!`) + newList = {} } -var newListAlphabetical={} -var goNext=function(){ - ++current - currentItem = list[current] - if(list.length===current){ - console.log('complete checking.. please wait') - Object.keys(newList).sort().forEach(function(y,t){ - newListAlphabetical[y]=newList[y] - }) - jsonfile.writeFile(chosenFile,newListAlphabetical,{spaces: 2},function(){ - console.log('complete writing') - }) - }else{ - next(currentItem) +async function writeLanguageFile(theList,alternatePath){ + const newListAlphabetical = {} + const sourceTermKeysOrdered = Object.keys(theList).sort() + sourceTermKeysOrdered.forEach(function(y){ + newListAlphabetical[y] = theList[y] + }) + await fs.promises.writeFile(alternatePath ? alternatePath : generatedFilePath,JSON.stringify(newListAlphabetical,null,3)) +} +function asyncSetTimeout(timeout){ + return new Promise((resolve,reject) => { + setTimeout(() => { + resolve() + },timeout || 1000) + }) +} +async function makeFolderForOutput(timeout){ + try{ + await fs.promises.mkdir(generatedLanguageFilesPath) + }catch(err){ + console.log(err) } } -var next=function(v){ - if(v===undefined){return false} - //trnsl.1.1.20170718T033617Z.a9bbd3b739ca59df.7f89b7474ec69812afd0014b5e338328ebf3fc39 - if(newList[v]&&newList[v]!==source[v]){ - goNext() - return +async function moveNewLanguageFile(){ + try{ + await fs.promises.unlink(chosenFile) + }catch(err){ + console.log('Failed to Delete old File!') + console.log(err) } - if(/<[a-z][\s\S]*>/i.test(source[v])===true){ - extra+='&format=html' + try{ + await writeLanguageFile(newList,chosenFile) + }catch(err){ + console.log('Failed to Move File!') + console.log(err) } - var url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=trnsl.1.1.20160311T042953Z.341f2f63f38bdac6.c7e5c01fff7f57160141021ca61b60e36ff4d379'+extra+'&lang='+process.argv[3]+'-'+process.argv[4]+'&text='+source[v] - https.request(url, function(data) { - data.setEncoding('utf8'); - var chunks=''; - data.on('data', (chunk) => { - chunks+=chunk; +} +function runTranslation(termKey,numberInLine){ + return new Promise((resolve,reject) => { + if(termKey === undefined)return false; + const existingTerm = newList[termKey] + if(existingTerm && existingTerm !== source[termKey]){ + resolve(existingTerm) + console.log('found a rule for this one, skipping : ',source[termKey]); + return + } + console.log(`${numberInLine} of ${list.length}`) + translate(source[termKey], { + to: outputFileLangauge, + from: sourceLangauge + }).then(res => { + translation = res.text; + newList[termKey] = translation; + console.log(termKey,' ---> ',translation) + setTimeout(() => { + resolve(translation) + },throttleTime) + }).catch(err => { + translation = `${source[termKey]}` + console.log('translation failed : ',translation); + console.error(err); + newList[termKey] = translation; + resolve() }); - data.on('end', () => { - try{ - chunks=JSON.parse(chunks) - if(chunks.html){ - if(chunks.html[0]){ - var translation=chunks.html[0] - }else{ - var translation=chunks.html - } - - }else{ - var translation=chunks.text[0] - } - }catch(err){ - var translation=source[v] - } - newList[v]=translation; - console.log(current+'/'+list.length+','+v+' ---> '+translation) - goNext() - }); - }).on('error', function(e) { - console.log('ERROR : 500 '+v) - res.sendStatus(500); - }).end(); + }) } -next(currentItem) \ No newline at end of file +async function runTranslatorOnSourceTerms(){ + await makeFolderForOutput() + for (let i = 0; i < list.length; i++) { + let termKey = list[i] + await runTranslation(termKey,i) + await writeLanguageFile(newList) + } + await moveNewLanguageFile() + console.log('Building Language File Complete!') +} +runTranslatorOnSourceTerms() diff --git a/web/assets/css/bootstrap5-theme.css b/web/assets/css/bootstrap5-theme.css new file mode 100644 index 00000000..67c2abf5 --- /dev/null +++ b/web/assets/css/bootstrap5-theme.css @@ -0,0 +1,55 @@ +/*! `Custom` Bootstrap 4 theme */ +/*! + * Bootstrap v4.5.0 (https://getbootstrap.com/) + * Copyright 2011-2020 The Bootstrap Authors + * Copyright 2011-2020 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */:root{--blue: #007bff;--indigo: #6610f2;--purple: #6f42c1;--pink: #e83e8c;--red: #dc3545;--orange: #fd7e14;--yellow: #ffc107;--green: #28a745;--teal: #20c997;--cyan: #17a2b8;--white: #fff;--gray: #6c757d;--gray-dark: #343a40;--primary: #1f80f9;--secondary: #206ed5;--success: #297ac2;--info: #456bc4;--warning: #d15f42;--danger: #c93831;--light: #eceeec;--dark: #1e2b37;--breakpoint-xs: 0;--breakpoint-sm: 576px;--breakpoint-md: 768px;--breakpoint-lg: 992px;--breakpoint-xl: 1200px;--font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-family-monospace: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace}*,*::before,*::after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:left;background-color:#fff}[tabindex="-1"]:focus:not(:focus-visible){outline:0 !important}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem}p{margin-top:0;margin-bottom:1rem}abbr[title],abbr[data-original-title]{text-decoration:underline;text-decoration:underline dotted;cursor:help;border-bottom:0;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul,dl{margin-top:0;margin-bottom:1rem}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#1f80f9;text-decoration:none;background-color:transparent}a:hover{color:#055bc6;text-decoration:underline}a:not([href]){text-decoration:none}a:not([href]):hover{text-decoration:none}pre,code,kbd,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}pre{margin-top:0;margin-bottom:1rem;overflow:auto;-ms-overflow-style:scrollbar}figure{margin:0 0 1rem}img{vertical-align:middle;border-style:none}svg{overflow:hidden;vertical-align:middle}table{border-collapse:collapse}caption{padding-top:.75rem;padding-bottom:.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}label{display:inline-block;margin-bottom:.5rem}button{border-radius:0}button:focus{outline:1px dotted;outline:5px auto -webkit-focus-ring-color}input,button,select,optgroup,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}[role="button"]{cursor:pointer}select{word-wrap:normal}button,[type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button:not(:disabled),[type="button"]:not(:disabled),[type="reset"]:not(:disabled),[type="submit"]:not(:disabled){cursor:pointer}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{padding:0;border-style:none}input[type="radio"],input[type="checkbox"]{box-sizing:border-box;padding:0}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{display:block;width:100%;max-width:100%;padding:0;margin-bottom:.5rem;font-size:1.5rem;line-height:inherit;color:inherit;white-space:normal}progress{vertical-align:baseline}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{outline-offset:-2px;-webkit-appearance:none}[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item;cursor:pointer}template{display:none}[hidden]{display:none !important}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{margin-bottom:.5rem;font-weight:500;line-height:1.2}h1,.h1{font-size:2.5rem}h2,.h2{font-size:2rem}h3,.h3{font-size:1.75rem}h4,.h4{font-size:1.5rem}h5,.h5{font-size:1.25rem}h6,.h6{font-size:1rem}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:6rem;font-weight:300;line-height:1.2}.display-2{font-size:5.5rem;font-weight:300;line-height:1.2}.display-3{font-size:4.5rem;font-weight:300;line-height:1.2}.display-4{font-size:3.5rem;font-weight:300;line-height:1.2}hr{margin-top:1rem;margin-bottom:1rem;border:0;border-top:1px solid rgba(0,0,0,0.1)}small,.small{font-size:80%;font-weight:400}mark,.mark{padding:.2em;background-color:#fcf8e3}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:90%;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote-footer{display:block;font-size:80%;color:#6c757d}.blockquote-footer::before{content:"\2014\00A0"}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:90%;color:#6c757d}code{font-size:87.5%;color:#e83e8c;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:87.5%;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:100%;font-weight:700}pre{display:block;font-size:87.5%;color:#212529}pre code{font-size:inherit;color:inherit;word-break:normal}.pre-scrollable{max-height:340px;overflow-y:scroll}.table{width:100%;margin-bottom:1rem;color:#212529}.table th,.table td{padding:.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table-sm th,.table-sm td{padding:.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered th,.table-bordered td{border:1px solid #dee2e6}.table-bordered thead th,.table-bordered thead td{border-bottom-width:2px}.table-borderless th,.table-borderless td,.table-borderless thead th,.table-borderless tbody+tbody{border:0}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,0.05)}.table-hover tbody tr:hover{color:#212529;background-color:rgba(0,0,0,0.075)}.table-primary,.table-primary>th,.table-primary>td{background-color:#c0dbfd}.table-primary th,.table-primary td,.table-primary thead th,.table-primary tbody+tbody{border-color:#8bbdfc}.table-hover .table-primary:hover{background-color:#a7cdfc}.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th{background-color:#a7cdfc}.table-secondary,.table-secondary>th,.table-secondary>td{background-color:#c1d6f3}.table-secondary th,.table-secondary td,.table-secondary thead th,.table-secondary tbody+tbody{border-color:#8bb4e9}.table-hover .table-secondary:hover{background-color:#acc8ef}.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th{background-color:#acc8ef}.table-success,.table-success>th,.table-success>td{background-color:#c3daee}.table-success th,.table-success td,.table-success thead th,.table-success tbody+tbody{border-color:#90badf}.table-hover .table-success:hover{background-color:#afcee8}.table-hover .table-success:hover>td,.table-hover .table-success:hover>th{background-color:#afcee8}.table-info,.table-info>th,.table-info>td{background-color:#cbd6ee}.table-info th,.table-info td,.table-info thead th,.table-info tbody+tbody{border-color:#9eb2e0}.table-hover .table-info:hover{background-color:#b8c7e8}.table-hover .table-info:hover>td,.table-hover .table-info:hover>th{background-color:#b8c7e8}.table-warning,.table-warning>th,.table-warning>td{background-color:#f2d2ca}.table-warning th,.table-warning td,.table-warning thead th,.table-warning tbody+tbody{border-color:#e7ac9d}.table-hover .table-warning:hover{background-color:#edc1b6}.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th{background-color:#edc1b6}.table-danger,.table-danger>th,.table-danger>td{background-color:#f0c7c5}.table-danger th,.table-danger td,.table-danger thead th,.table-danger tbody+tbody{border-color:#e39894}.table-hover .table-danger:hover{background-color:#ebb3b1}.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th{background-color:#ebb3b1}.table-light,.table-light>th,.table-light>td{background-color:#fafafa}.table-light th,.table-light td,.table-light thead th,.table-light tbody+tbody{border-color:#f5f6f5}.table-hover .table-light:hover{background-color:#ededed}.table-hover .table-light:hover>td,.table-hover .table-light:hover>th{background-color:#ededed}.table-dark,.table-dark>th,.table-dark>td{background-color:#c0c4c7}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#8a9197}.table-hover .table-dark:hover{background-color:#b3b7bb}.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th{background-color:#b3b7bb}.table-active,.table-active>th,.table-active>td{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,0.075)}.table-hover .table-active:hover>td,.table-hover .table-active:hover>th{background-color:rgba(0,0,0,0.075)}.table .thead-dark th{color:#fff;background-color:#343a40;border-color:#454d55}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#343a40}.table-dark th,.table-dark td,.table-dark thead th{border-color:#454d55}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,0.05)}.table-dark.table-hover tbody tr:hover{color:#fff;background-color:rgba(255,255,255,0.075)}@media (max-width: 575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-sm>.table-bordered{border:0}}@media (max-width: 767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-md>.table-bordered{border:0}}@media (max-width: 991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-lg>.table-bordered{border:0}}@media (max-width: 1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive-xl>.table-bordered{border:0}}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch}.table-responsive>.table-bordered{border:0}.form-control{display:block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;border-radius:.25rem;transition:border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.form-control{transition:none}}.form-control::-ms-expand{background-color:transparent;border:0}.form-control:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.form-control:focus{color:#495057;background-color:#fff;border-color:#9bc6fc;outline:0;box-shadow:0 0 0 .2rem rgba(31,128,249,0.25)}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}input[type="date"].form-control,input[type="time"].form-control,input[type="datetime-local"].form-control,input[type="month"].form-control{appearance:none}select.form-control:focus::-ms-value{color:#495057;background-color:#fff}.form-control-file,.form-control-range{display:block;width:100%}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem;line-height:1.5}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem;line-height:1.5}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;font-size:1rem;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-sm,.form-control-plaintext.form-control-lg{padding-right:0;padding-left:0}.form-control-sm{height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.form-control-lg{height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}select.form-control[size],select.form-control[multiple]{height:auto}textarea.form-control{height:auto}.form-group{margin-bottom:1rem}.form-text{display:block;margin-top:.25rem}.form-row{display:flex;flex-wrap:wrap;margin-right:-5px;margin-left:-5px}.form-row>.col,.form-row>[class*="col-"]{padding-right:5px;padding-left:5px}.form-check{position:relative;display:block;padding-left:1.25rem}.form-check-input{position:absolute;margin-top:.3rem;margin-left:-1.25rem}.form-check-input[disabled] ~ .form-check-label,.form-check-input:disabled ~ .form-check-label{color:#6c757d}.form-check-label{margin-bottom:0}.form-check-inline{display:inline-flex;align-items:center;padding-left:0;margin-right:.75rem}.form-check-inline .form-check-input{position:static;margin-top:0;margin-right:.3125rem;margin-left:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#297ac2}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(41,122,194,0.9);border-radius:.25rem}.was-validated :valid ~ .valid-feedback,.was-validated :valid ~ .valid-tooltip,.is-valid ~ .valid-feedback,.is-valid ~ .valid-tooltip{display:block}.was-validated .form-control:valid,.form-control.is-valid{border-color:#297ac2;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23297ac2' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:valid:focus,.form-control.is-valid:focus{border-color:#297ac2;box-shadow:0 0 0 .2rem rgba(41,122,194,0.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:valid,.custom-select.is-valid{border-color:#297ac2;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23297ac2' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .custom-select:valid:focus,.custom-select.is-valid:focus{border-color:#297ac2;box-shadow:0 0 0 .2rem rgba(41,122,194,0.25)}.was-validated .form-check-input:valid ~ .form-check-label,.form-check-input.is-valid ~ .form-check-label{color:#297ac2}.was-validated .form-check-input:valid ~ .valid-feedback,.was-validated .form-check-input:valid ~ .valid-tooltip,.form-check-input.is-valid ~ .valid-feedback,.form-check-input.is-valid ~ .valid-tooltip{display:block}.was-validated .custom-control-input:valid ~ .custom-control-label,.custom-control-input.is-valid ~ .custom-control-label{color:#297ac2}.was-validated .custom-control-input:valid ~ .custom-control-label::before,.custom-control-input.is-valid ~ .custom-control-label::before{border-color:#297ac2}.was-validated .custom-control-input:valid:checked ~ .custom-control-label::before,.custom-control-input.is-valid:checked ~ .custom-control-label::before{border-color:#4693d8;background-color:#4693d8}.was-validated .custom-control-input:valid:focus ~ .custom-control-label::before,.custom-control-input.is-valid:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(41,122,194,0.25)}.was-validated .custom-control-input:valid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-valid:focus:not(:checked) ~ .custom-control-label::before{border-color:#297ac2}.was-validated .custom-file-input:valid ~ .custom-file-label,.custom-file-input.is-valid ~ .custom-file-label{border-color:#297ac2}.was-validated .custom-file-input:valid:focus ~ .custom-file-label,.custom-file-input.is-valid:focus ~ .custom-file-label{border-color:#297ac2;box-shadow:0 0 0 .2rem rgba(41,122,194,0.25)}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:80%;color:#c93831}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;line-height:1.5;color:#fff;background-color:rgba(201,56,49,0.9);border-radius:.25rem}.was-validated :invalid ~ .invalid-feedback,.was-validated :invalid ~ .invalid-tooltip,.is-invalid ~ .invalid-feedback,.is-invalid ~ .invalid-tooltip{display:block}.was-validated .form-control:invalid,.form-control.is-invalid{border-color:#c93831;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23c93831' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23c93831' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .form-control:invalid:focus,.form-control.is-invalid:focus{border-color:#c93831;box-shadow:0 0 0 .2rem rgba(201,56,49,0.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.was-validated .custom-select:invalid,.custom-select.is-invalid{border-color:#c93831;padding-right:calc(.75em + 2.3125rem);background:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='none' stroke='%23c93831' viewBox='0 0 12 12'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23c93831' stroke='none'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(.75em + .375rem) calc(.75em + .375rem)}.was-validated .custom-select:invalid:focus,.custom-select.is-invalid:focus{border-color:#c93831;box-shadow:0 0 0 .2rem rgba(201,56,49,0.25)}.was-validated .form-check-input:invalid ~ .form-check-label,.form-check-input.is-invalid ~ .form-check-label{color:#c93831}.was-validated .form-check-input:invalid ~ .invalid-feedback,.was-validated .form-check-input:invalid ~ .invalid-tooltip,.form-check-input.is-invalid ~ .invalid-feedback,.form-check-input.is-invalid ~ .invalid-tooltip{display:block}.was-validated .custom-control-input:invalid ~ .custom-control-label,.custom-control-input.is-invalid ~ .custom-control-label{color:#c93831}.was-validated .custom-control-input:invalid ~ .custom-control-label::before,.custom-control-input.is-invalid ~ .custom-control-label::before{border-color:#c93831}.was-validated .custom-control-input:invalid:checked ~ .custom-control-label::before,.custom-control-input.is-invalid:checked ~ .custom-control-label::before{border-color:#d65d57;background-color:#d65d57}.was-validated .custom-control-input:invalid:focus ~ .custom-control-label::before,.custom-control-input.is-invalid:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(201,56,49,0.25)}.was-validated .custom-control-input:invalid:focus:not(:checked) ~ .custom-control-label::before,.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before{border-color:#c93831}.was-validated .custom-file-input:invalid ~ .custom-file-label,.custom-file-input.is-invalid ~ .custom-file-label{border-color:#c93831}.was-validated .custom-file-input:invalid:focus ~ .custom-file-label,.custom-file-input.is-invalid:focus ~ .custom-file-label{border-color:#c93831;box-shadow:0 0 0 .2rem rgba(201,56,49,0.25)}.form-inline{display:flex;flex-flow:row wrap;align-items:center}.form-inline .form-check{width:100%}@media (min-width: 576px){.form-inline label{display:flex;align-items:center;justify-content:center;margin-bottom:0}.form-inline .form-group{display:flex;flex:0 0 auto;flex-flow:row wrap;align-items:center;margin-bottom:0}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .form-control-plaintext{display:inline-block}.form-inline .input-group,.form-inline .custom-select{width:auto}.form-inline .form-check{display:flex;align-items:center;justify-content:center;width:auto;padding-left:0}.form-inline .form-check-input{position:relative;flex-shrink:0;margin-top:0;margin-right:.25rem;margin-left:0}.form-inline .custom-control{align-items:center;justify-content:center}.form-inline .custom-control-label{margin-bottom:0}}.btn{display:inline-block;font-weight:400;color:#212529;text-align:center;vertical-align:middle;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;line-height:1.5;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.btn{transition:none}}.btn:hover{color:#212529;text-decoration:none}.btn:focus,.btn.focus{outline:0;box-shadow:0 0 0 .2rem rgba(31,128,249,0.25)}.btn.disabled,.btn:disabled{opacity:.65}.btn:not(:disabled):not(.disabled){cursor:pointer}a.btn.disabled,fieldset:disabled a.btn{pointer-events:none}.btn-primary{color:#fff;background-color:#1f80f9;border-color:#1f80f9}.btn-primary:hover{color:#fff;background-color:#066ceb;border-color:#0667df}.btn-primary:focus,.btn-primary.focus{color:#fff;background-color:#066ceb;border-color:#0667df;box-shadow:0 0 0 .2rem rgba(65,147,250,0.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#1f80f9;border-color:#1f80f9}.btn-primary:not(:disabled):not(.disabled):active,.btn-primary:not(:disabled):not(.disabled).active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0667df;border-color:#0661d3}.btn-primary:not(:disabled):not(.disabled):active:focus,.btn-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(65,147,250,0.5)}.btn-secondary{color:#fff;background-color:#206ed5;border-color:#206ed5}.btn-secondary:hover{color:#fff;background-color:#1b5db4;border-color:#1957a9}.btn-secondary:focus,.btn-secondary.focus{color:#fff;background-color:#1b5db4;border-color:#1957a9;box-shadow:0 0 0 .2rem rgba(65,132,219,0.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#206ed5;border-color:#206ed5}.btn-secondary:not(:disabled):not(.disabled):active,.btn-secondary:not(:disabled):not(.disabled).active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#1957a9;border-color:#18519e}.btn-secondary:not(:disabled):not(.disabled):active:focus,.btn-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(65,132,219,0.5)}.btn-success{color:#fff;background-color:#297ac2;border-color:#297ac2}.btn-success:hover{color:#fff;background-color:#2266a2;border-color:#206098}.btn-success:focus,.btn-success.focus{color:#fff;background-color:#2266a2;border-color:#206098;box-shadow:0 0 0 .2rem rgba(73,142,203,0.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#297ac2;border-color:#297ac2}.btn-success:not(:disabled):not(.disabled):active,.btn-success:not(:disabled):not(.disabled).active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#206098;border-color:#1e598d}.btn-success:not(:disabled):not(.disabled):active:focus,.btn-success:not(:disabled):not(.disabled).active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(73,142,203,0.5)}.btn-info{color:#fff;background-color:#456bc4;border-color:#456bc4}.btn-info:hover{color:#fff;background-color:#375aac;border-color:#3455a2}.btn-info:focus,.btn-info.focus{color:#fff;background-color:#375aac;border-color:#3455a2;box-shadow:0 0 0 .2rem rgba(97,129,205,0.5)}.btn-info.disabled,.btn-info:disabled{color:#fff;background-color:#456bc4;border-color:#456bc4}.btn-info:not(:disabled):not(.disabled):active,.btn-info:not(:disabled):not(.disabled).active,.show>.btn-info.dropdown-toggle{color:#fff;background-color:#3455a2;border-color:#305099}.btn-info:not(:disabled):not(.disabled):active:focus,.btn-info:not(:disabled):not(.disabled).active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(97,129,205,0.5)}.btn-warning{color:#fff;background-color:#d15f42;border-color:#d15f42}.btn-warning:hover{color:#fff;background-color:#be4c2e;border-color:#b4472c}.btn-warning:focus,.btn-warning.focus{color:#fff;background-color:#be4c2e;border-color:#b4472c;box-shadow:0 0 0 .2rem rgba(216,119,94,0.5)}.btn-warning.disabled,.btn-warning:disabled{color:#fff;background-color:#d15f42;border-color:#d15f42}.btn-warning:not(:disabled):not(.disabled):active,.btn-warning:not(:disabled):not(.disabled).active,.show>.btn-warning.dropdown-toggle{color:#fff;background-color:#b4472c;border-color:#aa4329}.btn-warning:not(:disabled):not(.disabled):active:focus,.btn-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(216,119,94,0.5)}.btn-danger{color:#fff;background-color:#c93831;border-color:#c93831}.btn-danger:hover{color:#fff;background-color:#aa2f2a;border-color:#a02d27}.btn-danger:focus,.btn-danger.focus{color:#fff;background-color:#aa2f2a;border-color:#a02d27;box-shadow:0 0 0 .2rem rgba(209,86,80,0.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#c93831;border-color:#c93831}.btn-danger:not(:disabled):not(.disabled):active,.btn-danger:not(:disabled):not(.disabled).active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#a02d27;border-color:#962a25}.btn-danger:not(:disabled):not(.disabled):active:focus,.btn-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(209,86,80,0.5)}.btn-light{color:#212529;background-color:#eceeec;border-color:#eceeec}.btn-light:hover{color:#212529;background-color:#d8dcd8;border-color:#d1d6d1}.btn-light:focus,.btn-light.focus{color:#212529;background-color:#d8dcd8;border-color:#d1d6d1;box-shadow:0 0 0 .2rem rgba(206,208,207,0.5)}.btn-light.disabled,.btn-light:disabled{color:#212529;background-color:#eceeec;border-color:#eceeec}.btn-light:not(:disabled):not(.disabled):active,.btn-light:not(:disabled):not(.disabled).active,.show>.btn-light.dropdown-toggle{color:#212529;background-color:#d1d6d1;border-color:#cad0ca}.btn-light:not(:disabled):not(.disabled):active:focus,.btn-light:not(:disabled):not(.disabled).active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(206,208,207,0.5)}.btn-dark{color:#fff;background-color:#1e2b37;border-color:#1e2b37}.btn-dark:hover{color:#fff;background-color:#11181e;border-color:#0c1116}.btn-dark:focus,.btn-dark.focus{color:#fff;background-color:#11181e;border-color:#0c1116;box-shadow:0 0 0 .2rem rgba(64,75,85,0.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#1e2b37;border-color:#1e2b37}.btn-dark:not(:disabled):not(.disabled):active,.btn-dark:not(:disabled):not(.disabled).active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#0c1116;border-color:#080b0e}.btn-dark:not(:disabled):not(.disabled):active:focus,.btn-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(64,75,85,0.5)}.btn-outline-primary{color:#1f80f9;border-color:#1f80f9}.btn-outline-primary:hover{color:#fff;background-color:#1f80f9;border-color:#1f80f9}.btn-outline-primary:focus,.btn-outline-primary.focus{box-shadow:0 0 0 .2rem rgba(31,128,249,0.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#1f80f9;background-color:transparent}.btn-outline-primary:not(:disabled):not(.disabled):active,.btn-outline-primary:not(:disabled):not(.disabled).active,.show>.btn-outline-primary.dropdown-toggle{color:#fff;background-color:#1f80f9;border-color:#1f80f9}.btn-outline-primary:not(:disabled):not(.disabled):active:focus,.btn-outline-primary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-primary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(31,128,249,0.5)}.btn-outline-secondary{color:#206ed5;border-color:#206ed5}.btn-outline-secondary:hover{color:#fff;background-color:#206ed5;border-color:#206ed5}.btn-outline-secondary:focus,.btn-outline-secondary.focus{box-shadow:0 0 0 .2rem rgba(32,110,213,0.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#206ed5;background-color:transparent}.btn-outline-secondary:not(:disabled):not(.disabled):active,.btn-outline-secondary:not(:disabled):not(.disabled).active,.show>.btn-outline-secondary.dropdown-toggle{color:#fff;background-color:#206ed5;border-color:#206ed5}.btn-outline-secondary:not(:disabled):not(.disabled):active:focus,.btn-outline-secondary:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(32,110,213,0.5)}.btn-outline-success{color:#297ac2;border-color:#297ac2}.btn-outline-success:hover{color:#fff;background-color:#297ac2;border-color:#297ac2}.btn-outline-success:focus,.btn-outline-success.focus{box-shadow:0 0 0 .2rem rgba(41,122,194,0.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#297ac2;background-color:transparent}.btn-outline-success:not(:disabled):not(.disabled):active,.btn-outline-success:not(:disabled):not(.disabled).active,.show>.btn-outline-success.dropdown-toggle{color:#fff;background-color:#297ac2;border-color:#297ac2}.btn-outline-success:not(:disabled):not(.disabled):active:focus,.btn-outline-success:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-success.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(41,122,194,0.5)}.btn-outline-info{color:#456bc4;border-color:#456bc4}.btn-outline-info:hover{color:#fff;background-color:#456bc4;border-color:#456bc4}.btn-outline-info:focus,.btn-outline-info.focus{box-shadow:0 0 0 .2rem rgba(69,107,196,0.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#456bc4;background-color:transparent}.btn-outline-info:not(:disabled):not(.disabled):active,.btn-outline-info:not(:disabled):not(.disabled).active,.show>.btn-outline-info.dropdown-toggle{color:#fff;background-color:#456bc4;border-color:#456bc4}.btn-outline-info:not(:disabled):not(.disabled):active:focus,.btn-outline-info:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-info.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(69,107,196,0.5)}.btn-outline-warning{color:#d15f42;border-color:#d15f42}.btn-outline-warning:hover{color:#fff;background-color:#d15f42;border-color:#d15f42}.btn-outline-warning:focus,.btn-outline-warning.focus{box-shadow:0 0 0 .2rem rgba(209,95,66,0.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#d15f42;background-color:transparent}.btn-outline-warning:not(:disabled):not(.disabled):active,.btn-outline-warning:not(:disabled):not(.disabled).active,.show>.btn-outline-warning.dropdown-toggle{color:#fff;background-color:#d15f42;border-color:#d15f42}.btn-outline-warning:not(:disabled):not(.disabled):active:focus,.btn-outline-warning:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-warning.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(209,95,66,0.5)}.btn-outline-danger{color:#c93831;border-color:#c93831}.btn-outline-danger:hover{color:#fff;background-color:#c93831;border-color:#c93831}.btn-outline-danger:focus,.btn-outline-danger.focus{box-shadow:0 0 0 .2rem rgba(201,56,49,0.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#c93831;background-color:transparent}.btn-outline-danger:not(:disabled):not(.disabled):active,.btn-outline-danger:not(:disabled):not(.disabled).active,.show>.btn-outline-danger.dropdown-toggle{color:#fff;background-color:#c93831;border-color:#c93831}.btn-outline-danger:not(:disabled):not(.disabled):active:focus,.btn-outline-danger:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-danger.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(201,56,49,0.5)}.btn-outline-light{color:#eceeec;border-color:#eceeec}.btn-outline-light:hover{color:#212529;background-color:#eceeec;border-color:#eceeec}.btn-outline-light:focus,.btn-outline-light.focus{box-shadow:0 0 0 .2rem rgba(236,238,236,0.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#eceeec;background-color:transparent}.btn-outline-light:not(:disabled):not(.disabled):active,.btn-outline-light:not(:disabled):not(.disabled).active,.show>.btn-outline-light.dropdown-toggle{color:#212529;background-color:#eceeec;border-color:#eceeec}.btn-outline-light:not(:disabled):not(.disabled):active:focus,.btn-outline-light:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-light.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(236,238,236,0.5)}.btn-outline-dark{color:#1e2b37;border-color:#1e2b37}.btn-outline-dark:hover{color:#fff;background-color:#1e2b37;border-color:#1e2b37}.btn-outline-dark:focus,.btn-outline-dark.focus{box-shadow:0 0 0 .2rem rgba(30,43,55,0.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#1e2b37;background-color:transparent}.btn-outline-dark:not(:disabled):not(.disabled):active,.btn-outline-dark:not(:disabled):not(.disabled).active,.show>.btn-outline-dark.dropdown-toggle{color:#fff;background-color:#1e2b37;border-color:#1e2b37}.btn-outline-dark:not(:disabled):not(.disabled):active:focus,.btn-outline-dark:not(:disabled):not(.disabled).active:focus,.show>.btn-outline-dark.dropdown-toggle:focus{box-shadow:0 0 0 .2rem rgba(30,43,55,0.5)}.btn-link{font-weight:400;color:#1f80f9;text-decoration:none}.btn-link:hover{color:#055bc6;text-decoration:underline}.btn-link:focus,.btn-link.focus{text-decoration:underline}.btn-link:disabled,.btn-link.disabled{color:#6c757d;pointer-events:none}.btn-lg,.btn-group-lg>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.btn-sm,.btn-group-sm>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.btn-block{display:block;width:100%}.btn-block+.btn-block{margin-top:.5rem}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{transition:opacity 0.15s linear}@media (prefers-reduced-motion: reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{position:relative;height:0;overflow:hidden;transition:height 0.35s ease}@media (prefers-reduced-motion: reduce){.collapsing{transition:none}}.dropup,.dropright,.dropdown,.dropleft{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:10rem;padding:.5rem 0;margin:.125rem 0 0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.15);border-radius:.25rem}.dropdown-menu-left{right:auto;left:0}.dropdown-menu-right{right:0;left:auto}@media (min-width: 576px){.dropdown-menu-sm-left{right:auto;left:0}.dropdown-menu-sm-right{right:0;left:auto}}@media (min-width: 768px){.dropdown-menu-md-left{right:auto;left:0}.dropdown-menu-md-right{right:0;left:auto}}@media (min-width: 992px){.dropdown-menu-lg-left{right:auto;left:0}.dropdown-menu-lg-right{right:0;left:auto}}@media (min-width: 1200px){.dropdown-menu-xl-left{right:auto;left:0}.dropdown-menu-xl-right{right:0;left:auto}}.dropup .dropdown-menu{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-menu{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropright .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropright .dropdown-toggle:empty::after{margin-left:0}.dropright .dropdown-toggle::after{vertical-align:0}.dropleft .dropdown-menu{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropleft .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropleft .dropdown-toggle::after{display:none}.dropleft .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropleft .dropdown-toggle:empty::after{margin-left:0}.dropleft .dropdown-toggle::before{vertical-align:0}.dropdown-menu[x-placement^="top"],.dropdown-menu[x-placement^="right"],.dropdown-menu[x-placement^="bottom"],.dropdown-menu[x-placement^="left"]{right:auto;bottom:auto}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid #e9ecef}.dropdown-item{display:block;width:100%;padding:.25rem 1.5rem;clear:both;font-weight:400;color:#212529;text-align:inherit;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:hover,.dropdown-item:focus{color:#16181b;text-decoration:none;background-color:#f8f9fa}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#1f80f9}.dropdown-item.disabled,.dropdown-item:disabled{color:#6c757d;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1.5rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1.5rem;color:#212529}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;flex:1 1 auto}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover{z-index:1}.btn-group>.btn:focus,.btn-group>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn.active{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child){margin-left:-1px}.btn-group>.btn:not(:last-child):not(.dropdown-toggle),.btn-group>.btn-group:not(:last-child)>.btn{border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn:not(:first-child),.btn-group>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after,.dropright .dropdown-toggle-split::after{margin-left:0}.dropleft .dropdown-toggle-split::before{margin-right:0}.btn-sm+.dropdown-toggle-split,.btn-group-sm>.btn+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-lg+.dropdown-toggle-split,.btn-group-lg>.btn+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle),.btn-group-vertical>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:not(:first-child),.btn-group-vertical>.btn-group:not(:first-child)>.btn{border-top-left-radius:0;border-top-right-radius:0}.btn-group-toggle>.btn,.btn-group-toggle>.btn-group>.btn{margin-bottom:0}.btn-group-toggle>.btn input[type="radio"],.btn-group-toggle>.btn input[type="checkbox"],.btn-group-toggle>.btn-group>.btn input[type="radio"],.btn-group-toggle>.btn-group>.btn input[type="checkbox"]{position:absolute;clip:rect(0, 0, 0, 0);pointer-events:none}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-control-plaintext,.input-group>.custom-select,.input-group>.custom-file{position:relative;flex:1 1 auto;width:1%;min-width:0;margin-bottom:0}.input-group>.form-control+.form-control,.input-group>.form-control+.custom-select,.input-group>.form-control+.custom-file,.input-group>.form-control-plaintext+.form-control,.input-group>.form-control-plaintext+.custom-select,.input-group>.form-control-plaintext+.custom-file,.input-group>.custom-select+.form-control,.input-group>.custom-select+.custom-select,.input-group>.custom-select+.custom-file,.input-group>.custom-file+.form-control,.input-group>.custom-file+.custom-select,.input-group>.custom-file+.custom-file{margin-left:-1px}.input-group>.form-control:focus,.input-group>.custom-select:focus,.input-group>.custom-file .custom-file-input:focus ~ .custom-file-label{z-index:3}.input-group>.custom-file .custom-file-input:focus{z-index:4}.input-group>.form-control:not(:last-child),.input-group>.custom-select:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.form-control:not(:first-child),.input-group>.custom-select:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.custom-file{display:flex;align-items:center}.input-group>.custom-file:not(:last-child) .custom-file-label,.input-group>.custom-file:not(:last-child) .custom-file-label::after{border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.custom-file:not(:first-child) .custom-file-label{border-top-left-radius:0;border-bottom-left-radius:0}.input-group-prepend,.input-group-append{display:flex}.input-group-prepend .btn,.input-group-append .btn{position:relative;z-index:2}.input-group-prepend .btn:focus,.input-group-append .btn:focus{z-index:3}.input-group-prepend .btn+.btn,.input-group-prepend .btn+.input-group-text,.input-group-prepend .input-group-text+.input-group-text,.input-group-prepend .input-group-text+.btn,.input-group-append .btn+.btn,.input-group-append .btn+.input-group-text,.input-group-append .input-group-text+.input-group-text,.input-group-append .input-group-text+.btn{margin-left:-1px}.input-group-prepend{margin-right:-1px}.input-group-append{margin-left:-1px}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;margin-bottom:0;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-text input[type="radio"],.input-group-text input[type="checkbox"]{margin-top:0}.input-group-lg>.form-control:not(textarea),.input-group-lg>.custom-select{height:calc(1.5em + 1rem + 2px)}.input-group-lg>.form-control,.input-group-lg>.custom-select,.input-group-lg>.input-group-prepend>.input-group-text,.input-group-lg>.input-group-append>.input-group-text,.input-group-lg>.input-group-prepend>.btn,.input-group-lg>.input-group-append>.btn{padding:.5rem 1rem;font-size:1.25rem;line-height:1.5;border-radius:.3rem}.input-group-sm>.form-control:not(textarea),.input-group-sm>.custom-select{height:calc(1.5em + .5rem + 2px)}.input-group-sm>.form-control,.input-group-sm>.custom-select,.input-group-sm>.input-group-prepend>.input-group-text,.input-group-sm>.input-group-append>.input-group-text,.input-group-sm>.input-group-prepend>.btn,.input-group-sm>.input-group-append>.btn{padding:.25rem .5rem;font-size:.875rem;line-height:1.5;border-radius:.2rem}.input-group-lg>.custom-select,.input-group-sm>.custom-select{padding-right:1.75rem}.input-group>.input-group-prepend>.btn,.input-group>.input-group-prepend>.input-group-text,.input-group>.input-group-append:not(:last-child)>.btn,.input-group>.input-group-append:not(:last-child)>.input-group-text,.input-group>.input-group-append:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group>.input-group-append:last-child>.input-group-text:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>.input-group-append>.btn,.input-group>.input-group-append>.input-group-text,.input-group>.input-group-prepend:not(:first-child)>.btn,.input-group>.input-group-prepend:not(:first-child)>.input-group-text,.input-group>.input-group-prepend:first-child>.btn:not(:first-child),.input-group>.input-group-prepend:first-child>.input-group-text:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.custom-control{position:relative;display:block;min-height:1.5rem;padding-left:1.5rem}.custom-control-inline{display:inline-flex;margin-right:1rem}.custom-control-input{position:absolute;left:0;z-index:-1;width:1rem;height:1.25rem;opacity:0}.custom-control-input:checked ~ .custom-control-label::before{color:#fff;border-color:#1f80f9;background-color:#1f80f9}.custom-control-input:focus ~ .custom-control-label::before{box-shadow:0 0 0 .2rem rgba(31,128,249,0.25)}.custom-control-input:focus:not(:checked) ~ .custom-control-label::before{border-color:#9bc6fc}.custom-control-input:not(:disabled):active ~ .custom-control-label::before{color:#fff;background-color:#cde3fe;border-color:#cde3fe}.custom-control-input[disabled] ~ .custom-control-label,.custom-control-input:disabled ~ .custom-control-label{color:#6c757d}.custom-control-input[disabled] ~ .custom-control-label::before,.custom-control-input:disabled ~ .custom-control-label::before{background-color:#e9ecef}.custom-control-label{position:relative;margin-bottom:0;vertical-align:top}.custom-control-label::before{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;pointer-events:none;content:"";background-color:#fff;border:#adb5bd solid 1px}.custom-control-label::after{position:absolute;top:.25rem;left:-1.5rem;display:block;width:1rem;height:1rem;content:"";background:no-repeat 50% / 50% 50%}.custom-checkbox .custom-control-label::before{border-radius:.25rem}.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::before{border-color:#1f80f9;background-color:#1f80f9}.custom-checkbox .custom-control-input:indeterminate ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='4' viewBox='0 0 4 4'%3e%3cpath stroke='%23fff' d='M0 2h4'/%3e%3c/svg%3e")}.custom-checkbox .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(31,128,249,0.5)}.custom-checkbox .custom-control-input:disabled:indeterminate ~ .custom-control-label::before{background-color:rgba(31,128,249,0.5)}.custom-radio .custom-control-label::before{border-radius:50%}.custom-radio .custom-control-input:checked ~ .custom-control-label::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.custom-radio .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(31,128,249,0.5)}.custom-switch{padding-left:2.25rem}.custom-switch .custom-control-label::before{left:-2.25rem;width:1.75rem;pointer-events:all;border-radius:.5rem}.custom-switch .custom-control-label::after{top:calc(.25rem + 2px);left:calc(-2.25rem + 2px);width:calc(1rem - 4px);height:calc(1rem - 4px);background-color:#adb5bd;border-radius:.5rem;transition:transform 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-switch .custom-control-label::after{transition:none}}.custom-switch .custom-control-input:checked ~ .custom-control-label::after{background-color:#fff;transform:translateX(.75rem)}.custom-switch .custom-control-input:disabled:checked ~ .custom-control-label::before{background-color:rgba(31,128,249,0.5)}.custom-select{display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);padding:.375rem 1.75rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#495057;vertical-align:middle;background:#fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px;border:1px solid #ced4da;border-radius:.25rem;appearance:none}.custom-select:focus{border-color:#9bc6fc;outline:0;box-shadow:0 0 0 .2rem rgba(31,128,249,0.25)}.custom-select:focus::-ms-value{color:#495057;background-color:#fff}.custom-select[multiple],.custom-select[size]:not([size="1"]){height:auto;padding-right:.75rem;background-image:none}.custom-select:disabled{color:#6c757d;background-color:#e9ecef}.custom-select::-ms-expand{display:none}.custom-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #495057}.custom-select-sm{height:calc(1.5em + .5rem + 2px);padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.custom-select-lg{height:calc(1.5em + 1rem + 2px);padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.custom-file{position:relative;display:inline-block;width:100%;height:calc(1.5em + .75rem + 2px);margin-bottom:0}.custom-file-input{position:relative;z-index:2;width:100%;height:calc(1.5em + .75rem + 2px);margin:0;opacity:0}.custom-file-input:focus ~ .custom-file-label{border-color:#9bc6fc;box-shadow:0 0 0 .2rem rgba(31,128,249,0.25)}.custom-file-input[disabled] ~ .custom-file-label,.custom-file-input:disabled ~ .custom-file-label{background-color:#e9ecef}.custom-file-input:lang(en) ~ .custom-file-label::after{content:"Browse"}.custom-file-input ~ .custom-file-label[data-browse]::after{content:attr(data-browse)}.custom-file-label{position:absolute;top:0;right:0;left:0;z-index:1;height:calc(1.5em + .75rem + 2px);padding:.375rem .75rem;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;border:1px solid #ced4da;border-radius:.25rem}.custom-file-label::after{position:absolute;top:0;right:0;bottom:0;z-index:3;display:block;height:calc(1.5em + .75rem);padding:.375rem .75rem;line-height:1.5;color:#495057;content:"Browse";background-color:#e9ecef;border-left:inherit;border-radius:0 .25rem .25rem 0}.custom-range{width:100%;height:1.4rem;padding:0;background-color:transparent;appearance:none}.custom-range:focus{outline:none}.custom-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(31,128,249,0.25)}.custom-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(31,128,249,0.25)}.custom-range:focus::-ms-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .2rem rgba(31,128,249,0.25)}.custom-range::-moz-focus-outer{border:0}.custom-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#1f80f9;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-webkit-slider-thumb{transition:none}}.custom-range::-webkit-slider-thumb:active{background-color:#cde3fe}.custom-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#1f80f9;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-moz-range-thumb{transition:none}}.custom-range::-moz-range-thumb:active{background-color:#cde3fe}.custom-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.custom-range::-ms-thumb{width:1rem;height:1rem;margin-top:0;margin-right:.2rem;margin-left:.2rem;background-color:#1f80f9;border:0;border-radius:1rem;transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out;appearance:none}@media (prefers-reduced-motion: reduce){.custom-range::-ms-thumb{transition:none}}.custom-range::-ms-thumb:active{background-color:#cde3fe}.custom-range::-ms-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:transparent;border-color:transparent;border-width:.5rem}.custom-range::-ms-fill-lower{background-color:#dee2e6;border-radius:1rem}.custom-range::-ms-fill-upper{margin-right:15px;background-color:#dee2e6;border-radius:1rem}.custom-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.custom-range:disabled::-webkit-slider-runnable-track{cursor:default}.custom-range:disabled::-moz-range-thumb{background-color:#adb5bd}.custom-range:disabled::-moz-range-track{cursor:default}.custom-range:disabled::-ms-thumb{background-color:#adb5bd}.custom-control-label::before,.custom-file-label,.custom-select{transition:background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.custom-control-label::before,.custom-file-label,.custom-select{transition:none}}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem}.nav-link:hover,.nav-link:focus{text-decoration:none}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-item{margin-bottom:-1px}.nav-tabs .nav-link{border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:hover,.nav-tabs .nav-link:focus{border-color:#e9ecef #e9ecef #dee2e6}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-link.active,.nav-tabs .nav-item.show .nav-link{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#1f80f9}.nav-fill .nav-item{flex:1 1 auto;text-align:center}.nav-justified .nav-item{flex-basis:0;flex-grow:1;text-align:center}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:.5rem 1rem}.navbar .container,.navbar .container-fluid,.navbar>.container-sm,.navbar>.container-md,.navbar>.container-lg,.navbar>.container-xl{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between}.navbar-brand{display:inline-block;padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;line-height:inherit;white-space:nowrap}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static;float:none}.navbar-text{display:inline-block;padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem}.navbar-toggler:hover,.navbar-toggler:focus{text-decoration:none}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;content:"";background:no-repeat center center;background-size:100% 100%}@media (max-width: 575.98px){.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 576px){.navbar-expand-sm{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm>.container,.navbar-expand-sm>.container-fluid,.navbar-expand-sm>.container-sm,.navbar-expand-sm>.container-md,.navbar-expand-sm>.container-lg,.navbar-expand-sm>.container-xl{flex-wrap:nowrap}.navbar-expand-sm .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}}@media (max-width: 767.98px){.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 768px){.navbar-expand-md{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md>.container,.navbar-expand-md>.container-fluid,.navbar-expand-md>.container-sm,.navbar-expand-md>.container-md,.navbar-expand-md>.container-lg,.navbar-expand-md>.container-xl{flex-wrap:nowrap}.navbar-expand-md .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}}@media (max-width: 991.98px){.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 992px){.navbar-expand-lg{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg>.container,.navbar-expand-lg>.container-fluid,.navbar-expand-lg>.container-sm,.navbar-expand-lg>.container-md,.navbar-expand-lg>.container-lg,.navbar-expand-lg>.container-xl{flex-wrap:nowrap}.navbar-expand-lg .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}}@media (max-width: 1199.98px){.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{padding-right:0;padding-left:0}}@media (min-width: 1200px){.navbar-expand-xl{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl>.container,.navbar-expand-xl>.container-fluid,.navbar-expand-xl>.container-sm,.navbar-expand-xl>.container-md,.navbar-expand-xl>.container-lg,.navbar-expand-xl>.container-xl{flex-wrap:nowrap}.navbar-expand-xl .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}}.navbar-expand{flex-flow:row nowrap;justify-content:flex-start}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{padding-right:0;padding-left:0}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand>.container,.navbar-expand>.container-fluid,.navbar-expand>.container-sm,.navbar-expand>.container-md,.navbar-expand>.container-lg,.navbar-expand>.container-xl{flex-wrap:nowrap}.navbar-expand .navbar-collapse{display:flex !important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-light .navbar-brand{color:rgba(0,0,0,0.9)}.navbar-light .navbar-brand:hover,.navbar-light .navbar-brand:focus{color:rgba(0,0,0,0.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,0.5)}.navbar-light .navbar-nav .nav-link:hover,.navbar-light .navbar-nav .nav-link:focus{color:rgba(0,0,0,0.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,0.3)}.navbar-light .navbar-nav .show>.nav-link,.navbar-light .navbar-nav .active>.nav-link,.navbar-light .navbar-nav .nav-link.show,.navbar-light .navbar-nav .nav-link.active{color:rgba(0,0,0,0.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,0.5);border-color:rgba(0,0,0,0.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280,0,0,0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,0.5)}.navbar-light .navbar-text a{color:rgba(0,0,0,0.9)}.navbar-light .navbar-text a:hover,.navbar-light .navbar-text a:focus{color:rgba(0,0,0,0.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:hover,.navbar-dark .navbar-brand:focus{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-nav .nav-link:hover,.navbar-dark .navbar-nav .nav-link:focus{color:rgba(255,255,255,0.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,0.25)}.navbar-dark .navbar-nav .show>.nav-link,.navbar-dark .navbar-nav .active>.nav-link,.navbar-dark .navbar-nav .nav-link.show,.navbar-dark .navbar-nav .nav-link.active{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,0.5);border-color:rgba(255,255,255,0.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='30' height='30' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255,255,255,0.5%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,0.5)}.navbar-dark .navbar-text a{color:#fff}.navbar-dark .navbar-text a:hover,.navbar-dark .navbar-text a:focus{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,0.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-body{flex:1 1 auto;min-height:1px;padding:1.25rem}.card-title{margin-bottom:.75rem}.card-subtitle{margin-top:-.375rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link:hover{text-decoration:none}.card-link+.card-link{margin-left:1.25rem}.card-header{padding:.75rem 1.25rem;margin-bottom:0;background-color:rgba(0,0,0,0.03);border-bottom:1px solid rgba(0,0,0,0.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-header+.list-group .list-group-item:first-child{border-top:0}.card-footer{padding:.75rem 1.25rem;background-color:rgba(0,0,0,0.03);border-top:1px solid rgba(0,0,0,0.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.625rem;margin-bottom:-.75rem;margin-left:-.625rem;border-bottom:0}.card-header-pills{margin-right:-.625rem;margin-left:-.625rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1.25rem}.card-img,.card-img-top,.card-img-bottom{flex-shrink:0;width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-deck .card{margin-bottom:15px}@media (min-width: 576px){.card-deck{display:flex;flex-flow:row wrap;margin-right:-15px;margin-left:-15px}.card-deck .card{flex:1 0 0%;margin-right:15px;margin-bottom:0;margin-left:15px}}.card-group>.card{margin-bottom:15px}@media (min-width: 576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-img-top,.card-group>.card:not(:last-child) .card-header{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-img-bottom,.card-group>.card:not(:last-child) .card-footer{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-img-top,.card-group>.card:not(:first-child) .card-header{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-img-bottom,.card-group>.card:not(:first-child) .card-footer{border-bottom-left-radius:0}}.card-columns .card{margin-bottom:.75rem}@media (min-width: 576px){.card-columns{column-count:3;column-gap:1.25rem;orphans:1;widows:1}.card-columns .card{display:inline-block;width:100%}}.accordion>.card{overflow:hidden}.accordion>.card:not(:last-of-type){border-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.accordion>.card:not(:first-of-type){border-top-left-radius:0;border-top-right-radius:0}.accordion>.card>.card-header{border-radius:0;margin-bottom:-1px}.breadcrumb{display:flex;flex-wrap:wrap;padding:.75rem 1rem;margin-bottom:1rem;list-style:none;background-color:#e9ecef;border-radius:.25rem}.breadcrumb-item{display:flex}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{display:inline-block;padding-right:.5rem;color:#6c757d;content:"/"}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:underline}.breadcrumb-item+.breadcrumb-item:hover::before{text-decoration:none}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none;border-radius:.25rem}.page-link{position:relative;display:block;padding:.5rem .75rem;margin-left:-1px;line-height:1.25;color:#1f80f9;background-color:#fff;border:1px solid #dee2e6}.page-link:hover{z-index:2;color:#055bc6;text-decoration:none;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;outline:0;box-shadow:0 0 0 .2rem rgba(31,128,249,0.25)}.page-item:first-child .page-link{margin-left:0;border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.page-item.active .page-link{z-index:3;color:#fff;background-color:#1f80f9;border-color:#1f80f9}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;cursor:auto;background-color:#fff;border-color:#dee2e6}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem;line-height:1.5}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem;line-height:1.5}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.25em .4em;font-size:75%;font-weight:700;line-height:1;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem;transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out}@media (prefers-reduced-motion: reduce){.badge{transition:none}}a.badge:hover,a.badge:focus{text-decoration:none}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.badge-pill{padding-right:.6em;padding-left:.6em;border-radius:10rem}.badge-primary{color:#fff;background-color:#1f80f9}a.badge-primary:hover,a.badge-primary:focus{color:#fff;background-color:#0667df}a.badge-primary:focus,a.badge-primary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(31,128,249,0.5)}.badge-secondary{color:#fff;background-color:#206ed5}a.badge-secondary:hover,a.badge-secondary:focus{color:#fff;background-color:#1957a9}a.badge-secondary:focus,a.badge-secondary.focus{outline:0;box-shadow:0 0 0 .2rem rgba(32,110,213,0.5)}.badge-success{color:#fff;background-color:#297ac2}a.badge-success:hover,a.badge-success:focus{color:#fff;background-color:#206098}a.badge-success:focus,a.badge-success.focus{outline:0;box-shadow:0 0 0 .2rem rgba(41,122,194,0.5)}.badge-info{color:#fff;background-color:#456bc4}a.badge-info:hover,a.badge-info:focus{color:#fff;background-color:#3455a2}a.badge-info:focus,a.badge-info.focus{outline:0;box-shadow:0 0 0 .2rem rgba(69,107,196,0.5)}.badge-warning{color:#fff;background-color:#d15f42}a.badge-warning:hover,a.badge-warning:focus{color:#fff;background-color:#b4472c}a.badge-warning:focus,a.badge-warning.focus{outline:0;box-shadow:0 0 0 .2rem rgba(209,95,66,0.5)}.badge-danger{color:#fff;background-color:#c93831}a.badge-danger:hover,a.badge-danger:focus{color:#fff;background-color:#a02d27}a.badge-danger:focus,a.badge-danger.focus{outline:0;box-shadow:0 0 0 .2rem rgba(201,56,49,0.5)}.badge-light{color:#212529;background-color:#eceeec}a.badge-light:hover,a.badge-light:focus{color:#212529;background-color:#d1d6d1}a.badge-light:focus,a.badge-light.focus{outline:0;box-shadow:0 0 0 .2rem rgba(236,238,236,0.5)}.badge-dark{color:#fff;background-color:#1e2b37}a.badge-dark:hover,a.badge-dark:focus{color:#fff;background-color:#0c1116}a.badge-dark:focus,a.badge-dark.focus{outline:0;box-shadow:0 0 0 .2rem rgba(30,43,55,0.5)}.jumbotron{padding:2rem 1rem;margin-bottom:2rem;background-color:#e9ecef;border-radius:.3rem}@media (min-width: 576px){.jumbotron{padding:4rem 2rem}}.jumbotron-fluid{padding-right:0;padding-left:0;border-radius:0}.alert{position:relative;padding:.75rem 1.25rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:4rem}.alert-dismissible .close{position:absolute;top:0;right:0;padding:.75rem 1.25rem;color:inherit}.alert-primary{color:#104381;background-color:#d2e6fe;border-color:#c0dbfd}.alert-primary hr{border-top-color:#a7cdfc}.alert-primary .alert-link{color:#0a2b54}.alert-secondary{color:#11396f;background-color:#d2e2f7;border-color:#c1d6f3}.alert-secondary hr{border-top-color:#acc8ef}.alert-secondary .alert-link{color:#0a2243}.alert-success{color:#153f65;background-color:#d4e4f3;border-color:#c3daee}.alert-success hr{border-top-color:#afcee8}.alert-success .alert-link{color:#0c253b}.alert-info{color:#243866;background-color:#dae1f3;border-color:#cbd6ee}.alert-info hr{border-top-color:#b8c7e8}.alert-info .alert-link{color:#172340}.alert-warning{color:#6d3122;background-color:#f6dfd9;border-color:#f2d2ca}.alert-warning hr{border-top-color:#edc1b6}.alert-warning .alert-link{color:#462016}.alert-danger{color:#691d19;background-color:#f4d7d6;border-color:#f0c7c5}.alert-danger hr{border-top-color:#ebb3b1}.alert-danger .alert-link{color:#40120f}.alert-light{color:#7b7c7b;background-color:#fbfcfb;border-color:#fafafa}.alert-light hr{border-top-color:#ededed}.alert-light .alert-link{color:#626262}.alert-dark{color:#10161d;background-color:#d2d5d7;border-color:#c0c4c7}.alert-dark hr{border-top-color:#b3b7bb}.alert-dark .alert-link{color:#000}@keyframes progress-bar-stripes{from{background-position:1rem 0}to{background-position:0 0}}.progress{display:flex;height:1rem;overflow:hidden;line-height:0;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#1f80f9;transition:width 0.6s ease}@media (prefers-reduced-motion: reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:1rem 1rem}.progress-bar-animated{animation:progress-bar-stripes 1s linear infinite}@media (prefers-reduced-motion: reduce){.progress-bar-animated{animation:none}}.media{display:flex;align-items:flex-start}.media-body{flex:1}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:hover,.list-group-item-action:focus{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.75rem 1.25rem;background-color:#fff;border:1px solid rgba(0,0,0,0.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#1f80f9;border-color:#1f80f9}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width: 576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width: 1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#104381;background-color:#c0dbfd}.list-group-item-primary.list-group-item-action:hover,.list-group-item-primary.list-group-item-action:focus{color:#104381;background-color:#a7cdfc}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#104381;border-color:#104381}.list-group-item-secondary{color:#11396f;background-color:#c1d6f3}.list-group-item-secondary.list-group-item-action:hover,.list-group-item-secondary.list-group-item-action:focus{color:#11396f;background-color:#acc8ef}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#11396f;border-color:#11396f}.list-group-item-success{color:#153f65;background-color:#c3daee}.list-group-item-success.list-group-item-action:hover,.list-group-item-success.list-group-item-action:focus{color:#153f65;background-color:#afcee8}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#153f65;border-color:#153f65}.list-group-item-info{color:#243866;background-color:#cbd6ee}.list-group-item-info.list-group-item-action:hover,.list-group-item-info.list-group-item-action:focus{color:#243866;background-color:#b8c7e8}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#243866;border-color:#243866}.list-group-item-warning{color:#6d3122;background-color:#f2d2ca}.list-group-item-warning.list-group-item-action:hover,.list-group-item-warning.list-group-item-action:focus{color:#6d3122;background-color:#edc1b6}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#6d3122;border-color:#6d3122}.list-group-item-danger{color:#691d19;background-color:#f0c7c5}.list-group-item-danger.list-group-item-action:hover,.list-group-item-danger.list-group-item-action:focus{color:#691d19;background-color:#ebb3b1}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#691d19;border-color:#691d19}.list-group-item-light{color:#7b7c7b;background-color:#fafafa}.list-group-item-light.list-group-item-action:hover,.list-group-item-light.list-group-item-action:focus{color:#7b7c7b;background-color:#ededed}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#7b7c7b;border-color:#7b7c7b}.list-group-item-dark{color:#10161d;background-color:#c0c4c7}.list-group-item-dark.list-group-item-action:hover,.list-group-item-dark.list-group-item-action:focus{color:#10161d;background-color:#b3b7bb}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#10161d;border-color:#10161d}.close{float:right;font-size:1.5rem;font-weight:700;line-height:1;color:#000;text-shadow:0 1px 0 #fff;opacity:.5}.close:hover{color:#000;text-decoration:none}.close:not(:disabled):not(.disabled):hover,.close:not(:disabled):not(.disabled):focus{opacity:.75}button.close{padding:0;background-color:transparent;border:0}a.close.disabled{pointer-events:none}.toast{max-width:350px;overflow:hidden;font-size:.875rem;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border:1px solid rgba(0,0,0,0.1);box-shadow:0 0.25rem 0.75rem rgba(0,0,0,0.1);backdrop-filter:blur(10px);opacity:0;border-radius:.25rem}.toast:not(:last-child){margin-bottom:.75rem}.toast.showing{opacity:1}.toast.show{display:block;opacity:1}.toast.hide{display:none}.toast-header{display:flex;align-items:center;padding:.25rem .75rem;color:#6c757d;background-color:rgba(255,255,255,0.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,0.05)}.toast-body{padding:.75rem}.modal-open{overflow:hidden}.modal-open .modal{overflow-x:hidden;overflow-y:auto}.modal{position:fixed;top:0;left:0;z-index:1050;display:none;width:100%;height:100%;overflow:hidden;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform 0.3s ease-out;transform:translate(0, -50px)}@media (prefers-reduced-motion: reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{display:flex;max-height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 1rem);overflow:hidden}.modal-dialog-scrollable .modal-header,.modal-dialog-scrollable .modal-footer{flex-shrink:0}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-dialog-centered::before{display:block;height:calc(100vh - 1rem);height:min-content;content:""}.modal-dialog-centered.modal-dialog-scrollable{flex-direction:column;justify-content:center;height:100%}.modal-dialog-centered.modal-dialog-scrollable .modal-content{max-height:none}.modal-dialog-centered.modal-dialog-scrollable::before{content:none}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;align-items:flex-start;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .close{padding:1rem 1rem;margin:-1rem -1rem -1rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}.modal-scrollbar-measure{position:absolute;top:-9999px;width:50px;height:50px;overflow:scroll}@media (min-width: 576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{max-height:calc(100% - 3.5rem)}.modal-dialog-scrollable .modal-content{max-height:calc(100vh - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-dialog-centered::before{height:calc(100vh - 3.5rem);height:min-content}.modal-sm{max-width:300px}}@media (min-width: 992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width: 1200px){.modal-xl{max-width:1140px}}.tooltip{position:absolute;z-index:1070;display:block;margin:0;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-top,.bs-tooltip-auto[x-placement^="top"]{padding:.4rem 0}.bs-tooltip-top .arrow,.bs-tooltip-auto[x-placement^="top"] .arrow{bottom:0}.bs-tooltip-top .arrow::before,.bs-tooltip-auto[x-placement^="top"] .arrow::before{top:0;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-right,.bs-tooltip-auto[x-placement^="right"]{padding:0 .4rem}.bs-tooltip-right .arrow,.bs-tooltip-auto[x-placement^="right"] .arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-right .arrow::before,.bs-tooltip-auto[x-placement^="right"] .arrow::before{right:0;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-bottom,.bs-tooltip-auto[x-placement^="bottom"]{padding:.4rem 0}.bs-tooltip-bottom .arrow,.bs-tooltip-auto[x-placement^="bottom"] .arrow{top:0}.bs-tooltip-bottom .arrow::before,.bs-tooltip-auto[x-placement^="bottom"] .arrow::before{bottom:0;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-left,.bs-tooltip-auto[x-placement^="left"]{padding:0 .4rem}.bs-tooltip-left .arrow,.bs-tooltip-auto[x-placement^="left"] .arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-left .arrow::before,.bs-tooltip-auto[x-placement^="left"] .arrow::before{left:0;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1060;display:block;max-width:276px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,0.2);border-radius:.3rem}.popover .arrow{position:absolute;display:block;width:1rem;height:.5rem;margin:0 .3rem}.popover .arrow::before,.popover .arrow::after{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-top,.bs-popover-auto[x-placement^="top"]{margin-bottom:.5rem}.bs-popover-top>.arrow,.bs-popover-auto[x-placement^="top"]>.arrow{bottom:calc(-.5rem - 1px)}.bs-popover-top>.arrow::before,.bs-popover-auto[x-placement^="top"]>.arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,0.25)}.bs-popover-top>.arrow::after,.bs-popover-auto[x-placement^="top"]>.arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-right,.bs-popover-auto[x-placement^="right"]{margin-left:.5rem}.bs-popover-right>.arrow,.bs-popover-auto[x-placement^="right"]>.arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-right>.arrow::before,.bs-popover-auto[x-placement^="right"]>.arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,0.25)}.bs-popover-right>.arrow::after,.bs-popover-auto[x-placement^="right"]>.arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-bottom,.bs-popover-auto[x-placement^="bottom"]{margin-top:.5rem}.bs-popover-bottom>.arrow,.bs-popover-auto[x-placement^="bottom"]>.arrow{top:calc(-.5rem - 1px)}.bs-popover-bottom>.arrow::before,.bs-popover-auto[x-placement^="bottom"]>.arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,0.25)}.bs-popover-bottom>.arrow::after,.bs-popover-auto[x-placement^="bottom"]>.arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-bottom .popover-header::before,.bs-popover-auto[x-placement^="bottom"] .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f7f7f7}.bs-popover-left,.bs-popover-auto[x-placement^="left"]{margin-right:.5rem}.bs-popover-left>.arrow,.bs-popover-auto[x-placement^="left"]>.arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem;margin:.3rem 0}.bs-popover-left>.arrow::before,.bs-popover-auto[x-placement^="left"]>.arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,0.25)}.bs-popover-left>.arrow::after,.bs-popover-auto[x-placement^="left"]>.arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem .75rem;margin-bottom:0;font-size:1rem;background-color:#f7f7f7;border-bottom:1px solid #ebebeb;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:.5rem .75rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion: reduce){.carousel-item{transition:none}}.carousel-item.active,.carousel-item-next,.carousel-item-prev{display:block}.carousel-item-next:not(.carousel-item-left),.active.carousel-item-right{transform:translateX(100%)}.carousel-item-prev:not(.carousel-item-right),.active.carousel-item-left{transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item.active,.carousel-fade .carousel-item-next.carousel-item-left,.carousel-fade .carousel-item-prev.carousel-item-right{z-index:1;opacity:1}.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion: reduce){.carousel-fade .active.carousel-item-left,.carousel-fade .active.carousel-item-right{transition:none}}.carousel-control-prev,.carousel-control-next{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;color:#fff;text-align:center;opacity:.5;transition:opacity 0.15s ease}@media (prefers-reduced-motion: reduce){.carousel-control-prev,.carousel-control-next{transition:none}}.carousel-control-prev:hover,.carousel-control-prev:focus,.carousel-control-next:hover,.carousel-control-next:focus{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-prev-icon,.carousel-control-next-icon{display:inline-block;width:20px;height:20px;background:no-repeat 50% / 100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M5.25 0l-4 4 4 4 1.5-1.5L4.25 4l2.5-2.5L5.25 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath d='M2.75 0l-1.5 1.5L3.75 4l-2.5 2.5L2.75 8l4-4-4-4z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:15;display:flex;justify-content:center;padding-left:0;margin-right:15%;margin-left:15%;list-style:none}.carousel-indicators li{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity 0.6s ease}@media (prefers-reduced-motion: reduce){.carousel-indicators li{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:20px;left:15%;z-index:10;padding-top:20px;padding-bottom:20px;color:#fff;text-align:center}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;animation:spinner-border .75s linear infinite}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:text-bottom;background-color:currentColor;border-radius:50%;opacity:0;animation:spinner-grow .75s linear infinite}.spinner-grow-sm{width:1rem;height:1rem}.align-baseline{vertical-align:baseline !important}.align-top{vertical-align:top !important}.align-middle{vertical-align:middle !important}.align-bottom{vertical-align:bottom !important}.align-text-bottom{vertical-align:text-bottom !important}.align-text-top{vertical-align:text-top !important}.bg-primary{background-color:#1f80f9 !important}a.bg-primary:hover,a.bg-primary:focus,button.bg-primary:hover,button.bg-primary:focus{background-color:#0667df !important}.bg-secondary{background-color:#206ed5 !important}a.bg-secondary:hover,a.bg-secondary:focus,button.bg-secondary:hover,button.bg-secondary:focus{background-color:#1957a9 !important}.bg-success{background-color:#297ac2 !important}a.bg-success:hover,a.bg-success:focus,button.bg-success:hover,button.bg-success:focus{background-color:#206098 !important}.bg-info{background-color:#456bc4 !important}a.bg-info:hover,a.bg-info:focus,button.bg-info:hover,button.bg-info:focus{background-color:#3455a2 !important}.bg-warning{background-color:#d15f42 !important}a.bg-warning:hover,a.bg-warning:focus,button.bg-warning:hover,button.bg-warning:focus{background-color:#b4472c !important}.bg-danger{background-color:#c93831 !important}a.bg-danger:hover,a.bg-danger:focus,button.bg-danger:hover,button.bg-danger:focus{background-color:#a02d27 !important}.bg-light{background-color:#eceeec !important}a.bg-light:hover,a.bg-light:focus,button.bg-light:hover,button.bg-light:focus{background-color:#d1d6d1 !important}.bg-dark{background-color:#1e2b37 !important}a.bg-dark:hover,a.bg-dark:focus,button.bg-dark:hover,button.bg-dark:focus{background-color:#0c1116 !important}.bg-white{background-color:#fff !important}.bg-transparent{background-color:transparent !important}.border{border:1px solid #dee2e6 !important}.border-top{border-top:1px solid #dee2e6 !important}.border-right{border-right:1px solid #dee2e6 !important}.border-bottom{border-bottom:1px solid #dee2e6 !important}.border-left{border-left:1px solid #dee2e6 !important}.border-0{border:0 !important}.border-top-0{border-top:0 !important}.border-right-0{border-right:0 !important}.border-bottom-0{border-bottom:0 !important}.border-left-0{border-left:0 !important}.border-primary{border-color:#1f80f9 !important}.border-secondary{border-color:#206ed5 !important}.border-success{border-color:#297ac2 !important}.border-info{border-color:#456bc4 !important}.border-warning{border-color:#d15f42 !important}.border-danger{border-color:#c93831 !important}.border-light{border-color:#eceeec !important}.border-dark{border-color:#1e2b37 !important}.border-white{border-color:#fff !important}.rounded-sm{border-radius:.2rem !important}.rounded{border-radius:.25rem !important}.rounded-top{border-top-left-radius:.25rem !important;border-top-right-radius:.25rem !important}.rounded-right{border-top-right-radius:.25rem !important;border-bottom-right-radius:.25rem !important}.rounded-bottom{border-bottom-right-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-left{border-top-left-radius:.25rem !important;border-bottom-left-radius:.25rem !important}.rounded-lg{border-radius:.3rem !important}.rounded-circle{border-radius:50% !important}.rounded-pill{border-radius:50rem !important}.rounded-0{border-radius:0 !important}.clearfix::after{display:block;clear:both;content:""}.d-none{display:none !important}.d-inline{display:inline !important}.d-inline-block{display:inline-block !important}.d-block{display:block !important}.d-table{display:table !important}.d-table-row{display:table-row !important}.d-table-cell{display:table-cell !important}.d-flex{display:flex !important}.d-inline-flex{display:inline-flex !important}@media (min-width: 576px){.d-sm-none{display:none !important}.d-sm-inline{display:inline !important}.d-sm-inline-block{display:inline-block !important}.d-sm-block{display:block !important}.d-sm-table{display:table !important}.d-sm-table-row{display:table-row !important}.d-sm-table-cell{display:table-cell !important}.d-sm-flex{display:flex !important}.d-sm-inline-flex{display:inline-flex !important}}@media (min-width: 768px){.d-md-none{display:none !important}.d-md-inline{display:inline !important}.d-md-inline-block{display:inline-block !important}.d-md-block{display:block !important}.d-md-table{display:table !important}.d-md-table-row{display:table-row !important}.d-md-table-cell{display:table-cell !important}.d-md-flex{display:flex !important}.d-md-inline-flex{display:inline-flex !important}}@media (min-width: 992px){.d-lg-none{display:none !important}.d-lg-inline{display:inline !important}.d-lg-inline-block{display:inline-block !important}.d-lg-block{display:block !important}.d-lg-table{display:table !important}.d-lg-table-row{display:table-row !important}.d-lg-table-cell{display:table-cell !important}.d-lg-flex{display:flex !important}.d-lg-inline-flex{display:inline-flex !important}}@media (min-width: 1200px){.d-xl-none{display:none !important}.d-xl-inline{display:inline !important}.d-xl-inline-block{display:inline-block !important}.d-xl-block{display:block !important}.d-xl-table{display:table !important}.d-xl-table-row{display:table-row !important}.d-xl-table-cell{display:table-cell !important}.d-xl-flex{display:flex !important}.d-xl-inline-flex{display:inline-flex !important}}@media print{.d-print-none{display:none !important}.d-print-inline{display:inline !important}.d-print-inline-block{display:inline-block !important}.d-print-block{display:block !important}.d-print-table{display:table !important}.d-print-table-row{display:table-row !important}.d-print-table-cell{display:table-cell !important}.d-print-flex{display:flex !important}.d-print-inline-flex{display:inline-flex !important}}.embed-responsive{position:relative;display:block;width:100%;padding:0;overflow:hidden}.embed-responsive::before{display:block;content:""}.embed-responsive .embed-responsive-item,.embed-responsive iframe,.embed-responsive embed,.embed-responsive object,.embed-responsive video{position:absolute;top:0;bottom:0;left:0;width:100%;height:100%;border:0}.embed-responsive-21by9::before{padding-top:42.85714%}.embed-responsive-16by9::before{padding-top:56.25%}.embed-responsive-4by3::before{padding-top:75%}.embed-responsive-1by1::before{padding-top:100%}.flex-row{flex-direction:row !important}.flex-column{flex-direction:column !important}.flex-row-reverse{flex-direction:row-reverse !important}.flex-column-reverse{flex-direction:column-reverse !important}.flex-wrap{flex-wrap:wrap !important}.flex-nowrap{flex-wrap:nowrap !important}.flex-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-fill{flex:1 1 auto !important}.flex-grow-0{flex-grow:0 !important}.flex-grow-1{flex-grow:1 !important}.flex-shrink-0{flex-shrink:0 !important}.flex-shrink-1{flex-shrink:1 !important}.justify-content-start{justify-content:flex-start !important}.justify-content-end{justify-content:flex-end !important}.justify-content-center{justify-content:center !important}.justify-content-between{justify-content:space-between !important}.justify-content-around{justify-content:space-around !important}.align-items-start{align-items:flex-start !important}.align-items-end{align-items:flex-end !important}.align-items-center{align-items:center !important}.align-items-baseline{align-items:baseline !important}.align-items-stretch{align-items:stretch !important}.align-content-start{align-content:flex-start !important}.align-content-end{align-content:flex-end !important}.align-content-center{align-content:center !important}.align-content-between{align-content:space-between !important}.align-content-around{align-content:space-around !important}.align-content-stretch{align-content:stretch !important}.align-self-auto{align-self:auto !important}.align-self-start{align-self:flex-start !important}.align-self-end{align-self:flex-end !important}.align-self-center{align-self:center !important}.align-self-baseline{align-self:baseline !important}.align-self-stretch{align-self:stretch !important}@media (min-width: 576px){.flex-sm-row{flex-direction:row !important}.flex-sm-column{flex-direction:column !important}.flex-sm-row-reverse{flex-direction:row-reverse !important}.flex-sm-column-reverse{flex-direction:column-reverse !important}.flex-sm-wrap{flex-wrap:wrap !important}.flex-sm-nowrap{flex-wrap:nowrap !important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-sm-fill{flex:1 1 auto !important}.flex-sm-grow-0{flex-grow:0 !important}.flex-sm-grow-1{flex-grow:1 !important}.flex-sm-shrink-0{flex-shrink:0 !important}.flex-sm-shrink-1{flex-shrink:1 !important}.justify-content-sm-start{justify-content:flex-start !important}.justify-content-sm-end{justify-content:flex-end !important}.justify-content-sm-center{justify-content:center !important}.justify-content-sm-between{justify-content:space-between !important}.justify-content-sm-around{justify-content:space-around !important}.align-items-sm-start{align-items:flex-start !important}.align-items-sm-end{align-items:flex-end !important}.align-items-sm-center{align-items:center !important}.align-items-sm-baseline{align-items:baseline !important}.align-items-sm-stretch{align-items:stretch !important}.align-content-sm-start{align-content:flex-start !important}.align-content-sm-end{align-content:flex-end !important}.align-content-sm-center{align-content:center !important}.align-content-sm-between{align-content:space-between !important}.align-content-sm-around{align-content:space-around !important}.align-content-sm-stretch{align-content:stretch !important}.align-self-sm-auto{align-self:auto !important}.align-self-sm-start{align-self:flex-start !important}.align-self-sm-end{align-self:flex-end !important}.align-self-sm-center{align-self:center !important}.align-self-sm-baseline{align-self:baseline !important}.align-self-sm-stretch{align-self:stretch !important}}@media (min-width: 768px){.flex-md-row{flex-direction:row !important}.flex-md-column{flex-direction:column !important}.flex-md-row-reverse{flex-direction:row-reverse !important}.flex-md-column-reverse{flex-direction:column-reverse !important}.flex-md-wrap{flex-wrap:wrap !important}.flex-md-nowrap{flex-wrap:nowrap !important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-md-fill{flex:1 1 auto !important}.flex-md-grow-0{flex-grow:0 !important}.flex-md-grow-1{flex-grow:1 !important}.flex-md-shrink-0{flex-shrink:0 !important}.flex-md-shrink-1{flex-shrink:1 !important}.justify-content-md-start{justify-content:flex-start !important}.justify-content-md-end{justify-content:flex-end !important}.justify-content-md-center{justify-content:center !important}.justify-content-md-between{justify-content:space-between !important}.justify-content-md-around{justify-content:space-around !important}.align-items-md-start{align-items:flex-start !important}.align-items-md-end{align-items:flex-end !important}.align-items-md-center{align-items:center !important}.align-items-md-baseline{align-items:baseline !important}.align-items-md-stretch{align-items:stretch !important}.align-content-md-start{align-content:flex-start !important}.align-content-md-end{align-content:flex-end !important}.align-content-md-center{align-content:center !important}.align-content-md-between{align-content:space-between !important}.align-content-md-around{align-content:space-around !important}.align-content-md-stretch{align-content:stretch !important}.align-self-md-auto{align-self:auto !important}.align-self-md-start{align-self:flex-start !important}.align-self-md-end{align-self:flex-end !important}.align-self-md-center{align-self:center !important}.align-self-md-baseline{align-self:baseline !important}.align-self-md-stretch{align-self:stretch !important}}@media (min-width: 992px){.flex-lg-row{flex-direction:row !important}.flex-lg-column{flex-direction:column !important}.flex-lg-row-reverse{flex-direction:row-reverse !important}.flex-lg-column-reverse{flex-direction:column-reverse !important}.flex-lg-wrap{flex-wrap:wrap !important}.flex-lg-nowrap{flex-wrap:nowrap !important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-lg-fill{flex:1 1 auto !important}.flex-lg-grow-0{flex-grow:0 !important}.flex-lg-grow-1{flex-grow:1 !important}.flex-lg-shrink-0{flex-shrink:0 !important}.flex-lg-shrink-1{flex-shrink:1 !important}.justify-content-lg-start{justify-content:flex-start !important}.justify-content-lg-end{justify-content:flex-end !important}.justify-content-lg-center{justify-content:center !important}.justify-content-lg-between{justify-content:space-between !important}.justify-content-lg-around{justify-content:space-around !important}.align-items-lg-start{align-items:flex-start !important}.align-items-lg-end{align-items:flex-end !important}.align-items-lg-center{align-items:center !important}.align-items-lg-baseline{align-items:baseline !important}.align-items-lg-stretch{align-items:stretch !important}.align-content-lg-start{align-content:flex-start !important}.align-content-lg-end{align-content:flex-end !important}.align-content-lg-center{align-content:center !important}.align-content-lg-between{align-content:space-between !important}.align-content-lg-around{align-content:space-around !important}.align-content-lg-stretch{align-content:stretch !important}.align-self-lg-auto{align-self:auto !important}.align-self-lg-start{align-self:flex-start !important}.align-self-lg-end{align-self:flex-end !important}.align-self-lg-center{align-self:center !important}.align-self-lg-baseline{align-self:baseline !important}.align-self-lg-stretch{align-self:stretch !important}}@media (min-width: 1200px){.flex-xl-row{flex-direction:row !important}.flex-xl-column{flex-direction:column !important}.flex-xl-row-reverse{flex-direction:row-reverse !important}.flex-xl-column-reverse{flex-direction:column-reverse !important}.flex-xl-wrap{flex-wrap:wrap !important}.flex-xl-nowrap{flex-wrap:nowrap !important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse !important}.flex-xl-fill{flex:1 1 auto !important}.flex-xl-grow-0{flex-grow:0 !important}.flex-xl-grow-1{flex-grow:1 !important}.flex-xl-shrink-0{flex-shrink:0 !important}.flex-xl-shrink-1{flex-shrink:1 !important}.justify-content-xl-start{justify-content:flex-start !important}.justify-content-xl-end{justify-content:flex-end !important}.justify-content-xl-center{justify-content:center !important}.justify-content-xl-between{justify-content:space-between !important}.justify-content-xl-around{justify-content:space-around !important}.align-items-xl-start{align-items:flex-start !important}.align-items-xl-end{align-items:flex-end !important}.align-items-xl-center{align-items:center !important}.align-items-xl-baseline{align-items:baseline !important}.align-items-xl-stretch{align-items:stretch !important}.align-content-xl-start{align-content:flex-start !important}.align-content-xl-end{align-content:flex-end !important}.align-content-xl-center{align-content:center !important}.align-content-xl-between{align-content:space-between !important}.align-content-xl-around{align-content:space-around !important}.align-content-xl-stretch{align-content:stretch !important}.align-self-xl-auto{align-self:auto !important}.align-self-xl-start{align-self:flex-start !important}.align-self-xl-end{align-self:flex-end !important}.align-self-xl-center{align-self:center !important}.align-self-xl-baseline{align-self:baseline !important}.align-self-xl-stretch{align-self:stretch !important}}.float-left{float:left !important}.float-right{float:right !important}.float-none{float:none !important}@media (min-width: 576px){.float-sm-left{float:left !important}.float-sm-right{float:right !important}.float-sm-none{float:none !important}}@media (min-width: 768px){.float-md-left{float:left !important}.float-md-right{float:right !important}.float-md-none{float:none !important}}@media (min-width: 992px){.float-lg-left{float:left !important}.float-lg-right{float:right !important}.float-lg-none{float:none !important}}@media (min-width: 1200px){.float-xl-left{float:left !important}.float-xl-right{float:right !important}.float-xl-none{float:none !important}}.user-select-all{user-select:all !important}.user-select-auto{user-select:auto !important}.user-select-none{user-select:none !important}.overflow-auto{overflow:auto !important}.overflow-hidden{overflow:hidden !important}.position-static{position:static !important}.position-relative{position:relative !important}.position-absolute{position:absolute !important}.position-fixed{position:fixed !important}.position-sticky{position:sticky !important}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}@supports (position: sticky){.sticky-top{position:sticky;top:0;z-index:1020}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;overflow:visible;clip:auto;white-space:normal}.shadow-sm{box-shadow:0 0.125rem 0.25rem rgba(0,0,0,0.075) !important}.shadow{box-shadow:0 0.5rem 1rem rgba(0,0,0,0.15) !important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,0.175) !important}.shadow-none{box-shadow:none !important}.w-25{width:25% !important}.w-50{width:50% !important}.w-75{width:75% !important}.w-100{width:100% !important}.w-auto{width:auto !important}.h-25{height:25% !important}.h-50{height:50% !important}.h-75{height:75% !important}.h-100{height:100% !important}.h-auto{height:auto !important}.mw-100{max-width:100% !important}.mh-100{max-height:100% !important}.min-vw-100{min-width:100vw !important}.min-vh-100{min-height:100vh !important}.vw-100{width:100vw !important}.vh-100{height:100vh !important}.m-0{margin:0 !important}.mt-0,.my-0{margin-top:0 !important}.mr-0,.mx-0{margin-right:0 !important}.mb-0,.my-0{margin-bottom:0 !important}.ml-0,.mx-0{margin-left:0 !important}.m-1{margin:.25rem !important}.mt-1,.my-1{margin-top:.25rem !important}.mr-1,.mx-1{margin-right:.25rem !important}.mb-1,.my-1{margin-bottom:.25rem !important}.ml-1,.mx-1{margin-left:.25rem !important}.m-2{margin:.5rem !important}.mt-2,.my-2{margin-top:.5rem !important}.mr-2,.mx-2{margin-right:.5rem !important}.mb-2,.my-2{margin-bottom:.5rem !important}.ml-2,.mx-2{margin-left:.5rem !important}.m-3{margin:1rem !important}.mt-3,.my-3{margin-top:1rem !important}.mr-3,.mx-3{margin-right:1rem !important}.mb-3,.my-3{margin-bottom:1rem !important}.ml-3,.mx-3{margin-left:1rem !important}.m-4{margin:1.5rem !important}.mt-4,.my-4{margin-top:1.5rem !important}.mr-4,.mx-4{margin-right:1.5rem !important}.mb-4,.my-4{margin-bottom:1.5rem !important}.ml-4,.mx-4{margin-left:1.5rem !important}.m-5{margin:3rem !important}.mt-5,.my-5{margin-top:3rem !important}.mr-5,.mx-5{margin-right:3rem !important}.mb-5,.my-5{margin-bottom:3rem !important}.ml-5,.mx-5{margin-left:3rem !important}.p-0{padding:0 !important}.pt-0,.py-0{padding-top:0 !important}.pr-0,.px-0{padding-right:0 !important}.pb-0,.py-0{padding-bottom:0 !important}.pl-0,.px-0{padding-left:0 !important}.p-1{padding:.25rem !important}.pt-1,.py-1{padding-top:.25rem !important}.pr-1,.px-1{padding-right:.25rem !important}.pb-1,.py-1{padding-bottom:.25rem !important}.pl-1,.px-1{padding-left:.25rem !important}.p-2{padding:.5rem !important}.pt-2,.py-2{padding-top:.5rem !important}.pr-2,.px-2{padding-right:.5rem !important}.pb-2,.py-2{padding-bottom:.5rem !important}.pl-2,.px-2{padding-left:.5rem !important}.p-3{padding:1rem !important}.pt-3,.py-3{padding-top:1rem !important}.pr-3,.px-3{padding-right:1rem !important}.pb-3,.py-3{padding-bottom:1rem !important}.pl-3,.px-3{padding-left:1rem !important}.p-4{padding:1.5rem !important}.pt-4,.py-4{padding-top:1.5rem !important}.pr-4,.px-4{padding-right:1.5rem !important}.pb-4,.py-4{padding-bottom:1.5rem !important}.pl-4,.px-4{padding-left:1.5rem !important}.p-5{padding:3rem !important}.pt-5,.py-5{padding-top:3rem !important}.pr-5,.px-5{padding-right:3rem !important}.pb-5,.py-5{padding-bottom:3rem !important}.pl-5,.px-5{padding-left:3rem !important}.m-n1{margin:-.25rem !important}.mt-n1,.my-n1{margin-top:-.25rem !important}.mr-n1,.mx-n1{margin-right:-.25rem !important}.mb-n1,.my-n1{margin-bottom:-.25rem !important}.ml-n1,.mx-n1{margin-left:-.25rem !important}.m-n2{margin:-.5rem !important}.mt-n2,.my-n2{margin-top:-.5rem !important}.mr-n2,.mx-n2{margin-right:-.5rem !important}.mb-n2,.my-n2{margin-bottom:-.5rem !important}.ml-n2,.mx-n2{margin-left:-.5rem !important}.m-n3{margin:-1rem !important}.mt-n3,.my-n3{margin-top:-1rem !important}.mr-n3,.mx-n3{margin-right:-1rem !important}.mb-n3,.my-n3{margin-bottom:-1rem !important}.ml-n3,.mx-n3{margin-left:-1rem !important}.m-n4{margin:-1.5rem !important}.mt-n4,.my-n4{margin-top:-1.5rem !important}.mr-n4,.mx-n4{margin-right:-1.5rem !important}.mb-n4,.my-n4{margin-bottom:-1.5rem !important}.ml-n4,.mx-n4{margin-left:-1.5rem !important}.m-n5{margin:-3rem !important}.mt-n5,.my-n5{margin-top:-3rem !important}.mr-n5,.mx-n5{margin-right:-3rem !important}.mb-n5,.my-n5{margin-bottom:-3rem !important}.ml-n5,.mx-n5{margin-left:-3rem !important}.m-auto{margin:auto !important}.mt-auto,.my-auto{margin-top:auto !important}.mr-auto,.mx-auto{margin-right:auto !important}.mb-auto,.my-auto{margin-bottom:auto !important}.ml-auto,.mx-auto{margin-left:auto !important}@media (min-width: 576px){.m-sm-0{margin:0 !important}.mt-sm-0,.my-sm-0{margin-top:0 !important}.mr-sm-0,.mx-sm-0{margin-right:0 !important}.mb-sm-0,.my-sm-0{margin-bottom:0 !important}.ml-sm-0,.mx-sm-0{margin-left:0 !important}.m-sm-1{margin:.25rem !important}.mt-sm-1,.my-sm-1{margin-top:.25rem !important}.mr-sm-1,.mx-sm-1{margin-right:.25rem !important}.mb-sm-1,.my-sm-1{margin-bottom:.25rem !important}.ml-sm-1,.mx-sm-1{margin-left:.25rem !important}.m-sm-2{margin:.5rem !important}.mt-sm-2,.my-sm-2{margin-top:.5rem !important}.mr-sm-2,.mx-sm-2{margin-right:.5rem !important}.mb-sm-2,.my-sm-2{margin-bottom:.5rem !important}.ml-sm-2,.mx-sm-2{margin-left:.5rem !important}.m-sm-3{margin:1rem !important}.mt-sm-3,.my-sm-3{margin-top:1rem !important}.mr-sm-3,.mx-sm-3{margin-right:1rem !important}.mb-sm-3,.my-sm-3{margin-bottom:1rem !important}.ml-sm-3,.mx-sm-3{margin-left:1rem !important}.m-sm-4{margin:1.5rem !important}.mt-sm-4,.my-sm-4{margin-top:1.5rem !important}.mr-sm-4,.mx-sm-4{margin-right:1.5rem !important}.mb-sm-4,.my-sm-4{margin-bottom:1.5rem !important}.ml-sm-4,.mx-sm-4{margin-left:1.5rem !important}.m-sm-5{margin:3rem !important}.mt-sm-5,.my-sm-5{margin-top:3rem !important}.mr-sm-5,.mx-sm-5{margin-right:3rem !important}.mb-sm-5,.my-sm-5{margin-bottom:3rem !important}.ml-sm-5,.mx-sm-5{margin-left:3rem !important}.p-sm-0{padding:0 !important}.pt-sm-0,.py-sm-0{padding-top:0 !important}.pr-sm-0,.px-sm-0{padding-right:0 !important}.pb-sm-0,.py-sm-0{padding-bottom:0 !important}.pl-sm-0,.px-sm-0{padding-left:0 !important}.p-sm-1{padding:.25rem !important}.pt-sm-1,.py-sm-1{padding-top:.25rem !important}.pr-sm-1,.px-sm-1{padding-right:.25rem !important}.pb-sm-1,.py-sm-1{padding-bottom:.25rem !important}.pl-sm-1,.px-sm-1{padding-left:.25rem !important}.p-sm-2{padding:.5rem !important}.pt-sm-2,.py-sm-2{padding-top:.5rem !important}.pr-sm-2,.px-sm-2{padding-right:.5rem !important}.pb-sm-2,.py-sm-2{padding-bottom:.5rem !important}.pl-sm-2,.px-sm-2{padding-left:.5rem !important}.p-sm-3{padding:1rem !important}.pt-sm-3,.py-sm-3{padding-top:1rem !important}.pr-sm-3,.px-sm-3{padding-right:1rem !important}.pb-sm-3,.py-sm-3{padding-bottom:1rem !important}.pl-sm-3,.px-sm-3{padding-left:1rem !important}.p-sm-4{padding:1.5rem !important}.pt-sm-4,.py-sm-4{padding-top:1.5rem !important}.pr-sm-4,.px-sm-4{padding-right:1.5rem !important}.pb-sm-4,.py-sm-4{padding-bottom:1.5rem !important}.pl-sm-4,.px-sm-4{padding-left:1.5rem !important}.p-sm-5{padding:3rem !important}.pt-sm-5,.py-sm-5{padding-top:3rem !important}.pr-sm-5,.px-sm-5{padding-right:3rem !important}.pb-sm-5,.py-sm-5{padding-bottom:3rem !important}.pl-sm-5,.px-sm-5{padding-left:3rem !important}.m-sm-n1{margin:-.25rem !important}.mt-sm-n1,.my-sm-n1{margin-top:-.25rem !important}.mr-sm-n1,.mx-sm-n1{margin-right:-.25rem !important}.mb-sm-n1,.my-sm-n1{margin-bottom:-.25rem !important}.ml-sm-n1,.mx-sm-n1{margin-left:-.25rem !important}.m-sm-n2{margin:-.5rem !important}.mt-sm-n2,.my-sm-n2{margin-top:-.5rem !important}.mr-sm-n2,.mx-sm-n2{margin-right:-.5rem !important}.mb-sm-n2,.my-sm-n2{margin-bottom:-.5rem !important}.ml-sm-n2,.mx-sm-n2{margin-left:-.5rem !important}.m-sm-n3{margin:-1rem !important}.mt-sm-n3,.my-sm-n3{margin-top:-1rem !important}.mr-sm-n3,.mx-sm-n3{margin-right:-1rem !important}.mb-sm-n3,.my-sm-n3{margin-bottom:-1rem !important}.ml-sm-n3,.mx-sm-n3{margin-left:-1rem !important}.m-sm-n4{margin:-1.5rem !important}.mt-sm-n4,.my-sm-n4{margin-top:-1.5rem !important}.mr-sm-n4,.mx-sm-n4{margin-right:-1.5rem !important}.mb-sm-n4,.my-sm-n4{margin-bottom:-1.5rem !important}.ml-sm-n4,.mx-sm-n4{margin-left:-1.5rem !important}.m-sm-n5{margin:-3rem !important}.mt-sm-n5,.my-sm-n5{margin-top:-3rem !important}.mr-sm-n5,.mx-sm-n5{margin-right:-3rem !important}.mb-sm-n5,.my-sm-n5{margin-bottom:-3rem !important}.ml-sm-n5,.mx-sm-n5{margin-left:-3rem !important}.m-sm-auto{margin:auto !important}.mt-sm-auto,.my-sm-auto{margin-top:auto !important}.mr-sm-auto,.mx-sm-auto{margin-right:auto !important}.mb-sm-auto,.my-sm-auto{margin-bottom:auto !important}.ml-sm-auto,.mx-sm-auto{margin-left:auto !important}}@media (min-width: 768px){.m-md-0{margin:0 !important}.mt-md-0,.my-md-0{margin-top:0 !important}.mr-md-0,.mx-md-0{margin-right:0 !important}.mb-md-0,.my-md-0{margin-bottom:0 !important}.ml-md-0,.mx-md-0{margin-left:0 !important}.m-md-1{margin:.25rem !important}.mt-md-1,.my-md-1{margin-top:.25rem !important}.mr-md-1,.mx-md-1{margin-right:.25rem !important}.mb-md-1,.my-md-1{margin-bottom:.25rem !important}.ml-md-1,.mx-md-1{margin-left:.25rem !important}.m-md-2{margin:.5rem !important}.mt-md-2,.my-md-2{margin-top:.5rem !important}.mr-md-2,.mx-md-2{margin-right:.5rem !important}.mb-md-2,.my-md-2{margin-bottom:.5rem !important}.ml-md-2,.mx-md-2{margin-left:.5rem !important}.m-md-3{margin:1rem !important}.mt-md-3,.my-md-3{margin-top:1rem !important}.mr-md-3,.mx-md-3{margin-right:1rem !important}.mb-md-3,.my-md-3{margin-bottom:1rem !important}.ml-md-3,.mx-md-3{margin-left:1rem !important}.m-md-4{margin:1.5rem !important}.mt-md-4,.my-md-4{margin-top:1.5rem !important}.mr-md-4,.mx-md-4{margin-right:1.5rem !important}.mb-md-4,.my-md-4{margin-bottom:1.5rem !important}.ml-md-4,.mx-md-4{margin-left:1.5rem !important}.m-md-5{margin:3rem !important}.mt-md-5,.my-md-5{margin-top:3rem !important}.mr-md-5,.mx-md-5{margin-right:3rem !important}.mb-md-5,.my-md-5{margin-bottom:3rem !important}.ml-md-5,.mx-md-5{margin-left:3rem !important}.p-md-0{padding:0 !important}.pt-md-0,.py-md-0{padding-top:0 !important}.pr-md-0,.px-md-0{padding-right:0 !important}.pb-md-0,.py-md-0{padding-bottom:0 !important}.pl-md-0,.px-md-0{padding-left:0 !important}.p-md-1{padding:.25rem !important}.pt-md-1,.py-md-1{padding-top:.25rem !important}.pr-md-1,.px-md-1{padding-right:.25rem !important}.pb-md-1,.py-md-1{padding-bottom:.25rem !important}.pl-md-1,.px-md-1{padding-left:.25rem !important}.p-md-2{padding:.5rem !important}.pt-md-2,.py-md-2{padding-top:.5rem !important}.pr-md-2,.px-md-2{padding-right:.5rem !important}.pb-md-2,.py-md-2{padding-bottom:.5rem !important}.pl-md-2,.px-md-2{padding-left:.5rem !important}.p-md-3{padding:1rem !important}.pt-md-3,.py-md-3{padding-top:1rem !important}.pr-md-3,.px-md-3{padding-right:1rem !important}.pb-md-3,.py-md-3{padding-bottom:1rem !important}.pl-md-3,.px-md-3{padding-left:1rem !important}.p-md-4{padding:1.5rem !important}.pt-md-4,.py-md-4{padding-top:1.5rem !important}.pr-md-4,.px-md-4{padding-right:1.5rem !important}.pb-md-4,.py-md-4{padding-bottom:1.5rem !important}.pl-md-4,.px-md-4{padding-left:1.5rem !important}.p-md-5{padding:3rem !important}.pt-md-5,.py-md-5{padding-top:3rem !important}.pr-md-5,.px-md-5{padding-right:3rem !important}.pb-md-5,.py-md-5{padding-bottom:3rem !important}.pl-md-5,.px-md-5{padding-left:3rem !important}.m-md-n1{margin:-.25rem !important}.mt-md-n1,.my-md-n1{margin-top:-.25rem !important}.mr-md-n1,.mx-md-n1{margin-right:-.25rem !important}.mb-md-n1,.my-md-n1{margin-bottom:-.25rem !important}.ml-md-n1,.mx-md-n1{margin-left:-.25rem !important}.m-md-n2{margin:-.5rem !important}.mt-md-n2,.my-md-n2{margin-top:-.5rem !important}.mr-md-n2,.mx-md-n2{margin-right:-.5rem !important}.mb-md-n2,.my-md-n2{margin-bottom:-.5rem !important}.ml-md-n2,.mx-md-n2{margin-left:-.5rem !important}.m-md-n3{margin:-1rem !important}.mt-md-n3,.my-md-n3{margin-top:-1rem !important}.mr-md-n3,.mx-md-n3{margin-right:-1rem !important}.mb-md-n3,.my-md-n3{margin-bottom:-1rem !important}.ml-md-n3,.mx-md-n3{margin-left:-1rem !important}.m-md-n4{margin:-1.5rem !important}.mt-md-n4,.my-md-n4{margin-top:-1.5rem !important}.mr-md-n4,.mx-md-n4{margin-right:-1.5rem !important}.mb-md-n4,.my-md-n4{margin-bottom:-1.5rem !important}.ml-md-n4,.mx-md-n4{margin-left:-1.5rem !important}.m-md-n5{margin:-3rem !important}.mt-md-n5,.my-md-n5{margin-top:-3rem !important}.mr-md-n5,.mx-md-n5{margin-right:-3rem !important}.mb-md-n5,.my-md-n5{margin-bottom:-3rem !important}.ml-md-n5,.mx-md-n5{margin-left:-3rem !important}.m-md-auto{margin:auto !important}.mt-md-auto,.my-md-auto{margin-top:auto !important}.mr-md-auto,.mx-md-auto{margin-right:auto !important}.mb-md-auto,.my-md-auto{margin-bottom:auto !important}.ml-md-auto,.mx-md-auto{margin-left:auto !important}}@media (min-width: 992px){.m-lg-0{margin:0 !important}.mt-lg-0,.my-lg-0{margin-top:0 !important}.mr-lg-0,.mx-lg-0{margin-right:0 !important}.mb-lg-0,.my-lg-0{margin-bottom:0 !important}.ml-lg-0,.mx-lg-0{margin-left:0 !important}.m-lg-1{margin:.25rem !important}.mt-lg-1,.my-lg-1{margin-top:.25rem !important}.mr-lg-1,.mx-lg-1{margin-right:.25rem !important}.mb-lg-1,.my-lg-1{margin-bottom:.25rem !important}.ml-lg-1,.mx-lg-1{margin-left:.25rem !important}.m-lg-2{margin:.5rem !important}.mt-lg-2,.my-lg-2{margin-top:.5rem !important}.mr-lg-2,.mx-lg-2{margin-right:.5rem !important}.mb-lg-2,.my-lg-2{margin-bottom:.5rem !important}.ml-lg-2,.mx-lg-2{margin-left:.5rem !important}.m-lg-3{margin:1rem !important}.mt-lg-3,.my-lg-3{margin-top:1rem !important}.mr-lg-3,.mx-lg-3{margin-right:1rem !important}.mb-lg-3,.my-lg-3{margin-bottom:1rem !important}.ml-lg-3,.mx-lg-3{margin-left:1rem !important}.m-lg-4{margin:1.5rem !important}.mt-lg-4,.my-lg-4{margin-top:1.5rem !important}.mr-lg-4,.mx-lg-4{margin-right:1.5rem !important}.mb-lg-4,.my-lg-4{margin-bottom:1.5rem !important}.ml-lg-4,.mx-lg-4{margin-left:1.5rem !important}.m-lg-5{margin:3rem !important}.mt-lg-5,.my-lg-5{margin-top:3rem !important}.mr-lg-5,.mx-lg-5{margin-right:3rem !important}.mb-lg-5,.my-lg-5{margin-bottom:3rem !important}.ml-lg-5,.mx-lg-5{margin-left:3rem !important}.p-lg-0{padding:0 !important}.pt-lg-0,.py-lg-0{padding-top:0 !important}.pr-lg-0,.px-lg-0{padding-right:0 !important}.pb-lg-0,.py-lg-0{padding-bottom:0 !important}.pl-lg-0,.px-lg-0{padding-left:0 !important}.p-lg-1{padding:.25rem !important}.pt-lg-1,.py-lg-1{padding-top:.25rem !important}.pr-lg-1,.px-lg-1{padding-right:.25rem !important}.pb-lg-1,.py-lg-1{padding-bottom:.25rem !important}.pl-lg-1,.px-lg-1{padding-left:.25rem !important}.p-lg-2{padding:.5rem !important}.pt-lg-2,.py-lg-2{padding-top:.5rem !important}.pr-lg-2,.px-lg-2{padding-right:.5rem !important}.pb-lg-2,.py-lg-2{padding-bottom:.5rem !important}.pl-lg-2,.px-lg-2{padding-left:.5rem !important}.p-lg-3{padding:1rem !important}.pt-lg-3,.py-lg-3{padding-top:1rem !important}.pr-lg-3,.px-lg-3{padding-right:1rem !important}.pb-lg-3,.py-lg-3{padding-bottom:1rem !important}.pl-lg-3,.px-lg-3{padding-left:1rem !important}.p-lg-4{padding:1.5rem !important}.pt-lg-4,.py-lg-4{padding-top:1.5rem !important}.pr-lg-4,.px-lg-4{padding-right:1.5rem !important}.pb-lg-4,.py-lg-4{padding-bottom:1.5rem !important}.pl-lg-4,.px-lg-4{padding-left:1.5rem !important}.p-lg-5{padding:3rem !important}.pt-lg-5,.py-lg-5{padding-top:3rem !important}.pr-lg-5,.px-lg-5{padding-right:3rem !important}.pb-lg-5,.py-lg-5{padding-bottom:3rem !important}.pl-lg-5,.px-lg-5{padding-left:3rem !important}.m-lg-n1{margin:-.25rem !important}.mt-lg-n1,.my-lg-n1{margin-top:-.25rem !important}.mr-lg-n1,.mx-lg-n1{margin-right:-.25rem !important}.mb-lg-n1,.my-lg-n1{margin-bottom:-.25rem !important}.ml-lg-n1,.mx-lg-n1{margin-left:-.25rem !important}.m-lg-n2{margin:-.5rem !important}.mt-lg-n2,.my-lg-n2{margin-top:-.5rem !important}.mr-lg-n2,.mx-lg-n2{margin-right:-.5rem !important}.mb-lg-n2,.my-lg-n2{margin-bottom:-.5rem !important}.ml-lg-n2,.mx-lg-n2{margin-left:-.5rem !important}.m-lg-n3{margin:-1rem !important}.mt-lg-n3,.my-lg-n3{margin-top:-1rem !important}.mr-lg-n3,.mx-lg-n3{margin-right:-1rem !important}.mb-lg-n3,.my-lg-n3{margin-bottom:-1rem !important}.ml-lg-n3,.mx-lg-n3{margin-left:-1rem !important}.m-lg-n4{margin:-1.5rem !important}.mt-lg-n4,.my-lg-n4{margin-top:-1.5rem !important}.mr-lg-n4,.mx-lg-n4{margin-right:-1.5rem !important}.mb-lg-n4,.my-lg-n4{margin-bottom:-1.5rem !important}.ml-lg-n4,.mx-lg-n4{margin-left:-1.5rem !important}.m-lg-n5{margin:-3rem !important}.mt-lg-n5,.my-lg-n5{margin-top:-3rem !important}.mr-lg-n5,.mx-lg-n5{margin-right:-3rem !important}.mb-lg-n5,.my-lg-n5{margin-bottom:-3rem !important}.ml-lg-n5,.mx-lg-n5{margin-left:-3rem !important}.m-lg-auto{margin:auto !important}.mt-lg-auto,.my-lg-auto{margin-top:auto !important}.mr-lg-auto,.mx-lg-auto{margin-right:auto !important}.mb-lg-auto,.my-lg-auto{margin-bottom:auto !important}.ml-lg-auto,.mx-lg-auto{margin-left:auto !important}}@media (min-width: 1200px){.m-xl-0{margin:0 !important}.mt-xl-0,.my-xl-0{margin-top:0 !important}.mr-xl-0,.mx-xl-0{margin-right:0 !important}.mb-xl-0,.my-xl-0{margin-bottom:0 !important}.ml-xl-0,.mx-xl-0{margin-left:0 !important}.m-xl-1{margin:.25rem !important}.mt-xl-1,.my-xl-1{margin-top:.25rem !important}.mr-xl-1,.mx-xl-1{margin-right:.25rem !important}.mb-xl-1,.my-xl-1{margin-bottom:.25rem !important}.ml-xl-1,.mx-xl-1{margin-left:.25rem !important}.m-xl-2{margin:.5rem !important}.mt-xl-2,.my-xl-2{margin-top:.5rem !important}.mr-xl-2,.mx-xl-2{margin-right:.5rem !important}.mb-xl-2,.my-xl-2{margin-bottom:.5rem !important}.ml-xl-2,.mx-xl-2{margin-left:.5rem !important}.m-xl-3{margin:1rem !important}.mt-xl-3,.my-xl-3{margin-top:1rem !important}.mr-xl-3,.mx-xl-3{margin-right:1rem !important}.mb-xl-3,.my-xl-3{margin-bottom:1rem !important}.ml-xl-3,.mx-xl-3{margin-left:1rem !important}.m-xl-4{margin:1.5rem !important}.mt-xl-4,.my-xl-4{margin-top:1.5rem !important}.mr-xl-4,.mx-xl-4{margin-right:1.5rem !important}.mb-xl-4,.my-xl-4{margin-bottom:1.5rem !important}.ml-xl-4,.mx-xl-4{margin-left:1.5rem !important}.m-xl-5{margin:3rem !important}.mt-xl-5,.my-xl-5{margin-top:3rem !important}.mr-xl-5,.mx-xl-5{margin-right:3rem !important}.mb-xl-5,.my-xl-5{margin-bottom:3rem !important}.ml-xl-5,.mx-xl-5{margin-left:3rem !important}.p-xl-0{padding:0 !important}.pt-xl-0,.py-xl-0{padding-top:0 !important}.pr-xl-0,.px-xl-0{padding-right:0 !important}.pb-xl-0,.py-xl-0{padding-bottom:0 !important}.pl-xl-0,.px-xl-0{padding-left:0 !important}.p-xl-1{padding:.25rem !important}.pt-xl-1,.py-xl-1{padding-top:.25rem !important}.pr-xl-1,.px-xl-1{padding-right:.25rem !important}.pb-xl-1,.py-xl-1{padding-bottom:.25rem !important}.pl-xl-1,.px-xl-1{padding-left:.25rem !important}.p-xl-2{padding:.5rem !important}.pt-xl-2,.py-xl-2{padding-top:.5rem !important}.pr-xl-2,.px-xl-2{padding-right:.5rem !important}.pb-xl-2,.py-xl-2{padding-bottom:.5rem !important}.pl-xl-2,.px-xl-2{padding-left:.5rem !important}.p-xl-3{padding:1rem !important}.pt-xl-3,.py-xl-3{padding-top:1rem !important}.pr-xl-3,.px-xl-3{padding-right:1rem !important}.pb-xl-3,.py-xl-3{padding-bottom:1rem !important}.pl-xl-3,.px-xl-3{padding-left:1rem !important}.p-xl-4{padding:1.5rem !important}.pt-xl-4,.py-xl-4{padding-top:1.5rem !important}.pr-xl-4,.px-xl-4{padding-right:1.5rem !important}.pb-xl-4,.py-xl-4{padding-bottom:1.5rem !important}.pl-xl-4,.px-xl-4{padding-left:1.5rem !important}.p-xl-5{padding:3rem !important}.pt-xl-5,.py-xl-5{padding-top:3rem !important}.pr-xl-5,.px-xl-5{padding-right:3rem !important}.pb-xl-5,.py-xl-5{padding-bottom:3rem !important}.pl-xl-5,.px-xl-5{padding-left:3rem !important}.m-xl-n1{margin:-.25rem !important}.mt-xl-n1,.my-xl-n1{margin-top:-.25rem !important}.mr-xl-n1,.mx-xl-n1{margin-right:-.25rem !important}.mb-xl-n1,.my-xl-n1{margin-bottom:-.25rem !important}.ml-xl-n1,.mx-xl-n1{margin-left:-.25rem !important}.m-xl-n2{margin:-.5rem !important}.mt-xl-n2,.my-xl-n2{margin-top:-.5rem !important}.mr-xl-n2,.mx-xl-n2{margin-right:-.5rem !important}.mb-xl-n2,.my-xl-n2{margin-bottom:-.5rem !important}.ml-xl-n2,.mx-xl-n2{margin-left:-.5rem !important}.m-xl-n3{margin:-1rem !important}.mt-xl-n3,.my-xl-n3{margin-top:-1rem !important}.mr-xl-n3,.mx-xl-n3{margin-right:-1rem !important}.mb-xl-n3,.my-xl-n3{margin-bottom:-1rem !important}.ml-xl-n3,.mx-xl-n3{margin-left:-1rem !important}.m-xl-n4{margin:-1.5rem !important}.mt-xl-n4,.my-xl-n4{margin-top:-1.5rem !important}.mr-xl-n4,.mx-xl-n4{margin-right:-1.5rem !important}.mb-xl-n4,.my-xl-n4{margin-bottom:-1.5rem !important}.ml-xl-n4,.mx-xl-n4{margin-left:-1.5rem !important}.m-xl-n5{margin:-3rem !important}.mt-xl-n5,.my-xl-n5{margin-top:-3rem !important}.mr-xl-n5,.mx-xl-n5{margin-right:-3rem !important}.mb-xl-n5,.my-xl-n5{margin-bottom:-3rem !important}.ml-xl-n5,.mx-xl-n5{margin-left:-3rem !important}.m-xl-auto{margin:auto !important}.mt-xl-auto,.my-xl-auto{margin-top:auto !important}.mr-xl-auto,.mx-xl-auto{margin-right:auto !important}.mb-xl-auto,.my-xl-auto{margin-bottom:auto !important}.ml-xl-auto,.mx-xl-auto{margin-left:auto !important}}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;pointer-events:auto;content:"";background-color:rgba(0,0,0,0)}.text-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace !important}.text-justify{text-align:justify !important}.text-wrap{white-space:normal !important}.text-nowrap{white-space:nowrap !important}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text-left{text-align:left !important}.text-right{text-align:right !important}.text-center{text-align:center !important}@media (min-width: 576px){.text-sm-left{text-align:left !important}.text-sm-right{text-align:right !important}.text-sm-center{text-align:center !important}}@media (min-width: 768px){.text-md-left{text-align:left !important}.text-md-right{text-align:right !important}.text-md-center{text-align:center !important}}@media (min-width: 992px){.text-lg-left{text-align:left !important}.text-lg-right{text-align:right !important}.text-lg-center{text-align:center !important}}@media (min-width: 1200px){.text-xl-left{text-align:left !important}.text-xl-right{text-align:right !important}.text-xl-center{text-align:center !important}}.text-lowercase{text-transform:lowercase !important}.text-uppercase{text-transform:uppercase !important}.text-capitalize{text-transform:capitalize !important}.font-weight-light{font-weight:300 !important}.font-weight-lighter{font-weight:lighter !important}.font-weight-normal{font-weight:400 !important}.font-weight-bold{font-weight:700 !important}.font-weight-bolder{font-weight:bolder !important}.font-italic{font-style:italic !important}.text-white{color:#fff !important}.text-primary{color:#1f80f9 !important}a.text-primary:hover,a.text-primary:focus{color:#055bc6 !important}.text-secondary{color:#206ed5 !important}a.text-secondary:hover,a.text-secondary:focus{color:#164c92 !important}.text-info{color:#456bc4 !important}a.text-info:hover,a.text-info:focus{color:#2d4b8f !important}.text-warning{color:#d15f42 !important}a.text-warning:hover,a.text-warning:focus{color:#a03f27 !important}.text-danger{color:#c93831 !important}a.text-danger:hover,a.text-danger:focus{color:#8b2722 !important}.text-light{color:#eceeec !important}a.text-light:hover,a.text-light:focus{color:#c4cac4 !important}.text-dark{color:#1e2b37 !important}a.text-dark:hover,a.text-dark:focus{color:#030406 !important}.text-body{color:#212529 !important}.text-muted{color:#6c757d !important}.text-black-50{color:rgba(0,0,0,0.5) !important}.text-white-50{color:rgba(255,255,255,0.5) !important}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.text-decoration-none{text-decoration:none !important}.text-break{word-wrap:break-word !important}.text-reset{color:inherit !important}.visible{visibility:visible !important}.invisible{visibility:hidden !important}@media print{*,*::before,*::after{text-shadow:none !important;box-shadow:none !important}a:not(.btn){text-decoration:underline}abbr[title]::after{content:" (" attr(title) ")"}pre{white-space:pre-wrap !important}pre,blockquote{border:1px solid #adb5bd;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}@page{size:a3}body{min-width:992px !important}.container{min-width:992px !important}.navbar{display:none}.badge{border:1px solid #000}.table{border-collapse:collapse !important}.table td,.table th{background-color:#fff !important}.table-bordered th,.table-bordered td{border:1px solid #dee2e6 !important}.table-dark{color:inherit}.table-dark th,.table-dark td,.table-dark thead th,.table-dark tbody+tbody{border-color:#dee2e6}.table .thead-dark th{color:inherit;border-color:#dee2e6}} + + + .progress-striped .progress-bar, + .progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; + } + .progress.active .progress-bar, + .progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; + } + .progress-bar-success { + background-color: #5cb85c; + } + .progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + } + .progress-bar-info { + background-color: #5bc0de; + } + .progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + } + .progress-bar-warning { + background-color: #f0ad4e; + } + .progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + } + .progress-bar-danger { + background-color: #d9534f; + } + .progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + } diff --git a/web/assets/css/bs5.darktheme.css b/web/assets/css/bs5.darktheme.css new file mode 100644 index 00000000..55718e20 --- /dev/null +++ b/web/assets/css/bs5.darktheme.css @@ -0,0 +1,51 @@ +.dark .list-group-item{border-color: #444;background:#222} +.dark .list-group-item.active{background:#c49a68;border-color:#a7865f} +.novideos{text-transform: uppercase;text-align: center;border-bottom:0!important;padding-top: 55%!important;letter-spacing:2px} + +.btn-warning { + color: #fff; + background-color: #c49a68; + border-color: #c49a68; +} +.dark .table-striped>tbody>tr>td{border-color:#222;color:#fff} +.dark code{color: #c49a68;background-color: #36333d;} +.dark a:not(.btn){color: #c49a68;} + + + + +.dark.form-control, +.dark .form-control, +.dark.form-select, +.dark .form-select + { + background-color: #2b3c4b; + border: 1px solid #2b3c4b; + color: #fff; +} + +.dark.form-control:focus, +.dark .form-control:focus, +.dark.form-select:focus, +.dark .form-select:focus +{ + color: #ddd; + background-color: #2b3c4b; + border: 1px solid #222; + box-shadow: none; +} + +.dark .slider-selection { + background: #375182; +} +.dark .slider-handle { + background: #375182; +} +.dark .slider-track { + background: #333; +} + +.progress { + height: 5px; + background: #1e2b37; +} diff --git a/web/assets/css/bs5.forms.css b/web/assets/css/bs5.forms.css new file mode 100644 index 00000000..7f1a8de7 --- /dev/null +++ b/web/assets/css/bs5.forms.css @@ -0,0 +1,33 @@ +form.modal-body{margin:0} +.dark .form-group-group{color:#fff;} +.form-group-group label{margin: 0} +.form-group-group blockquote:before,.form-group-group blockquote:after{display:none!important} +.form-group-group blockquote{letter-spacing:normal;font-style:normal} +.form-group-group blockquote p:empty{display:none} +.form-group-group blockquote p{font-size:inherit} +.form-group-group blockquote p:last-child{margin-bottom:0} +.form-group-group-group>div,.form-group-group-group .h_us_advanced>div{margin-bottom:15px;} +.form-group-group table{width:100%} +.form-group-group table tr td{padding:10px 5px} +.form-group-group table tr:not(:last-child) td{border-bottom:1px dotted #eee} +/* .form-group-group .mdl-list__item{border-bottom:1px solid #eee;} +.form-group-group .mdl-list__item:hover{background:#e6e6e6;border-radius:4px;} +.dark .form-group-group .mdl-list__item{color:#fff;border-bottom:1px solid #444;} +.dark .form-group-group .mdl-list__item:hover{background:#555;} */ +.form-group-group:visible:last-child,.form-group-group > .form-group:last-child{margin-bottom:0} +.form-group-group:last-child {margin-bottom: 0} + +.btn-group-justified {display: flex} +.btn-group-justified .btn {flex: 1} + + +/* .hide-box-wrapper.form-group-group > h4{ + margin: 0; +} +.hide-box-wrapper.form-group-group{ + padding: 0; +} */ +.hide-box-wrapper > .box-wrapper{ + height: 0; + overflow: hidden; +} diff --git a/web/libs/css/dash2.monitors.css b/web/assets/css/bs5.liveGrid.css similarity index 74% rename from web/libs/css/dash2.monitors.css rename to web/assets/css/bs5.liveGrid.css index 6bfff741..c6e75e6a 100644 --- a/web/libs/css/dash2.monitors.css +++ b/web/assets/css/bs5.liveGrid.css @@ -1,4 +1,45 @@ +#monitors_live { + overflow: hidden; +} +.mdl-card__media { + background-color: #333; + background-repeat: repeat; + background-position: 50% 50%; + background-size: cover; + background-origin: padding-box; + background-attachment: scroll; + box-sizing: border-box; +} + +.monitor_item .mdl-card__media { + box-sizing: border-box; + background-size: cover; + padding: 24px; + display: -webkit-flex; + display: -ms-flexbox; + display: flex; + -webkit-flex-grow: 1; + -ms-flex-positive: 1; + flex-grow: 1; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-align-items: flex-end; + -ms-flex-align: end; + align-items: flex-end; +} +.monitor_item * { + transition: 0.2s!important; +} +.monitor_item .mdl-card__supporting-text:not(:last-child) { + box-sizing: border-box; + padding: 10px 16px; + min-height: auto; +} +.monitor_item .mdl-card__supporting-text{ + width: 100%; +} .jpegMode .cpu_load .progress-bar,.jpegMode .ram_load .progress-bar{background-color:#5cb85c} .jpegMode [system="jpegToggle"],[system].text-success{color:#5cb85c!important} @@ -12,12 +53,64 @@ .monitor_item .stream-hud:hover .bottom-text{top:0;} .monitor_item .stream-hud .bottom-text .detector-fade{background: rgba(0,0,0,0.4);padding:10px 20px;border-radius:10px} .monitor_item .stream-hud .lamp{position:absolute;top:5px;right:5px;z-index:1;text-shadow: 0 0 15px #333;} +.monitor_item .mdl-overlay-menu{ + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + margin: auto; + width: 50%; + min-width: 200px; + height: 50%; + min-height: 200px; + overflow: auto; + z-index: 100; + background: rgba(0,0,0,0.7); + color: #fff; +} +.monitor_item .mdl-overlay-menu-backdrop{ + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + margin: auto; + width: 100%; + height: 100%; +} +.monitor_item .mdl-overlay-menu{ + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + margin: auto; + width: 50%; + min-width: 200px; + height: 50%; + min-height: 150px; + overflow: auto; + z-index: 100; + background: rgba(0,0,0,0.7); + color: #fff; + text-align: center; +} +.monitor_item .mdl-overlay-menu li{ + padding: 1rem; +} +.monitor_item .mdl-overlay-menu li i{ + float: left; + font-size: 1.5em; +} +.monitor_item .mdl-overlay-menu li:hover{ + background: #021B79; +} .monitor_item[mode="Disabled"] .stream-hud .lamp{color:#5d5d5d} .monitor_item[mode="Watch Only"] .stream-hud .lamp{color:#5da8e8} .monitor_item[mode="Idle"] .stream-hud .lamp{color:#fff} .monitor_item[mode="Record"] .stream-hud .lamp{color:#d9534f} /*.data-menu{max-height:700px}*/ -.data-menu:not(:last-child){border-right:1px solid #fff;} .data-menu.logs{list-style:none;} .monitor_item .motionVision{display:none} @@ -33,7 +126,7 @@ .monitor_item .stream-element{border: 0;object-fit: fill;height: 100%;width:100%} .monitor_item{position:relative;padding:0;transition:none;background:#000} .monitor_item .mdl-card{min-height:auto;border:1px solid #272727;border-radius:0px;overflow:hidden} -.monitor_item .mdl-card__media{position:relative;padding:0!important;display:block!important;background:#000;} +.monitor_item .mdl-card__media{height:100%;position:relative;padding:0!important;display:block!important;background:#000;} .monitor_item.selected .stream-element{height:600px} .monitor_item.selected .fa-expand:before{content:"\f066"} .monitor_item .mdl-card__supporting-text{background:#222;color:#fff!important;display:block;min-height:auto!important} @@ -50,11 +143,7 @@ .monitor_item .stream-hud .controls .btn{opacity:0.7} .monitor_item.doObjectDetection .progress-bar{background-color: #57d94f} -.data-menu{text-align:left} -.data-menu ul,.side-menu ul{list-style:none;margin:0;padding:0;} -.data-menu li,.side-menu li:not(.mdl-menu__item){ - border-bottom:1px solid #54502d;padding:10px; -} +.data-menu{text-align:left;height: 100%; overflow: auto;} .data-menu .progress-circle{margin:0 10px 0 0;position:relative;height:40px;width:40px;float:left} .data-menu .progress-circle span:after{content:''} img.circle-img,div.circle-img{border-radius:50%;height:50px;width:50px} @@ -91,7 +180,6 @@ img.circle-img,div.circle-img{border-radius:50%;height:50px;width:50px} #monSectionStreamChannels:empty,#monSectionInputMaps:empty{display:none} -#monitors_list .monitor_block{transition:none} .dropdown-menu.scrollable{max-height:300px} .upload_file input{display:none} #video_preview .stream-objects{right:0;margin:auto;display:inline-block;position:relative;width:auto} @@ -121,3 +209,6 @@ img.circle-img,div.circle-img{border-radius:50%;height:50px;width:50px} top: 10px; right: 10px; } +.dont-stretch-monitors .monitor_item .stream-element { + object-fit: contain!important; +} diff --git a/web/assets/css/bs5.livePlayer.css b/web/assets/css/bs5.livePlayer.css new file mode 100644 index 00000000..713c2b59 --- /dev/null +++ b/web/assets/css/bs5.livePlayer.css @@ -0,0 +1,35 @@ +.tab-livePlayer video{ + width: 100%; + background: #333; +} +.tab-livePlayer-event-row { + cursor: pointer; +} +.livePlayer-event-objects .tag { + position: absolute; + bottom: 100%; + left: 0; + background: red; + color: #fff; + font-family: monospace; + font-size: 80%; + border-radius: 5px 5px 0 0; + padding: 3px 5px; +} + +.tab-livePlayer-event-objects .stream-detected-object { + position: absolute; + top: 0; + left: 0; + border: 3px solid red; + background: transparent; + border-radius: 5px +} + +.tab-livePlayer:hover .tab-livePlayer-event-objects { + display: none; +} + +.tab-livePlayer .stream-element { + width: 100%; +} diff --git a/web/assets/css/bs5.onvifScanner.css b/web/assets/css/bs5.onvifScanner.css new file mode 100644 index 00000000..9a38f40f --- /dev/null +++ b/web/assets/css/bs5.onvifScanner.css @@ -0,0 +1,14 @@ +#tab-onvifScanner .onvif_result .preview-image { + width: 100%; + background-size: cover; + height: 100px; +} +#tab-onvifScanner .onvif_result .card { + cursor: pointer; +} +#tab-onvifScanner .onvif_result .card-body { + min-height: auto; +} +#tab-onvifScanner .onvif_result { + padding-top:2rem; +} diff --git a/web/libs/css/dash2.powerVideo2.css b/web/assets/css/bs5.powerVideo.css similarity index 95% rename from web/libs/css/dash2.powerVideo2.css rename to web/assets/css/bs5.powerVideo.css index 59973929..3a7737d2 100644 --- a/web/libs/css/dash2.powerVideo2.css +++ b/web/assets/css/bs5.powerVideo.css @@ -40,7 +40,7 @@ #powerVideo .videoPlayer .videoPlayer-detection-info { position: absolute; padding: 20px 10px 20px 10px; - height: 100%; + /* height: 100%; */ width: 100%; top: 0; left: 0; @@ -175,3 +175,11 @@ .vis-selected [timeline-video-file] .progress{ opacity: 1; } + +#powerVideo .vis-item.vis-box { + border-radius: 5px; +} + +#powerVideo .vis-labelset .vis-label { + color: #fff; +} diff --git a/web/libs/css/dash2.ptzcontrols.css b/web/assets/css/bs5.ptzControls.css similarity index 100% rename from web/libs/css/dash2.ptzcontrols.css rename to web/assets/css/bs5.ptzControls.css diff --git a/web/assets/css/bs5.regionEditor.css b/web/assets/css/bs5.regionEditor.css new file mode 100644 index 00000000..738046d7 --- /dev/null +++ b/web/assets/css/bs5.regionEditor.css @@ -0,0 +1,11 @@ +#tab-regionEditor .canvas_holder{position:relative;display:inline-block;overflow:hidden;height:450px} +#tab-regionEditor .cord_element{position:absolute;background:rgba(221, 221, 221, 0.8);z-index:11;padding:5px;} +#tab-regionEditor .cord_element.selected{z-index:12;} +#tab-regionEditor .cord_element .controls{margin-bottom:5px;} + +#tab-regionEditor .canvas_holder{ + width: 100%; +} + +#region_editor_live iframe,.canvas_holder canvas,#tab-regionEditor .grid{border:0;position:absolute;left:0;top:0} +#tab-regionEditor .grid{width:100%;height:100%;} diff --git a/web/assets/css/bs5.sideMenu.css b/web/assets/css/bs5.sideMenu.css new file mode 100644 index 00000000..6818580c --- /dev/null +++ b/web/assets/css/bs5.sideMenu.css @@ -0,0 +1,161 @@ +body.hide-side-menu #sidebarMenu{ + width: 0px!important; + overflow: none!important; +} +body.hide-side-menu #pageTabContainer{ + margin: initial!important; + width: 100%!important; +} +#floating-hide-button { + top: 10px; + right: initial; + left: 10px; + opacity: 0.3; +} +#floating-hide-button:hover { + opacity: 1; +} +.compressed-monitor-icons .monitor-icon { + padding: 0.25rem; + position: relative; +} + +#monitorSideList.compressed-monitor-icons { + flex-direction: row !important; + justify-content: center; +} + +#monitorSideList { + padding-bottom: 25px!important; +} + +ul:not(.compressed-monitor-icons) .monitor-icon img{ + height: 50px!important; + width: 50px!important; +} + +.compressed-monitor-icons .monitor-icon img{ + margin-right: 0!important; + border-radius: 0!important; + height: 50px; + width: 100%; +} + +.compressed-monitor-icons .monitor-icon > div{ + border-radius: 10px!important; + padding: 0!important; + overflow: hidden; +} + +.compressed-monitor-icons .monitor-icon .hidden-squeeze { + display: none; +} + +.compressed-monitor-icons .squeeze-button { + position: absolute; + bottom: 5px; + right: 5px; +} + +#createdTabLinks .nav-item:not(:last-child){ + margin-bottom: 0.5rem; +} + +#createdTabLinks .side-menu-link:not(.active){ + border: 1px solid rgb(31 128 249 / 20%); +} + +.sidebar { + position: fixed; + top: 0; + bottom: 0; + left: 0; + z-index: 100; + padding: 0; + box-shadow: inset -1px 0 0 rgb(0 0 0 / 10%); +} +.sidebar .nav-link:not(.active) i { + color: #888; +} +.sidebar .nav-link { + transition: none; + color: #fff; +} + +#monitorSideList > li.nav-item.monitor-icon > div > div.flex-fill.text-end > div > ul { + transform: translate(-20px, 0px)!important; +} + +#toggleSideBarMenu, #dropdownSideBarMenu { + top: 55px; + left: 10px; + right: initial; + color: #333; + background: #fff; +} + +#monitorSideList.compressed-monitor-icons .monitor-icon { + width: 25%; +} + +@keyframes border-pulsate-stopping { + 0% { border-color: rgba(84, 84, 84, 1); } + 50% { border-color: rgba(84, 84, 84, 0); } + 100% { border-color: rgba(84, 84, 84, 1); } +} + +@keyframes border-pulsate-starting { + 0% { border-color: rgba(0, 255, 255, 1); } + 50% { border-color: rgba(0, 255, 255, 0); } + 100% { border-color: rgba(0, 255, 255, 1); } +} + +@keyframes border-pulsate-died { + 0% { border-color: rgba(251, 71, 71, 1); } + 50% { border-color: rgba(251, 71, 71, 0); } + 100% { border-color: rgba(251, 71, 71, 1); } +} + +#monitorSideList .nav-item > .btn { + border: 1px solid #393939; +} + +#monitorSideList [data-status-code="0"] > .btn { + border-color: #393939; +} +/* Starting */ +#monitorSideList [data-status-code="1"] > .btn { + border-color: cyan; + animation: border-pulsate-died 2s infinite; +} +/* Watching */ +#monitorSideList [data-status-code="2"] > .btn { + border-color: #2371ff; +} +/* Recording */ +#monitorSideList [data-status-code="3"] > .btn { + border-color: #ff0000; +} +/* Restarting */ +#monitorSideList [data-status-code="4"] > .btn { + border-color: #ff0000; + animation: border-pulsate-died 2s infinite; +} +/* Stopped */ +#monitorSideList [data-status-code="5"] > .btn { + border-color: #393939; +} +/* Idle */ +#monitorSideList [data-status-code="6"] > .btn { + border-color: #ddd; +} +/* Died */ +#monitorSideList [data-status-code="7"] > .btn { + border-color: #ff0000; + animation: border-pulsate-died 2s infinite; +} +/* Stopping */ +#monitorSideList [data-status-code="8"] > .btn { + border-color: #ff0000; + animation: border-pulsate-stopping 2s infinite; +} diff --git a/web/assets/css/bs5.streamCarousel.css b/web/assets/css/bs5.streamCarousel.css new file mode 100644 index 00000000..e9158564 --- /dev/null +++ b/web/assets/css/bs5.streamCarousel.css @@ -0,0 +1,47 @@ +#stream-carousel{ + overflow: hidden; + height: 400px; + position: relative; +} +#stream-carousel iframe{ + width: 100%; + height: 100%; +} +#stream-carousel .stream-carousel-inner{ + width: 100%; + height: 100%; +} +#stream-carousel .stream-carousel-label{ + padding: 0.5rem 2rem; + position: absolute; + right: 0; + bottom: 0; + border-radius: 5px 0 0 0; + background: rgba(0,0,0,0.6); + color: #fff; +} +#stream-carousel .carousel-block { + display: block; + transition: 0.2s; + overflow: hidden; + width: 100%; + height: 0px; +} +#stream-carousel .carousel-block.active-block { + height: 100%; + width: 100%; +} +#stream-carousel .stream-carousel-go { + position: absolute; + height: 100%; + width: 100%; + top: 0; + opacity: 0; +} +#stream-carousel .stream-carousel-go-arrow { + cursor: pointer; + text-shadow: 0 0 10px #333; +} +#stream-carousel:hover .stream-carousel-go { + opacity: 1; +} diff --git a/web/assets/css/bs5.timelapseViewer.css b/web/assets/css/bs5.timelapseViewer.css new file mode 100644 index 00000000..4cbbe338 --- /dev/null +++ b/web/assets/css/bs5.timelapseViewer.css @@ -0,0 +1,94 @@ +#tab-timelapseViewer .frames .frame{ + height: 100px; +} +#tab-timelapseViewer .frames .frame img{ + width: 100%; + height: 100%; +} +#tab-timelapseViewer .frameStrip img{ + position:absolute; + width:100%; + height:100%; + top:0; + left:0; +} +#tab-timelapseViewer .modal-body > .row, +#tab-timelapseViewer .modal-body > .row > div { + position:relative; + height: 100%; +} +#tab-timelapseViewer .frameIcons .frame{ + border-radius: 15px; + overflow: hidden; +} +#tab-timelapseViewer .frameIcons .frame.selected{ + border: 3px solid #ffc96b; +} +#tab-timelapseViewer .frameIcons > div:not(:last-child){ + margin-bottom: 15px +} +#tab-timelapseViewer .frameIcons{ + overflow-y: auto; + overflow-x: hidden; + max-height: 400px; +} +#tab-timelapseViewer .frameIcons .frame{ + background-size: cover; + width: 100%; + height: 100px; + position: relative; + cursor: pointer; +} +#tab-timelapseViewer .frameIcons .frame .shade, +#tab-timelapseViewer .frameIcons .button-strip{ + position: absolute; + bottom: 0; + left: 0; + width: 100%; + padding: 5px 10px; + font-family: monospace; + background: rgba(0,0,0,0.6); + color: #fff +} +#tab-timelapseViewer .frameIcons .button-strip{ + top: 0; + bottom: auto; + opacity: 0; + transition: 0.2; + text-align: right; +} +#tab-timelapseViewer .frameIcons .frame:hover .button-strip{ + opacity: 1; +} +#tab-timelapseViewer .playBackView{ + width: 100%; + background:#333; +} +#tab-timelapseViewer .playBackView img{ + width:100%; + border-radius: 4px; +} +#tab-timelapseViewer .liveStreamView{ + position:absolute; + text-align: center; + height: 50%; + width:100%; + top:50%; + left:0; + background:#333; +} +#tab-timelapseViewer .liveStreamView iframe{ + width:100%; + height:100%; + display: inline-block; + margin: auto; + border: 0; +} +/* stand alone */ +#tab-timelapseViewer.standalone { + background: #2f2f2f; + color:#fff; +} +#tab-timelapseViewer.standalone .fieldHolder{ + padding-top: 20px; +} diff --git a/web/assets/css/bs5.videoPlayer.css b/web/assets/css/bs5.videoPlayer.css new file mode 100644 index 00000000..8ca1ab7f --- /dev/null +++ b/web/assets/css/bs5.videoPlayer.css @@ -0,0 +1,96 @@ +.tab-videoPlayer video{ + width: 100%; + background: #333; +} +.tab-videoPlayer-event-row { + cursor: pointer; +} +.videoPlayer-event-objects .tag { + position: absolute; + bottom: 100%; + left: 0; + background: red; + color: #fff; + font-family: monospace; + font-size: 80%; + border-radius: 5px 5px 0 0; + padding: 3px 5px; +} + +.tab-videoPlayer-event-objects .stream-detected-object { + position: absolute; + top: 0; + left: 0; + border: 3px solid red; + background: transparent; + border-radius: 5px +} + +.tab-videoPlayer-view-container:hover .tab-videoPlayer-event-objects { + display: none; +} + +/* video-time-strip */ +.video-time-img { + background-color: #000; + background-size: contain; + background-position: center; + height: 400px; + min-height: 400px; + background-repeat: no-repeat; +} + +/* @media screen and (max-width: 992px) { + .video-time-img { + min-height: 400px; + } +} + +@media screen and (max-width: 600px) { + .video-time-img { + min-height: 400px; + } +} */ + +.video-time-img:not(.video-time-no-img) .card-body:hover { + background: rgba(0,0,0,0.9); + color: #fff; +} +.video-time-strip { + min-height: 30px; + position: relative; +} +.video-time-needle { + position: absolute; + border-left: 3px solid #1f80f9; + height: 100%; + transition: none; + top: 0; +} +.video-time-needle-event { + border-left: 3px solid #fffb00; + border-radius: 0; + z-index: 999; +} +.video-time-needle-seeker { + border-left: 3px solid #f00; +} +.video-day-slice { + height: 100%; + background: #1f80f9; +} +.video-day-slice-spacer { + background: rgba(0,0,0,0.3); +} +.video-time-card { + position: relative; + border-radius: 10px; + overflow: hidden; +} +.video-time-header { + position: absolute; + left: 0; + top: 0; + width: 100%; + background: rgba(0,0,0,0.6) +} diff --git a/web/assets/css/bs5.videosTable.css b/web/assets/css/bs5.videosTable.css new file mode 100644 index 00000000..cf896a52 --- /dev/null +++ b/web/assets/css/bs5.videosTable.css @@ -0,0 +1,28 @@ +.video-thumbnail { + position: relative; + overflow: hidden; +} +.video-thumbnail img { + height: 75px; + border-radius: 10px; +} +.video-thumbnail-buttons { + position: absolute; + height: 100%; + width:100%; + top:0; + left:0; +} +.video-thumbnail-buttons > .video-thumbnail-button-cell { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + text-decoration: none; + text-shadow: 0 0 15px #333; + cursor: pointer; +} +.video-thumbnail-buttons > .video-thumbnail-button-cell:hover { + background: rgba(0,0,0,0.6); + color: #fff; +} diff --git a/web/assets/css/clock.css b/web/assets/css/clock.css new file mode 100644 index 00000000..be2e42a3 --- /dev/null +++ b/web/assets/css/clock.css @@ -0,0 +1,4 @@ +#clock { text-align:center; } +#clock ul { width:100%; margin:0 auto; padding:0px; list-style:none; } +#clock ul li { display:inline; font-size:1.6em; text-align:center;font-family:monospace;} +#clock .point { position:relative; -moz-animation:mymove 1s ease infinite; -webkit-animation:mymove 1s ease infinite; } diff --git a/web/assets/css/dashboard.css b/web/assets/css/dashboard.css new file mode 100644 index 00000000..3b547536 --- /dev/null +++ b/web/assets/css/dashboard.css @@ -0,0 +1,437 @@ +html, +body { + overflow-x: hidden; /* Prevent scroll on narrow devices */ +} + +* { + transition: 0.2s; +} + +*::-webkit-scrollbar-track +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + border-radius: 10px; + background-color: #333; +} + +*::-webkit-scrollbar +{ + width: 6px; + background-color: #0ae; +} + +*::-webkit-scrollbar-thumb +{ + border-radius: 10px; + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,.3); + background-color: #0ae; +} + +.scroll-style-6::-webkit-scrollbar-track +{ + -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3); + background-color: #333; + border-radius: 15px; +} + +.scroll-style-6::-webkit-scrollbar +{ + width: 10px; + background-color: #333; +} + +.scroll-style-6::-webkit-scrollbar-thumb +{ + background-color: #555; + background-image: -webkit-linear-gradient(45deg, + rgba(255, 255, 255, .2) 25%, + transparent 25%, + transparent 50%, + rgba(255, 255, 255, .2) 50%, + rgba(255, 255, 255, .2) 75%, + transparent 75%, + transparent) +} + +.grid-stack * { + transition: none; +} + +.progress { + height: 5px; +} + +.hidden { + display: none; +} + +.bg-dark a.nav-link:not([href]):hover { + color:#fff; +} + +.btn-default { + background-color: #2b3c4b; + border-color: #2b3c4b; + color: #fff; +} + +.btn-default:hover { + color: #fff; +} + +.form-check-input.no-abs { + position: relative; + margin-left: 0!important; +} + +.border-size-1 { + border-width: 1px; +} + +.border-dotted { + border-style: dotted; +} + +.border-bottom-dotted { + border-bottom-style: dotted; +} + +.border-bottom-dark { + border-bottom-color: #2b3c4b; +} + +.border-color-red{border-color:#d9534f} +.border-color-purple{border-color:#3f51b5} +.border-color-blue{border-color:#5a82ce} +.border-color-navy{border-color:#0858ab} +.border-color-green{border-color:#449d44} +.border-color-forestgreen{border-color:#27b392} +.border-color-orange{border-color:#c49a68} +.border-color-grey{border-color:#777} +.border-color-dark{border-color:#4d4d4d} + +.card.red{border: 1px solid rgba(217, 83, 79, 0.2)} +.card.purple{border: 1px solid rgba(63, 81, 181, 0.2)} +.card.blue{border: 1px solid rgba(90, 130, 206, 0.2)} +.card.navy{border: 1px solid rgba(8, 88, 171, 0.2)} +.card.green{border: 1px solid rgba(68, 157, 68, 0.2)} +.card.forestgreen{border: 1px solid rgba(39, 179, 146, 0.2)} +.card.orange{border: 1px solid rgba(196, 154, 104, 0.2)} +.card.grey{border: 1px solid rgba(119, 119, 119, 0.2)} +.card.dark{border: 1px solid rgba(77, 77, 77, 0.2)} + +.table th, .table td { + border: 0; +} + +.border-top-dotted { + border-top-style: dotted; +} + +.border-top-dark { + border-top-color: #4d4d4d; +} + +.dot { + width:10px; + height:10px; + display:inline-block; + border-radius: 50%; + margin-right: 5px; + box-shadow: 0 0 5px #1e2b37; +} + +.dot-red {background:#d9534f} +.dot-purple {background:#3f51b5} +.dot-blue {background:#375182} +.dot-navy {background:#0858ab} +.dot-green {background:#449d44} +.dot-forestgreen {background:#408693} +.dot-orange {background:#c49a68} +.dot-grey {background:#777} + +.slidemenu .nav-link:hover { + color: #9abadd!important; +} + +.slidemenu .nav-link.page-link-active { + color: #007bff!important; +} + +@media (max-width: 991.98px) { + .offcanvas-collapse { + position: fixed; + top: 56px; /* Height of navbar */ + bottom: 0; + left: 100%; + width: 100%; + padding-right: 1rem; + padding-left: 1rem; + overflow-y: auto; + visibility: hidden; + background-color: #343a40; + transition: transform .3s ease-in-out, visibility .3s ease-in-out; + } + .offcanvas-collapse.open { + visibility: visible; + transform: translateX(-100%); + } +} + +.nav-scroller { + position: relative; + z-index: 2; + height: 2.75rem; + overflow-y: hidden; +} + +.nav-scroller .nav { + display: flex; + flex-wrap: nowrap; + padding-bottom: 1rem; + margin-top: -1px; + overflow-x: auto; + color: rgba(255, 255, 255, .75); + text-align: center; + white-space: nowrap; + -webkit-overflow-scrolling: touch; +} + +.nav-underline .nav-link { + padding-top: .75rem; + padding-bottom: .75rem; + font-size: .875rem; + color: #6c757d; +} + +.nav-underline .nav-link:hover { + color: #007bff; +} + +.nav-underline .active { + font-weight: 500; + color: #343a40; +} + +.text-white-50 { color: rgba(255, 255, 255, .5); } + +.bg-purple { background-color: #6f42c1; } +.bg-darker { background-color: #121417; } + +/* a { + color: #277dfb; + text-decoration: none; +} */ + +.nav-link, .cursor-pointer { + cursor: pointer; +} + +.text-monospace { + font-family: monospace; +} +.text-ellipsis { + text-overflow: ellipsis; +} +.float{ + +} + +.hidden-empty:empty { + display: none; +} + + +.my-float{ +} +#floating-back-button, .sticky-button { + position: fixed; + width: 35px; + height: 35px; + top: 10px; + left: 10px; + background-color: #fff; + color: #333; + border-radius: 50px; + text-align: center; + z-index: 1030; + cursor: pointer; +} +#floating-back-button i, .sticky-button i { + margin-top: 9px; +} +.sticky-button { + top: initial; + left: initial; + right: 10px; + bottom: 10px; + background-color: #449d44; +} + +.sticky-bar { + top: initial; + left: initial; + right: 0; + bottom: 0; + background-color: rgba(0,0,0,0.5); + position: fixed; + border-radius: 15px 0 0 15px; +} + +.video_video{ + margin: auto; + max-width: 100%; + max-height: 600px; +} + +.ui-pnotify-title { + font-size: 1rem; + line-height: 1; +} + +.monitor-card-preview { + width: 100%; + height: 100px; + background-position: center; + background-size: cover; +} + +/* */ +.side-menu-link { + position: relative; +} +.side-menu-link .delete-tab { + position: absolute; + top: 5px; + right: 5px; +} +.side-menu-link .delete-tab:hover { + text-shadow: 0 0 15px #333; + color: #eee; +} + +/* */ + +ul.squeeze { + flex-direction: row!important; + align-items: center; + justify-content: center; +} +.squeeze .monitor-icon { + display: inline-block; + margin: 0 0.1rem; +} + +.squeeze .monitor-icon .btn{ + padding: 0!important; + overflow: hidden; + margin-bottom: 0.2rem!important; + border:0; + position: relative; +} + +.monitor-icon img{ + border-radius: 5px; + height: 48px; + width: 48px; +} + +/* */ + +.text-epic { + text-transform: uppercase; + font-weight: normal; + letter-spacing: 3px; + font-family: monospace; +} + +/* Flex Table (Dark Table) */ +.flex-table.flex-table-dark { + +} +.flex-table > .d-flex > *{ + padding: .5rem 1rem; +} +.flex-table.flex-table-dark > .d-flex:nth-child(even){ + background: rgba(0,0,0,0.4); +} + +/* image pop */ +.popped-image img{ + position: absolute; + right: 0; + top: 0; + bottom: 0; + left: 0; + margin: auto; + max-width: 100%; +} +.popped-image { + position: fixed; + top:0; + left:0; + z-index: 1000; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.6); +} + + +/* */ +.vertical-center { + display: flex; + align-items: center; + justify-content: center; +} +.position-initial { + position: initial; +} +.position-absolute { + position: absolute; +} +.position-fixed { + position: fixed; +} + +/* Shake Animation */ +.animate-shake { + animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; +} + +.animate-shake-hover:hover { + animation: shake 0.82s cubic-bezier(.36,.07,.19,.97) both; + transform: translate3d(0, 0, 0); + backface-visibility: hidden; + perspective: 1000px; +} + +@keyframes shake { + 10%, 90% { + transform: translate3d(-1px, 0, 0); + } + + 20%, 80% { + transform: translate3d(2px, 0, 0); + } + + 30%, 50%, 70% { + transform: translate3d(-4px, 0, 0); + } + + 40%, 60% { + transform: translate3d(4px, 0, 0); + } +} + +.bg-dark .bootstrap-table .table, +.bg-dark .bootstrap-table .table td { + color: #fff!important; +} + +.grid { + background-image: repeating-linear-gradient(rgb(204 204 204 / 50%) 0 1px, transparent 1px 100%), repeating-linear-gradient(90deg, rgb(204 204 204 / 50%) 0 1px, transparent 1px 100%); + background-size: 71px 71px; +} diff --git a/web/assets/css/gradients.css b/web/assets/css/gradients.css new file mode 100644 index 00000000..ccb97af5 --- /dev/null +++ b/web/assets/css/gradients.css @@ -0,0 +1,36 @@ +/* Gradients from https://uigradients.com/ */ +.bg-gradient-dark { + background: #000428; /* fallback for old browsers */ + background: -webkit-linear-gradient(to left, #004e92, #000428); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to left, #004e92, #000428); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ +} +.bg-gradient-blue { + background: #0575E6; /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #021B79, #0575E6); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #021B79, #0575E6); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ +} +.bg-gradient-red { + background: #ED213A; /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #93291E, #ED213A); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #93291E, #ED213A); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ +} +.bg-gradient-orange { + background: #f12711; /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #f5af19, #f12711); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #f5af19, #f12711); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ +} +.bg-gradient-yellow { + background: #CAC531; /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #F3F9A7, #CAC531); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #F3F9A7, #CAC531); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ +} +.bg-gradient-green { + background: #134E5E; /* fallback for old browsers */ + background: -webkit-linear-gradient(to left, #116134, #134E5E); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to left, #116134, #134E5E); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ +} +.bg-gradient-pinkgreen { + background: #aa4b6b; /* fallback for old browsers */ + background: -webkit-linear-gradient(to right, #3b8d99, #6b6b83, #aa4b6b); /* Chrome 10-25, Safari 5.1-6 */ + background: linear-gradient(to right, #3b8d99, #6b6b83, #aa4b6b); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */ +} diff --git a/web/assets/css/login.css b/web/assets/css/login.css new file mode 100644 index 00000000..a76d30a4 --- /dev/null +++ b/web/assets/css/login.css @@ -0,0 +1,51 @@ +.shinobi-bg-shade{ + position:absolute; + height:100%; + width:100%; +} +.bg-none { + background: transparent; +} +.monospace{ + font-family: monospace; +} +.full-height { + height: 100%; +} +.vertical-center { + display: flex; + align-items: center; + justify-content: center; +} +.text-color { + color: #28b391!important; +} +.text-white { + color: #fff!important; +} +hr.divider { + border-top: 1px solid #595959; + background-color: initial; +} +#page-contents { + background: #334a6d; +} +.social-link.btn { + border-radius: 50%; + width: 40px; + height: 40px; + display: inline-block; + font-size: 1.2rem; + border: 0; +} +.card label { + color: #fff; +} +#login-submit { + background: #2596fb; +} +#remember_me { + width: 15px; + height: 15px; + transform: translate(0px, 3px); +} diff --git a/web/assets/css/modal.dark.css b/web/assets/css/modal.dark.css new file mode 100644 index 00000000..86987ca3 --- /dev/null +++ b/web/assets/css/modal.dark.css @@ -0,0 +1,10 @@ +.modal-dark .modal-content { + background: #222; + color: #fff; +} +.modal-dark .modal-header, +.modal-dark .modal-footer + { + background: #333; + border-color: #222; +} diff --git a/web/assets/css/super-page.css b/web/assets/css/super-page.css new file mode 100644 index 00000000..ea671435 --- /dev/null +++ b/web/assets/css/super-page.css @@ -0,0 +1,311 @@ +.navbar.bg-primary,.nav.bg-primary { + background-color: #903619 !important; +} +.navbar.bg-success { + background-color: #1f791b !important; +} +.super-system-info{ + padding: 1rem; + overflow: auto!important; +} +.super-system-info > ul{ + margin: 0; + padding: 0; +} +.form-group-group table tr:first-child > td { + border-top: 0; +} +.form-group label>div:first-child {width:40%} +.list-group li .form-group {margin:0} +pre ul {margin:0;padding:0;list-style: none;} +pre ul > li > ul {padding-left: 10px!important;} +a {cursor:pointer} +.flex-grow-1 { + flex: 1!important; +} +.card .card-body { + min-height: auto; +} +.flex-padding.d-flex.flex-row > div { + padding: 1rem; +} +/* Now Navbar */ + +.nav-tabs { + border: 0; + padding: 15px 0.7rem; +} + +.nav-tabs>.nav-item>.nav-link { + color: #888; + margin: 0; + margin-right: 5px; + background-color: transparent; + border: 1px solid transparent; + border-radius: 30px; + font-size: 14px; + padding: 11px 23px; + line-height: 1.5; +} + +.nav-tabs>.nav-item>.nav-link:hover { + background-color: transparent; +} + +.nav-tabs>.nav-item>.nav-link.active { + border: 1px solid #888; + border-radius: 30px; +} + +.nav-tabs>.nav-item>.nav-link i.now-ui-icons { + font-size: 14px; + position: relative; + top: 1px; + margin-right: 3px; +} + +.nav-tabs>.nav-item.disabled>.nav-link, +.nav-tabs>.nav-item.disabled>.nav-link:hover { + color: rgba(255, 255, 255, 0.5); +} + +.nav-tabs.nav-tabs-neutral>.nav-item>.nav-link { + color: #FFFFFF; +} + +.nav-tabs.nav-tabs-neutral>.nav-item>.nav-link.active { + border-color: rgba(255, 255, 255, 0.5); + color: #FFFFFF; +} + +.nav-tabs.nav-tabs-primary>.nav-item>.nav-link.active { + border-color: #f96332; + color: #f96332; +} + +.nav-tabs.nav-tabs-info>.nav-item>.nav-link.active { + border-color: #2CA8FF; + color: #2CA8FF; +} + +.nav-tabs.nav-tabs-danger>.nav-item>.nav-link.active { + border-color: #FF3636; + color: #FF3636; +} + +.nav-tabs.nav-tabs-warning>.nav-item>.nav-link.active { + border-color: #FFB236; + color: #FFB236; +} + +.nav-tabs.nav-tabs-success>.nav-item>.nav-link.active { + border-color: #18ce0f; + color: #18ce0f; +} + +.navbar-rounded { + border-radius: 15px; +} +.navbar { + padding-top: 0.625rem; + padding-bottom: 0.625rem; + min-height: 53px; + margin-bottom: 20px; + box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.15); +} + +.navbar a { + vertical-align: middle; +} + +.navbar a:not(.btn):not(.dropdown-item) { + color: #FFFFFF; +} + +.navbar p { + display: inline-block; + margin: 0; + line-height: 21px; +} + +.navbar .navbar-nav.navbar-logo { + position: absolute; + left: 0; + right: 0; + margin: 0 auto; + width: 49px; + top: -4px; +} + +.navbar .navbar-nav .nav-link.btn { + padding: 11px 22px; +} + +.navbar .navbar-nav .nav-link.btn.btn-lg { + padding: 15px 48px; +} + +.navbar .navbar-nav .nav-link.btn.btn-sm { + padding: 5px 15px; +} + +.navbar .navbar-nav .nav-link:not(.btn) { + text-transform: uppercase; + font-size: 0.7142em; + padding: 0.5rem 0.7rem; + line-height: 1.625rem; +} + +.navbar .navbar-nav .nav-link:not(.btn) i.fa+p, +.navbar .navbar-nav .nav-link:not(.btn) i.now-ui-icons+p { + margin-left: 5px; +} + +.navbar .navbar-nav .nav-link:not(.btn) i.fa, +.navbar .navbar-nav .nav-link:not(.btn) i.now-ui-icons { + font-size: 18px; + position: relative; + top: 2px; + text-align: center; + width: 21px; +} + +.navbar .navbar-nav .nav-link:not(.btn) i.now-ui-icons { + top: 4px; + font-size: 16px; +} + +.navbar .navbar-nav .nav-link:not(.btn).profile-photo .profile-photo-small { + width: 27px; + height: 27px; +} + +.navbar .navbar-nav .nav-link:not(.btn).disabled { + opacity: .5; + color: #FFFFFF; +} + +.navbar .navbar-nav .nav-item.active .nav-link:not(.btn), +.navbar .navbar-nav .nav-item .nav-link:not(.btn):focus, +.navbar .navbar-nav .nav-item .nav-link:not(.btn):hover, +.navbar .navbar-nav .nav-item .nav-link:not(.btn):active { + background-color: rgba(255, 255, 255, 0.2); + border-radius: 0.1875rem; +} + +.navbar .logo-container { + width: 27px; + height: 27px; + overflow: hidden; + margin: 0 auto; + border-radius: 50%; + border: 1px solid transparent; +} + +.navbar .navbar-brand { + text-transform: uppercase; + font-size: 0.8571em; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + line-height: 1.625rem; +} + +.navbar .navbar-toggler { + width: 37px; + height: 27px; + outline: 0; + cursor: pointer; +} + +.navbar .navbar-toggler.navbar-toggler-left { + position: relative; + left: 0; + padding-left: 0; +} + +.navbar .navbar-toggler:hover .navbar-toggler-bar.bar2 { + width: 22px; +} + +.navbar .button-dropdown .navbar-toggler-bar:nth-child(2) { + width: 17px; +} + +.navbar.navbar-transparent { + background-color: transparent !important; + box-shadow: none; + color: #FFFFFF; + padding-top: 20px; +} + +.navbar.bg-white:not(.navbar-transparent) a:not(.dropdown-item) { + color: #888; +} + +.navbar.bg-white:not(.navbar-transparent) a:not(.dropdown-item).disabled { + opacity: .5; + color: #888; +} + +.navbar.bg-white:not(.navbar-transparent) .button-bar { + background: #888; +} + +.navbar.bg-white:not(.navbar-transparent) .nav-item.active .nav-link:not(.btn), +.navbar.bg-white:not(.navbar-transparent) .nav-item .nav-link:not(.btn):focus, +.navbar.bg-white:not(.navbar-transparent) .nav-item .nav-link:not(.btn):hover, +.navbar.bg-white:not(.navbar-transparent) .nav-item .nav-link:not(.btn):active { + background-color: rgba(222, 222, 222, 0.3); +} + +.navbar.bg-white:not(.navbar-transparent) .logo-container { + border: 1px solid #888; +} + +/* .bg-default { + background-color: #888 !important; +} + +.bg-primary { + background-color: #f96332 !important; +} + +.bg-info { + background-color: #2CA8FF !important; +} + +.bg-success { + background-color: #18ce0f !important; +} + +.bg-danger { + background-color: #FF3636 !important; +} + +.bg-warning { + background-color: #FFB236 !important; +} + +.bg-white { + background-color: #FFFFFF !important; +} */ +/* Slim Style */ +.form-control { + /* background-color: transparent; + border: 1px solid #E3E3E3; + border-radius: 30px; + color: #2c2c2c; + line-height: normal; + font-size: 0.8571em; + box-shadow: none; */ +} +.btn-round { + /* border-width: 1px; */ + /* border-radius: 30px !important; */ +} +.btn, .navbar .navbar-nav>a.btn { + font-weight: 400; + line-height: 1.35em; + margin: 5px 1px; + border: none; +} diff --git a/web/libs/css/super.configEditor.css b/web/assets/css/super.configEditor.css similarity index 69% rename from web/libs/css/super.configEditor.css rename to web/assets/css/super.configEditor.css index 040574b0..0347cf32 100644 --- a/web/libs/css/super.configEditor.css +++ b/web/assets/css/super.configEditor.css @@ -3,8 +3,8 @@ margin: 10px 10px 20px 0; } .better-json-editor h3 > span{ - color: #bd4147; - background-color: #f8f9fa; + color: #fff; + background-color: #132232; padding: 7px 10px; border-radius: .25rem; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; @@ -14,12 +14,13 @@ margin-left:20px; } .better-json-editor label, .better-json-editor .control-label { - color: #bd4147; - background-color: #f8f9fa; - padding: 7px 10px; - border-radius: .25rem; + color: #fff; + background-color: #132232; + padding: 5px; + border-radius: 0.25rem; font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; - margin-bottom:5px; + margin-bottom: 5px; + font-size: 80%; } .better-json-editor .floating-json { padding: 10px; @@ -34,7 +35,13 @@ margin-bottom: 10px; overflow: hidden; border-radius: 5px; - background: #fbfbfb; + background: #132232; + color: #fff; +} +.better-json-editor table tr, +.better-json-editor table th, +.better-json-editor table.table-bordered { + border: 0; } .better-json-editor .floating-json textarea.form-control { padding: 20px; @@ -58,9 +65,19 @@ margin: 0; } .better-json-editor .row > div { - border: 1px solid #eee; + border: 0; + background-color: #1e2b37 !important; border-radius: 5px; padding-top: 15px; padding-bottom: 15px; margin-bottom: 10px; } +.better-json-editor .badge.badge-default { + background: #ffffff; + color: #333; + border: 0; + padding: 5px 7px 5px 5px; +} +.better-json-editor .fa-caret-square-o-down:before { + content: "\f0d7"; +} diff --git a/web/libs/css/super.customAutoLoad.css b/web/assets/css/super.customAutoLoad.css similarity index 100% rename from web/libs/css/super.customAutoLoad.css rename to web/assets/css/super.customAutoLoad.css diff --git a/web/libs/css/super.easyRemoteAccess.css b/web/assets/css/super.easyRemoteAccess.css similarity index 73% rename from web/libs/css/super.easyRemoteAccess.css rename to web/assets/css/super.easyRemoteAccess.css index bb3aaba7..692f183f 100644 --- a/web/libs/css/super.easyRemoteAccess.css +++ b/web/assets/css/super.easyRemoteAccess.css @@ -1,12 +1,11 @@ -#easyRemoteAccess .card { - cursor: pointer; +#easyRemoteAccess .card * { transition: none; } #easyRemoteAccess .card.active { - background-color: #18ce0f !important; + background-color: #088c4c !important; color: #fff; } -#easyRemoteAccess .card.active *{ +#easyRemoteAccess .card.active * { color: #fff!important; } #easyRemoteAccess .card.active .card-header, @@ -45,6 +44,15 @@ #easyRemoteAccess .card.selected .selected-badge { display: inline-block; } -#easyRemoteAccess .card.active .table-striped tbody tr:nth-of-type(odd) { +#p2pServerList .card.active .table-striped tbody tr:nth-of-type(odd) { background-color: rgb(49 204 68 / 5%); } +#p2pServerList .card .d-flex.flex-row > div { + padding: 1rem; +} +#p2pServerList .card.active .d-flex.flex-row .epic-text-filter { + font-weight: bold; +} +#p2pServerList .card .d-flex.flex-row:nth-of-type(odd) { + background: rgba(0,0,0,0.1) +} diff --git a/web/assets/img/splash.avif b/web/assets/img/splash.avif new file mode 100644 index 00000000..b41f5a73 Binary files /dev/null and b/web/assets/img/splash.avif differ diff --git a/web/assets/js/bs5.accountSettings.js b/web/assets/js/bs5.accountSettings.js new file mode 100644 index 00000000..9be1df98 --- /dev/null +++ b/web/assets/js/bs5.accountSettings.js @@ -0,0 +1,199 @@ +$(document).ready(function(){ + var accountSettingsWereSaved = false; + var theBlock = $('#tab-accountSettings') + var theContainer = $('#accountSettingsContainer') + var theForm = $('#settings') + var addStorageMaxAmounts = $('#add_storage_max_amounts') + var addStorageMaxAmountsField = theForm.find('[detail="addStorage"]') + var monitorGroups = $('#settings_mon_groups') + function drawAddStorageFields(){ + try{ + var addStorageData = JSON.parse($user.details.addStorage || '{}') + var html = '' + $.each(addStorage,function(n,storage){ + var limit = "" + if(addStorageData[storage.path] && addStorageData[storage.path].limit){ + limit = addStorageData[storage.path].limit + } + html += `
+
${lang['Max Storage Amount']} : ${storage.name}
+
+
` + }) + addStorageMaxAmounts.html(html) + }catch(err){ + console.log(err) + } + } + function writewMonGroups(){ + theForm.find('[detail="mon_groups"]').val(JSON.stringify($user.mon_groups)).change() + } + window.getMonitorGroups = function(){ + return $user.mon_groups ? $user.mon_groups : safeJsonParse($user.details.mon_groups) + } + window.reDrawMonGroupsInAccountSettings = function(){ + $user.mon_groups = getMonitorGroups() + var monitorList = Object.values($user.mon_groups).map(function(item){ + return { + value: item.id, + label: item.name, + } + }); + monitorGroups.html(createOptionListHtml(monitorList)) + monitorGroups.change() + } + function getAndWriteMonitorGroups(){ + var theValue = monitorGroups.val().trim() + if(!theValue){ + var el = theForm.find('[group="name"]') + theValue = el.val() + theForm.find('.mon_groups .add').click(); + var selectedGroup = monitorGroups.val() + el.val(name) + } + var selectedGroup = $user.mon_groups[theValue]; + theForm.find('[group]').each(function(n,v){ + var el = $(v) + var groupName = el.attr('group') + selectedGroup[groupName] = el.val() + }) + $user.mon_groups[theValue] = selectedGroup; + monitorGroups.find(`option[value="${monitorGroups.val()}"]`).text(selectedGroup.name) + writewMonGroups() + } + function createNewMonitorGroup(){ + var newId = generateId(5); + var newGroup = { + id: newId, + name: newId + } + $user.mon_groups[newId] = newGroup + monitorGroups.append(createOptionHtml({ + value: newGroup.id, + label: newGroup.name, + })) + monitorGroups.val(newId) + monitorGroups.change() + } + function deleteMonitorGroup(theValue){ + delete($user.mon_groups[theValue]) + reDrawMonGroupsInAccountSettings() + } + function fillFormFields(){ + $.each($user,function(n,v){ + theForm.find(`[name="${n}"]`).val(v).change() + }) + $.each($user.details,function(n,v){ + theForm.find(`[detail="${n}"]`).val(v).change() + }) + reDrawMonGroupsInAccountSettings() + accountSettings.onLoadFieldsExtensions.forEach(function(extender){ + extender(theForm) + }) + } + addStorageMaxAmounts.on('change','[addStorageLimit]',function(){ + var json = {} + $.each(addStorage,function(n,storage){ + var storageId = storage.path + var el = addStorageMaxAmounts.find('[addStorageLimit="' + storageId + '"]') + var value = el.val() + json[storageId] = { + name: storage.name, + path: storage.path, + limit: value + } + }) + addStorageMaxAmountsField.val(JSON.stringify(json)) + }) + $('body') + theForm.find('[detail]').change(onDetailFieldChange) + theForm.find('[detail]').change(function(){ + onDetailFieldChange(this) + }) + theForm.find('[selector]').change(function(){ + onSelectorChange(this,theForm) + }) + theForm.find('[group]').change(getAndWriteMonitorGroups) + theForm.submit(function(e){ + e.preventDefault() + writewMonGroups() + var formData = theForm.serializeObject() + var errors = [] + if(formData.pass !== '' && formData.password_again !== formData.pass){ + errors.push(lang[`Passwords don't match`]) + } + if(errors.length > 0){ + new PNotify({ + title: lang.accountSettingsError, + text: errors.join('
'), + type: 'danger' + }) + return + } + $.each(formData,function(n,v){ + formData[n] = v.trim() + }) + var details = getDetailValues(theForm) + formData.details = details + accountSettings.onSaveFieldsExtensions.forEach(function(extender){ + extender(formData) + }) + $.post(getApiPrefix('accounts') + '/edit',{ + data: JSON.stringify(formData) + },function(data){ + if(data.ok){ + // new PNotify({ + // title: lang['Settings Changed'], + // text: lang.SettingsChangedText, + // type: 'success' + // }) + } + }) + if(details.googd_save === '1' && details.googd_code && details.googd_code !== '***************'){ + theForm.find('[detail="googd_code"]').val('***************') + } + return false + }) + monitorGroups.change(function(e){ + var selectedValue = $(this).val() + var selectedGroup = $user.mon_groups[selectedValue]; + if(!selectedGroup){return} + $.each(selectedGroup,function(key,value){ + theForm.find(`[group="${key}"]`).val(value) + }) + }) + theForm.on('click','.mon_groups .delete',function(e){ + var theValue = monitorGroups.val() + deleteMonitorGroup(theValue) + }) + theForm.on('click','.mon_groups .add',createNewMonitorGroup) + fillFormFields() + drawAddStorageFields() + drawSubMenuItems('accountSettings',definitions['Account Settings']) + mainSocket.on('f',function (d){ + switch(d.f){ + case'user_settings_change': + new PNotify({ + title: lang['Settings Changed'], + text: lang.SettingsChangedText, + type: 'success' + }) + // $.ccio.init('id',d.form); + d.form.details = safeJsonParse(d.form.details) + $('#custom_css').html(d.form.details.css) + if(d.form.details){ + $user.details = d.form.details + } + sortListMonitors() + accountSettingsWereSaved = true; + break; + } + }) + addOnTabReopen('accountSettings', function () { + if(accountSettingsWereSaved){ + accountSettingsWereSaved = false; + fillFormFields() + } + reDrawMonGroupsInAccountSettings() + }) +}) diff --git a/web/libs/js/dash2.apiwindow.js b/web/assets/js/bs5.apiKeys.js similarity index 59% rename from web/libs/js/dash2.apiwindow.js rename to web/assets/js/bs5.apiKeys.js index 6360073e..1ae619d7 100644 --- a/web/libs/js/dash2.apiwindow.js +++ b/web/assets/js/bs5.apiKeys.js @@ -9,23 +9,27 @@ $(document).ready(function(e){ $.each(rowDetails,function(key,value){ var foundOption = permissionSelector.find(`option[value="${key}"]`) var label = foundOption.text() - newDetails += `
  ${label}
` + newDetails += `   ${label}` }) return newDetails } window.drawApiKeyRow = function(row){ - var html = ` - - ${row.code} -
-
${moment(row.time).format(`DD-MM-YYYY hh:mm:ss A`)}
-
${row.ip}
-
${getHumanNamesForRowDetails(row.details || {})}
- - -    - - `; + var html = `
+
+
+
+ ${row.code}
+ ${row.ip}
+ ${moment(Date.parse(row.time) || new Date()).format(`DD-MM-YYYY hh:mm:ss A`)} +
+ +
+
${getHumanNamesForRowDetails(row.details || {})}
+
+
`; apiKeyTable.prepend(html) } var writePermissionsFromFieldsToString = function(){ @@ -44,7 +48,7 @@ $(document).ready(function(e){ detailsElement.val(JSON.stringify(details)) } var getApiKeys = function(callback){ - $.get(getApiPrefix('api') + '/list',function(data){ + $.getJSON(getApiPrefix('api') + '/list',function(data){ callback(data.keys) }) } @@ -54,7 +58,7 @@ $(document).ready(function(e){ errors.push(lang['Enter at least one IP']) } if(errors.length > 0){ - $.ccio.init('note',{title:lang['API Key Action Failed'],text:errors.join('
'),type:'danger'}); + new PNotify({title:lang['API Key Action Failed'],text:errors.join('
'),type:'danger'}); return } $.each(formValues,function(n,v){ @@ -64,7 +68,7 @@ $(document).ready(function(e){ data: JSON.stringify(formValues) },function(data){ if(data.ok){ - $.ccio.init('note',{title:lang['API Key Added'],text:lang.FiltersUpdatedText,type:'success'}); + new PNotify({title:lang['API Key Added'],text:lang.FiltersUpdatedText,type:'success'}); drawApiKeyRow(data.api) } }) @@ -82,7 +86,7 @@ $(document).ready(function(e){ code: code },function(data){ if(data.ok){ - $.ccio.init('note',{title:lang['API Key Deleted'],text:lang.APIKeyDeletedText,type:'notice'}); + new PNotify({title:lang['API Key Deleted'],text:lang.APIKeyDeletedText,type:'notice'}); apiKeyTable.find('[api_key="'+code+'"]').remove() } }) @@ -101,12 +105,20 @@ $(document).ready(function(e){ var code = el.attr('api_key') deleteApiKey(code) }) - theWindow.on('shown.bs.modal',function(e){ - getApiKeys(function(apiKeys){ - apiKeyTable.empty() - $.each(apiKeys,function(n,row){ - drawApiKeyRow(row) - }) + theWindow.on('click','.copy',function(e){ + var el = $(this).parents('[api_key]') + var code = el.attr('api_key') + copyToClipboard(code) + new PNotify({ + title: lang['Copied'], + text: lang['Copied to Clipboard'], + type: 'success' + }) + }) + getApiKeys(function(apiKeys){ + apiKeyTable.empty() + $.each(apiKeys,function(n,row){ + drawApiKeyRow(row) }) }) }) diff --git a/web/assets/js/bs5.calendar.js b/web/assets/js/bs5.calendar.js new file mode 100644 index 00000000..7740ee4e --- /dev/null +++ b/web/assets/js/bs5.calendar.js @@ -0,0 +1,111 @@ +$(document).ready(function(e){ + //Timelapse JPEG Window + var theEnclosure = $('#tab-calendarView') + var monitorsList = theEnclosure.find('.monitors_list') + var dateSelector = theEnclosure.find('.date_selector') + var calendarDrawArea = $('#calendar_draw_area') + var loadedVideosInMemory = {}; + var openCalendarView = function(monitorId,startDate,endDate){ + drawCalendarViewElements(monitorId,startDate,endDate) + } + var getSelectedTime = function(asUtc){ + var dateRange = dateSelector.data('daterangepicker') + var startDate = dateRange.startDate.clone() + var endDate = dateRange.endDate.clone() + if(asUtc){ + startDate = startDate.utc() + endDate = endDate.utc() + } + startDate = startDate.format('YYYY-MM-DDTHH:mm:ss') + endDate = endDate.format('YYYY-MM-DDTHH:mm:ss') + return { + startDate: startDate, + endDate: endDate + } + } + + dateSelector.daterangepicker({ + startDate: moment().utc().subtract(2, 'days'), + endDate: moment().utc(), + timePicker: true, + locale: { + format: 'YYYY/MM/DD hh:mm:ss A' + } + }, function(start, end, label) { + drawCalendarViewElements() + }) + monitorsList.change(function(){ + drawCalendarViewElements() + }) + var drawCalendarViewElements = function(selectedMonitor,startDate,endDate){ + var dateRange = getSelectedTime(false) + if(!startDate)startDate = dateRange.startDate + if(!endDate)endDate = dateRange.endDate + if(!selectedMonitor)selectedMonitor = monitorsList.val() + var queryString = ['start=' + startDate,'end=' + endDate,'limit=0'] + var frameIconsHtml = '' + var apiURL = getApiPrefix('videos') + '/' + selectedMonitor; + var calendarData = [] + loadedVideosInMemory = {} + $.getJSON(apiURL + '?' + queryString.join('&'),function(data){ + $.each(data.videos,function(n,v){ + if(v.status !== 0){ + loadedVideosInMemory[`${v.mid}${v.time}`] = Object.assign({},v) + var loadedMonitor = loadedMonitors[v.mid] + if(loadedMonitor){ + v.title = loadedMonitor.name+' - '+(parseInt(v.size)/1048576).toFixed(2)+'mb'; + } + v.start = moment.utc(v.time).local() + v.end = moment.utc(v.end).local() + calendarData.push(v) + } + }) + try{ + calendarDrawArea.fullCalendar('destroy') + }catch(er){ + + } + calendarDrawArea.fullCalendar({ + header: { + left: 'prev,next today', + center: 'title', + right: 'month,agendaWeek,agendaDay,listWeek' + }, + defaultDate: moment(startDate).format('YYYY-MM-DD'), + locale: $user.details.lang.substring(0, 2), + navLinks: true, + eventLimit: true, + events: calendarData, + eventClick: function(v){ + var video = loadedVideosInMemory[`${v.mid}${v.time}`] + createVideoPlayerTab(video) + $(this).css('border-color', 'red'); + } + }); + + }) + } + + $('body').on('click','.open-timelapse-viewer',function(){ + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + openTab(`calendarView`,{},null) + monitorsList.val(monitorId).change() + }) + theEnclosure + .on('click','.refresh-data',function(e){ + e.preventDefault() + drawCalendarViewElements() + return false; + }) + addOnTabOpen('calendarView', function () { + drawMonitorListToSelector(monitorsList) + drawCalendarViewElements() + }) + addOnTabReopen('calendarView', function () { + var theSelected = `${monitorsList.val()}` + drawMonitorListToSelector(monitorsList) + monitorsList.val(theSelected) + drawCalendarViewElements() + }) +}) diff --git a/web/libs/js/dash2.probe.js b/web/assets/js/bs5.cameraProbe.js similarity index 87% rename from web/libs/js/dash2.probe.js rename to web/assets/js/bs5.cameraProbe.js index 65ef1019..570dee7c 100644 --- a/web/libs/js/dash2.probe.js +++ b/web/assets/js/bs5.cameraProbe.js @@ -1,7 +1,7 @@ $(document).ready(function(e){ //probe var loadedProbe = {} - var probeWindow = $('#probe') + var probeWindow = $('#tab-cameraProbe') var probeForm = probeWindow.find('form') var outputView = probeWindow.find('.output_data') var setAsLoading = function(appearance){ @@ -15,6 +15,9 @@ $(document).ready(function(e){ probeWindow.find('[type="submit"]').prop('disabled',false).html(' ' + lang.FFprobe) } } + function writeProbeResult(jsonString){ + outputView.append(jsonToHtmlBlock(safeJsonParse(jsonString))) + } probeForm.submit(function(e){ e.preventDefault() setAsLoading(true) @@ -22,20 +25,20 @@ $(document).ready(function(e){ var form = el.serializeObject() var flags = 'default' var url = form.url.trim() - $.get(`${getApiPrefix()}/probe/${$user.ke}?url=${url}`,function(data){ + $.getJSON(`${getApiPrefix()}/probe/${$user.ke}?url=${url}`,function(data){ if(data.ok === true){ var html try{ loadedProbe = data.result loadedProbe.url = url - html = $.ccio.init('jsontoblock',loadedProbe) + html = jsonToHtmlBlock(loadedProbe) }catch(err){ console.log(err) html = data.result } outputView.append(html) }else{ - $.ccio.init('note',{title:'Failed to Probe',text:$.stringJSON(data.error),type:'error'}); + new PNotify({title:'Failed to Probe',text:prettyPrint(data.error),type:'error'}); } setAsLoading(false) }) @@ -134,24 +137,31 @@ $(document).ready(function(e){ console.log('No Probe Result Loaded!') } }) - probeWindow.on('hidden.bs.modal',function(){ - outputView.empty() - }) probeWindow.find('.stop').click(function(e){ el = $(this) $.get(`${getApiPrefix()}/probe/${$user.ke}?action=stop`,function(data){ setAsLoading(false) }) }) + mainSocket.on('f',function(d){ + switch(d.f){ + case'ffprobe_stop': + setAsLoading(false) + break; + case'ffprobe_start': + setAsLoading(true) + break; + case'ffprobe_data': + writeProbeResult(d.data) + break; + } + }) $.pB = { submit: function(url,show){ probeWindow.find('[name="url"]').val(url) probeForm.submit() - if(show)probeWindow.modal('show') + // if(show)probeWindow.modal('show') + alert('open Prober') }, - writeData: function(jsonString){ - outputView.append($.ccio.init('jsontoblock',JSON.parse(jsonString))) - }, - setAsLoading: setAsLoading } }) diff --git a/web/libs/js/dash2.shinobiHub.js b/web/assets/js/bs5.configFinder.js similarity index 59% rename from web/libs/js/dash2.shinobiHub.js rename to web/assets/js/bs5.configFinder.js index 9695b26b..1210590d 100644 --- a/web/libs/js/dash2.shinobiHub.js +++ b/web/assets/js/bs5.configFinder.js @@ -1,16 +1,31 @@ $(document).ready(function(){ var loadedConfigs = {} - var shinobiHubWindow = $('#shinobihub_viewer') - var shinobiHubWindowTableBody = shinobiHubWindow.find('tbody') + var shinobiHubWindow = $('#tab-configFinder') + var shinobiHubWindowTableBody = $('#shinobihub-results') var shinobiHubWindowSearch = $('#shinobihub-search') var shinobiHubWindowSortBy = $('#shinobihub-sort-by') var shinobiHubWindowExplore = $('#shinobihub-explore') var shinobiHubWindowSortDirection = $('#shinobihub-sort-direction') var shinobiHubWindowPages = $('#shinobihub-pages') - + var sideMenuList = $(`#side-menu-link-configFinder ul`) + var pageLimit = 15 + function drawFoundConfigsSubMenu(){ + var allFound = [] + Object.keys(loadedConfigs).forEach(function(configId){ + var item = loadedConfigs[configId] + allFound.push({ + attributes: `href="#monitor-settings-found-${configId}" scrollToParent="#tab-configFinder"`, + class: `scrollTo`, + color: 'blue', + label: item.brand + ' ' + item.name, + }) + }) + var html = buildSubMenuItems(allFound) + sideMenuList.html(html) + } var getConfigurationsFromHub = function(rowLimit,skipOver,explore,searchQuery,sortBy,sortDirection,callback){ // $.get(,callback) - $.get(`https://hub.shinobi.video/searchConfiguration?skipOver=${skipOver}&rowLimit=${rowLimit}&sortBy=${sortBy}&sortDirection=${sortDirection}`,function(data){ + $.getJSON(`https://hub.shinobi.video/searchConfiguration?skipOver=${skipOver}&rowLimit=${rowLimit}&sortBy=${sortBy}&sortDirection=${sortDirection}${searchQuery ? `&text=${searchQuery}` : ''}`,function(data){ callback(data) // $.get(getApiPrefix() + `/getShinobiHubConfigurations/${$user.ke}/cam?rowLimit=${rowLimit}&skipOver=${skipOver}&explore=${explore ? explore : "0"}&search=${searchQuery}&sortDirection=${sortDirection}&sortBy=${sortBy}`,function(privateData){ // callback(data.concat(privateData || [])) @@ -18,20 +33,32 @@ $(document).ready(function(){ }) } var buildConfigRow = function(row){ - return ` - ${row.name} - ${row.brand} - ${row.description} - ${moment(row.dateAdded).format('DD-MM-YYYY hh:mm:ss A')} - ${moment(row.dateUpdated).format('DD-MM-YYYY hh:mm:ss A')} - - - ` + return `
+
+
+
+ ${row.brand} ${row.name} +
+
+ +
+
+
+
${row.description}
+ ${lang['Date Added']} : ${moment(row.dateAdded).format('DD-MM-YYYY hh:mm:ss A')} + ${lang['Date Updated']} : ${moment(row.dateUpdated).format('DD-MM-YYYY hh:mm:ss A')} +
+ +
+
` } var loadRows = function(skipOver,rowLimit,explore){ shinobiHubWindowTableBody.empty() if(!skipOver)skipOver = 0 - if(!rowLimit)rowLimit = 10 + if(!rowLimit)rowLimit = pageLimit + loadedConfigs = {} var searchQuery = shinobiHubWindowSearch.val() var sortBy = shinobiHubWindowSortBy.val() var sortDirection = shinobiHubWindowSortDirection.val() @@ -62,33 +89,31 @@ $(document).ready(function(){ } } shinobiHubWindowPages.html(html) + drawFoundConfigsSubMenu() }) } - shinobiHubWindow.on('shown.bs.modal',function(){ + addOnTabOpen('configFinder', function () { loadRows() }) + addOnTabReopen('configFinder', function () { + // loadRows() + }) shinobiHubWindow.on('click','.copy',function(){ + openMonitorEditorPage() var configId = $(this).parents(`[drawn-id]`).attr('drawn-id') var json = loadedConfigs[configId].json - $.aM.import({ - values: mergeDeep($.aM.generateDefaultMonitorSettings(),json) - }) - $.aM.e.find('[name="mode"]').val('start').change() - shinobiHubWindow.modal('hide') - $.aM.e.modal('show') + writeToMonitorSettingsWindow(json) }) shinobiHubWindowPages.on('click','.page-select',function(){ var pageSelect = parseInt($(this).attr('page')) - 1 - var pageLimit = 10 loadRows(pageSelect * pageLimit, pageLimit,'0') }) shinobiHubWindow.on('change','.page-number-input',function(){ var pageSelect = parseInt($(this).val()) - 1 - var pageLimit = 10 loadRows(pageSelect * pageLimit, pageLimit,'0') }) shinobiHubWindowSearch.change(function(){ - loadRows(0, 10, '0') + loadRows(0, pageLimit, '0') }) shinobiHubWindowSortBy.change(function(){ var descText @@ -107,12 +132,12 @@ $(document).ready(function(){ } shinobiHubWindowSortDirection.find('[value="DESC"]').html(descText) shinobiHubWindowSortDirection.find('[value="ASC"]').html(ascText) - loadRows(0, 10, '0') + loadRows(0, pageLimit, '0') }) shinobiHubWindowSortDirection.change(function(){ - loadRows(0, 10, '0') + loadRows(0, pageLimit, '0') }) shinobiHubWindowExplore.change(function(){ - loadRows(0, 10, '0') + loadRows(0, pageLimit, '0') }) }) diff --git a/web/libs/js/dash2.confirm.js b/web/assets/js/bs5.confirm.js similarity index 100% rename from web/libs/js/dash2.confirm.js rename to web/assets/js/bs5.confirm.js diff --git a/web/assets/js/bs5.dashboard-base.js b/web/assets/js/bs5.dashboard-base.js new file mode 100644 index 00000000..aecc77c2 --- /dev/null +++ b/web/assets/js/bs5.dashboard-base.js @@ -0,0 +1,1080 @@ +PNotify.prototype.options.styling = "fontawesome"; +var isSubAccount = !!$user.details.sub +var loadedMonitors = {} +var tabTree = null +var pageLoadingData = {} +var pageTabLinks = $('#pageTabLinks') +var createdTabLinks = $('#createdTabLinks') +var pageTabContainer = $('#pageTabContainer') +var floatingBackButton = $('#floating-back-button') +var loadedPages = {} +var activeTabName = 'initial' +var chartColors = { + red: 'rgb(255, 99, 132)', + orange: 'rgb(255, 159, 64)', + yellow: 'rgb(255, 205, 86)', + green: 'rgb(75, 192, 192)', + blue: 'rgb(54, 162, 235)', + purple: 'rgb(153, 102, 255)', + grey: 'rgb(201, 203, 207)' +}; +var isAppleDevice = navigator.userAgent.match(/(iPod|iPhone|iPad)/)||(navigator.userAgent.match(/(Safari)/)&&!navigator.userAgent.match('Chrome')); +var isMobile = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(navigator.userAgent) + || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(navigator.userAgent.substr(0,4)) +function base64ArrayBuffer(arrayBuffer) { + var base64 = '' + var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' + + var bytes = new Uint8Array(arrayBuffer) + var byteLength = bytes.byteLength + var byteRemainder = byteLength % 3 + var mainLength = byteLength - byteRemainder + + var a, b, c, d + var chunk + + // Main loop deals with bytes in chunks of 3 + for (var i = 0; i < mainLength; i = i + 3) { + // Combine the three bytes into a single integer + chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2] + + // Use bitmasks to extract 6-bit segments from the triplet + a = (chunk & 16515072) >> 18 // 16515072 = (2^6 - 1) << 18 + b = (chunk & 258048) >> 12 // 258048 = (2^6 - 1) << 12 + c = (chunk & 4032) >> 6 // 4032 = (2^6 - 1) << 6 + d = chunk & 63 // 63 = 2^6 - 1 + + // Convert the raw binary segments to the appropriate ASCII encoding + base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d] + } + + // Deal with the remaining bytes and padding + if (byteRemainder == 1) { + chunk = bytes[mainLength] + + a = (chunk & 252) >> 2 // 252 = (2^6 - 1) << 2 + + // Set the 4 least significant bits to zero + b = (chunk & 3) << 4 // 3 = 2^2 - 1 + + base64 += encodings[a] + encodings[b] + '==' + } else if (byteRemainder == 2) { + chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1] + + a = (chunk & 64512) >> 10 // 64512 = (2^6 - 1) << 10 + b = (chunk & 1008) >> 4 // 1008 = (2^6 - 1) << 4 + + // Set the 2 least significant bits to zero + c = (chunk & 15) << 2 // 15 = 2^4 - 1 + + base64 += encodings[a] + encodings[b] + encodings[c] + '=' + } + + return base64 +} +function getLocationPathName(){ + return location.pathname.endsWith('/') ? location.pathname : location.pathname + '/' +} +function getUrlProtocol(urlString){ + let modifiedUrlString = `${urlString}`.split('://') + const originalProtocol = `${modifiedUrlString[0]}` + return originalProtocol +} +function modifyUrlProtocol(urlString,newProtocol){ + let modifiedUrlString = `${urlString}`.split('://') + const originalProtocol = `${modifiedUrlString[0]}` + modifiedUrlString[0] = newProtocol; + modifiedUrlString = modifiedUrlString.join('://') + return modifiedUrlString +} +function getUrlParts(urlString){ + const originalProtocol = getUrlProtocol(urlString) + const modifiedUrlString = modifyUrlProtocol(urlString,'http') + const url = new URL(modifiedUrlString) + const data = {} + $.each(url,function(key,value){ + data[key] = value + }); + data.href = `${urlString}` + data.origin = modifyUrlProtocol(data.origin,originalProtocol) + data.protocol = `${originalProtocol}:` + delete(data.toString) + delete(data.toJSON) + return data +} +function addCredentialsToUrl(streamUrl,username,password){ + const urlParts = streamUrl.split('://') + return [urlParts[0],'://',`${username}:${password}@`,urlParts[1]].join('') +} +function mergeConcattedJsonString(textData){ + return textData.replace(/[\r\n]/gm, '').replace('}{',',') +} +function getFullOrigin(withoutTrailingSlash){ + var url = location.origin + getLocationPathName() + if(withoutTrailingSlash)url = url.slice(0, -1); + return url +} +function debugLog(...args){ + console.log(...args) +} +function generateId(x){ + if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for( var i=0; i < x; i++ ) + t += p.charAt(Math.floor(Math.random() * p.length)); + return t; +} +function removeSpecialCharacters(stringToReplace){ + return stringToReplace.replace(/[^\w\s]/gi, '').replace(/\s+/g, ''); +} +const mergeDeep = function(...objects) { + const isObject = obj => obj && typeof obj === 'object'; + + return objects.reduce((prev, obj) => { + Object.keys(obj).forEach(key => { + const pVal = prev[key]; + const oVal = obj[key]; + + if (Array.isArray(pVal) && Array.isArray(oVal)) { + prev[key] = pVal.concat(...oVal); + } + else if (isObject(pVal) && isObject(oVal)) { + prev[key] = mergeDeep(pVal, oVal); + } + else { + prev[key] = oVal; + } + }); + + return prev; + }, {}); +} +function dashboardOptions(r,rr,rrr){ + if(!rrr){rrr={};};if(typeof rrr === 'string'){rrr={n:rrr}};if(!rrr.n){rrr.n='ShinobiOptions_'+location.host} + ii={o:localStorage.getItem(rrr.n)};try{ii.o=JSON.parse(ii.o)}catch(e){ii.o={}} + if(!ii.o){ii.o={}} + if(r&&rr&&!rrr.x){ + ii.o[r]=rr; + } + switch(rrr.x){ + case 0: + delete(ii.o[r]) + break; + case 1: + delete(ii.o[r][rr]) + break; + } + localStorage.setItem(rrr.n,JSON.stringify(ii.o)) + return ii.o +} +function getLocation(d){ + var url + if(d && d.info && d.info.URL){ + url = d.info.URL + if(url.charAt(url.length-1) !== '/'){ + url = url+'/' + } + }else{ + url = libURL + } + return url +} +function getApiPrefix(path,isAdmin){ + var mainPart = getLocation() + (window.adminApiPrefix && isAdmin ? `${window.adminApiPrefix}` : '') + $user.auth_token + return path ? mainPart + '/' + path + '/' + $user.ke : mainPart +} + +function formattedTime(time,twelveHourClock,utcConvert){ + var theMoment = moment(time) + if(utcConvert)theMoment = theMoment.clone().utc() + return theMoment.format(twelveHourClock ? 'hh:mm:ss A YYYY-MM-DD' : 'HH:mm:ss YYYY-MM-DD') +} + +function durationBetweenTimes(start,end){ + var duration = moment.duration(moment(end).diff(moment(start))); + console.log(duration) + var hours = duration.asMinutes().toFixed(0); + return hours +} + +function formattedTimeForFilename(time,utcConvert,timeFormat){ + var theMoment = moment(time) + if(utcConvert)theMoment = theMoment.clone().utc() + return theMoment.format(timeFormat ? timeFormat : 'YYYY-MM-DDTHH:mm:ss') +} + +function checkCorrectPathEnding(x){ + var length=x.length + if(x.charAt(length-1)!=='/'){ + x=x+'/' + } + return x +} + +function safeJsonParse(string){ + if(string instanceof Object){ + return string + }else{ + + } + var newObject = {} + try{ + newObject = JSON.parse(string) + }catch(err){ + + } + return newObject +} +function prettyPrint(string){ + return JSON.stringify(string,null,3) +} + +function liveStamp(){ + var allLiveStampable = $('.livestamp') + allLiveStampable.each(function(){ + var el = jQuery(this) + var time = el.attr('title') + if(!time){ + return + }; + el.toggleClass('livestamp livestamped') + .attr('title',formattedTime(time)) + .livestamp(time); + }) + return allLiveStampable +} + +function loadMonitorsIntoMemory(callback){ + $.getJSON(`${getApiPrefix(`monitor`)}`,function(data){ + $.each(data,function(n,monitor){ + monitor.details = safeJsonParse(monitor.details) + loadedMonitors[monitor.mid] = monitor + }) + callback(data) + }) +} + +function compileConnectUrl(options){ + var porty = '' + if(options.port && options.port !== ''){ + porty = ':' + options.port + } + var url = options.protocol + '://' + options.host + porty + return options.url +} + +function jsonToHtmlBlock(target){ + var html = '' + if(target instanceof Object){ + $.each(target,function(key,value){ + html += `
+ ${key} : ${jsonToHtmlBlock(value)} +
` + }) + }else{ + html += `${target}` + } + return html +} + +function fullScreenInit(target){ + if (target.requestFullscreen) { + target.requestFullscreen(); + } else if (target.mozRequestFullScreen) { + target.mozRequestFullScreen(); + } else if (target.webkitRequestFullscreen) { + target.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } +} + +function saveTabBlipPosition(tabId){ + var loadedTab = loadedPages[tabId] + if(!loadedTab)return; + var theDoc = document.documentElement + loadedTab.bodyScrollX = parseFloat(`${theDoc.scrollTop}`) + loadedTab.bodyScrollY = parseFloat(`${theDoc.scrollLeft}`) +} +function loadTabBlipPosition(tabId){ + var loadedTab = loadedPages[tabId] + if(!loadedTab)return; + blipTo(loadedTab.bodyScrollX || 0,loadedTab.bodyScrollY || 0) +} + +function blipTo(xPageValue,yPageValue){ + document.documentElement.style.scrollBehavior = 'auto'; + setTimeout(() => window.scrollTo(yPageValue, xPageValue), 5); + setTimeout(() => document.documentElement.style.scrollBehavior = 'smooth', 5); +} + +function openTab(theTab,loadData,backAction,haltTrigger,type,overrideOnTabOpen){ + loadData = loadData ? loadData : {} + if(tabTree && tabTree.back && tabTree.back.name === theTab){ + goBackOneTab() + return; + } + saveTabBlipPosition(activeTabName) + var allTabs = $('.page-tab'); + allTabs.hide().removeClass('tab-active'); + $(`#tab-${theTab}`).show().addClass('tab-active'); + pageLoadingData = Object.assign(loadData,{}); + if(!haltTrigger){ + tabTree = { + name: theTab, + loadData: loadData, + back: tabTree, + } + $('body').trigger(`tab-open-${theTab}`); + } + if(tabTree && tabTree.back){ + floatingBackButton.show() + }else{ + floatingBackButton.hide() + } + var targetUiElement = pageTabLinks.find(`.side-menu-link[page-open="${theTab}"]`) + if(targetUiElement.length > 0){ + pageTabLinks.find(`.side-menu-link`).removeClass('page-link-active active') + pageTabLinks.find('ul').hide(); + targetUiElement.addClass('page-link-active active').parents('li').find('ul').show(); + } + // + $('[tab-specific-content]').hide() + $(`[tab-specific-content="${theTab}"]`).show() + // + onTabAway(activeTabName) + activeTabName = `${theTab}`; + if(!loadedPages[theTab]){ + loadedPages[theTab] = { + name: theTab, + loadData: loadData, + type: type || 'other' + } + overrideOnTabOpen ? overrideOnTabOpen(activeTabName) : onTabOpen(activeTabName) + }else{ + overrideOnTabOpen ? overrideOnTabOpen(activeTabName) : onTabReopen(activeTabName) + } + // mobile-only, close menu on page change + $('#sidebarMenu').removeClass('show'); +} + +function goBackOneTab(){ + if(tabTree.backAction)tabTree.backAction() + tabTree = tabTree.back + if(tabTree){ + if($(`#tab-${tabTree.name}`).length === 0)goBackOneTab(); + openTab(tabTree.name,tabTree.loadData,tabTree.backAction,true) + } +} + +function goBackHome(){ + if(tabTree.back){ + goBackOneTab() + goBackHome() + }else{ + pageTabLinks.find(`.side-menu-link`).removeClass('page-link-active active'); + pageTabLinks.find(`.side-menu-link.go-home`).addClass('page-link-active active'); + } +} + +function createNewTab(tabName,tabLabel,baseHtml,loadData,backAction,type){ + var theTab = $(`#tab-${tabName}`) + var existAlready = true + if(theTab.length === 0){ + var tabIcon = 'file-o' + switch(type){ + case'videoPlayer': + tabIcon = 'play-circle' + break; + case'livePlayer': + tabIcon = 'eye' + break; + case'videosList': + tabIcon = 'film' + break; + } + existAlready = false + pageTabContainer.append(baseHtml) + createdTabLinks.append(buildTabHtml(tabName,tabLabel,tabIcon)) + theTab = $(`#tab-${tabName}`) + } + openTab(tabName,loadData,backAction,null,type) + return { + existAlready: existAlready, + theTab: theTab, + } +} + +function deleteTab(tabId){ + $(`[page-open="${tabId}"]`).remove() + $(`#tab-${tabId}`).remove() + onTabClose(tabId) + delete(loadedPages[tabId]) +} + +var addedOnTabAway = {} +function addOnTabAway(tabId,action){ + if(!addedOnTabAway[tabId])addedOnTabAway[tabId] = [] + addedOnTabAway[tabId].push(action) +} + +function onTabAway(tabId){ + var loadedTab = loadedPages[tabId] + if(!loadedTab)return + var type = loadedTab.type + switch(type){ + case'videoPlayer': + pauseVideoPlayer(tabId) + break; + case'livePlayer': + break; + } + if(addedOnTabAway[tabId]){ + addedOnTabAway[tabId].forEach(function(theAction){ + try{ + theAction(loadedTab) + }catch(err){ + console.log(err) + } + }) + } +} + +var addedOnTabReopen = {} +function addOnTabReopen(tabId,action){ + if(!addedOnTabReopen[tabId])addedOnTabReopen[tabId] = [] + addedOnTabReopen[tabId].push(action) +} + +function onTabReopen(tabId){ + var loadedTab = loadedPages[tabId] + if(!loadedTab)return + var type = loadedTab.type + loadTabBlipPosition(tabId) + console.log(`onTabReopen`,tabId,type) + switch(type){ + case'videoPlayer': + resumeVideoPlayer(tabId) + break; + case'livePlayer': + break; + } + if(addedOnTabReopen[tabId]){ + addedOnTabReopen[tabId].forEach(function(theAction){ + try{ + theAction(loadedTab) + }catch(err){ + console.log(err) + } + }) + } +} + +var addedOnTabOpen = {} +function addOnTabOpen(tabId,action){ + if(!addedOnTabOpen[tabId])addedOnTabOpen[tabId] = [] + addedOnTabOpen[tabId].push(action) +} + +function onTabOpen(tabId){ + var loadedTab = loadedPages[tabId] + if(!loadedTab)return + var type = loadedTab.type + loadTabBlipPosition(tabId) + if(addedOnTabOpen[tabId]){ + addedOnTabOpen[tabId].forEach(function(theAction){ + theAction(loadedTab) + }) + } +} + + +var addedOnTabClose = {} +function addOnTabClose(tabId,action){ + addedOnTabClose[tabId] = action +} + +function onTabClose(tabId){ + var loadedTab = loadedPages[tabId] + if(!loadedTab)return + var type = loadedTab.type + console.log(`onTabClose`,tabId,type) + switch(type){ + case'videoPlayer': + closeVideoPlayer(tabId) + break; + case'livePlayer': + closeLivePlayer(tabId) + break; + } + if(addedOnTabClose[tabId])addedOnTabClose[tabId](loadedTab) +} + +// function saveTabStates(){ +// localStorage.setItem(`Shinobi-Tab-States-${location.origin}`,JSON.stringify(loadedPages)) +// } +// +// function loadTabStates(){ +// var tabStates = JSON.parse(localStorage.getItem(`Shinobi-Tab-States-${location.origin}`)) || {} +// $.each(tabStates,function(tabId,data){ +// +// }) +// } + +function getDetailValues(parentForm){ + var theList = {} + var allDetailFieldsInThisForm = parentForm.find('[detail]') + allDetailFieldsInThisForm.each(function(n,v){ + var el = $(v) + var detailParam = el.attr('detail') + var theValue = el.val() + theList[detailParam] = theValue + }) + return theList +} + +function onDetailFieldChange(_this){ + var parentForm = $(_this).parents('form'); + parentForm.find('[name="details"]').val(JSON.stringify(getDetailValues(parentForm))); +} + +function onSelectorChange(_this,parent){ + var el = $(_this) + var theParam = el.attr('selector') + var theValue = el.val() + var theSelected = el.find('option:selected').text() + parent.find(`.${theParam}_input`).hide() + parent.find(`.${theParam}_${theValue}`).show() + parent.find(`.${theParam}_text`).text(theSelected) +} + +function createOptionHtml(options){ + return `` +} + +function createOptionListHtml(list){ + var html = `` + $.each(list,function(n,options){ + html += createOptionHtml(options) + }) + return html +} + +function copyToClipboard(str){ + const el = document.createElement('textarea'); + el.value = str; + el.setAttribute('readonly', ''); + el.style.position = 'absolute'; + el.style.left = '-9999px'; + document.body.appendChild(el); + el.select(); + document.execCommand('copy'); + document.body.removeChild(el); +}; + +function diffObject(obj1, obj2) { + if (!obj2 || Object.prototype.toString.call(obj2) !== '[object Object]') { + return obj1; + } + var diffs = {}; + var key; + var arraysMatch = function (arr1, arr2) { + if (arr1.length !== arr2.length) return false; + for (var i = 0; i < arr1.length; i++) { + if (arr1[i] !== arr2[i]) return false; + } + return true; + + }; + + var compare = function (item1, item2, key) { + var type1 = Object.prototype.toString.call(item1); + var type2 = Object.prototype.toString.call(item2); + if (type2 === '[object Undefined]') { + // diffs[key] = null; + return; + } + if (item2 && type1 !== type2) { + diffs[key] = item2; + return; + } + if (type1 === '[object Object]') { + var objDiff = diffObject(item1, item2); + if (objDiff && Object.keys(objDiff).length > 0) { + diffs[key] = objDiff; + } + return; + } + if (type1 === '[object Array]') { + if (item2 && !arraysMatch(item1, item2)) { + diffs[key] = item2; + } + return; + } + if (type1 === '[object Function]') { + if (item2 && item1.toString() !== item2.toString()) { + diffs[key] = item2; + } + } else { + if (item2 && item1 !== item2) { + diffs[key] = item2; + } + } + }; + for (key in obj1) { + if (obj1.hasOwnProperty(key)) { + compare(obj1[key], obj2[key], key); + } + } + for (key in obj2) { + if (obj2.hasOwnProperty(key)) { + if (obj2[key] && !obj1[key] && obj1[key] !== obj2[key] ) { + diffs[key] = obj2[key]; + } + } + } + return diffs; +} + +function getAllSectionsFromDefinition(definitionsBase){ + var sections = {} + var addSection = function(section,parentName){ + sections[section.id + section.name] = { + name: section.name, + id: section.id, + color: section.color, + parentName: parentName + } + if(section.info){ + $.each(section.info,function(m,block){ + if(block.isFormGroupGroup === true){ + addSection(block,section.name) + } + }) + } + if(section.blocks){ + $.each(section.blocks,function(m,block){ + addSection(block,section.name) + }) + } + } + $.each(definitionsBase.blocks,function(n,section){ + addSection(section) + }) + return sections +} + +function buildSubMenuItems(listOfItems){ + var html = '' + $.each(listOfItems,function(n,item){ + if(item)html += `
  • ${item.label}
  • ` + }) + return html +} + +function drawSubMenuItems(linkTarget,definitionsBase){ + var list = $(`#side-menu-link-${linkTarget} ul`) + window[`sectionList-${linkTarget}`] = window[`sectionList-${linkTarget}`] || getAllSectionsFromDefinition(definitionsBase) + var sectionSubLinks = Object.values(window[`sectionList-${linkTarget}`]).map(function(item){ + var sectionId = item.id + if(!sectionId)return null + var sectionElement = document.getElementById(`${sectionId}`) + if(sectionElement && $(sectionElement).is(":hidden"))return null; + var parentName = item.parentName + var completeLabel = `${item.name}` + return { + attributes: `href="#${sectionId}" scrollToParent="#tab-${linkTarget}"`, + class: `scrollTo`, + color: item.color, + hasParent: !!parentName, + label: completeLabel, + } + }) + var html = buildSubMenuItems(sectionSubLinks) + list.html(html) +} + +function getMonitorIdFromElement(_this){ + var monitorId + var thisEl = $(_this) + var monitorId = thisEl.attr('data-mid') + if(!monitorId){ + var el = thisEl.parents('[data-mid]') + monitorId = el.attr('data-mid') + } + return monitorId +} + +function permissionCheck(toCheck,monitorId){ + var details = $user.details + if(details.sub && details.allmonitors === '0'){ + var chosenValue = details[toCheck] + if(details[toCheck] instanceof Array && chosenValue.indexOf(monitorId) > -1){ + return true + }else if(chosenValue === '1'){ + return true + } + }else{ + return true + } + return false +} + +function drawMonitorListToSelector(jqTarget,selectFirst,showId,addAllMonitorsOption){ + var html = '' + $.each(loadedMonitors,function(n,v){ + html += createOptionHtml({ + value: v.mid, + label: v.name + (showId === 'host' ? ` (${v.host})` : showId ? ` (${v.mid})` : ''), + }) + }) + addAllMonitorsOption ? jqTarget.html(` + + ${html} + `) : jqTarget.html(html); + if(selectFirst){ + jqTarget + .find('option') + .first() + .prop('selected',true) + .parent() + .change() + } +} +var logWriterIconIndicator = $('#side-menu-link-logViewer i') +var logWriterIconIndicatorShaking = false +var logWriterIconIndicatorTimeout = null +function shakeLogWriterIcon(){ + if(logWriterIconIndicatorShaking)return; + logWriterIconIndicatorShaking = true; + logWriterIconIndicator.addClass('animate-shake') + logWriterIconIndicatorTimeout = setTimeout(function(){ + logWriterIconIndicatorShaking = false; + logWriterIconIndicator.removeClass('animate-shake') + },3000) +} +var logWriterFloodTimeout = null +var logWriterFloodCounter = 0 +var logWriterFloodLock = null +function buildLogRow(v){ + var monitor = loadedMonitors[v.mid] + var humanMonitorName = monitor ? monitor.name + ` (${monitor.mid}) : ` : '' + var html = '' + html += `
    +
    + ${humanMonitorName}${v.info && v.info.type ? v.info.type : v.mid} +
    +
    +
    ${jsonToHtmlBlock(v.info.msg)}
    +
    + +
    ` + return html +} +function logWriterDraw(id,data){ + if(logWriterFloodLock)return debugLog('logWriterFloodLock : Log was dropped'); + var elementTags = '#global-log-stream,'+id+'.monitor_item .logs:visible,'+id+'#tab-monitorSettings:visible .logs' + if(logWriterFloodTimeout){ + ++logWriterFloodCounter + } + if(logWriterFloodCounter > 10){ + window.logWriterFloodLock = setTimeout(function(){ + delete(logWriterFloodLock) + },10000) + } + clearTimeout(logWriterFloodTimeout) + logWriterFloodTimeout = setTimeout(function(){ + delete(logWriterFloodTimeout) + logWriterFloodCounter = 0 + },2000) + if(!data.time)data.time = formattedTime(); + var html = buildLogRow({ + ke: data.ke, + mid: data.mid, + info: data.log, + time: data.time, + }) + shakeLogWriterIcon() + $(elementTags).prepend(html).each(function(n,v){ + var el = $(v); + var theRows = el.find('.log-item') + if(theRows.length > 10){ + theRows.last().remove() + } + }) +} + +function setSwitchUIState(systemSwitch,toggleState){ + var el = $(`[shinobi-switch="${systemSwitch}"]`) + var onClass = el.attr('on-class') + var offClass = el.attr('off-class') + var childTarget = el.attr('ui-change-target') + if(childTarget)el = el.find(childTarget) + if(onClass || offClass){ + if(toggleState === 1){ + if(onClass)el.addClass(onClass) + if(offClass)el.removeClass(offClass) + }else{ + if(onClass)el.removeClass(onClass) + if(offClass)el.addClass(offClass) + } + } +} +var dashboardSwitchCallbacks = {} +function runDashboardSwitchCallback(systemSwitch){ + var theSwitches = dashboardOptions().switches + var afterAction = dashboardSwitchCallbacks[systemSwitch] + if(afterAction){ + afterAction(theSwitches[systemSwitch]) + } +} +function dashboardSwitch(systemSwitch){ + var theSwitches = dashboardOptions().switches + if(!theSwitches){ + theSwitches={} + } + if(!theSwitches[systemSwitch]){ + theSwitches[systemSwitch]=0 + } + if(theSwitches[systemSwitch]===1){ + theSwitches[systemSwitch]=0 + }else{ + theSwitches[systemSwitch]=1 + } + dashboardOptions('switches',theSwitches) + runDashboardSwitchCallback(systemSwitch) + setSwitchUIState(systemSwitch,theSwitches[systemSwitch]) +} + +function downloadJSON(jsonData,filename){ + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData,null,3)); + $('#temp').html('') + .find('a') + .attr('href',dataStr) + .attr('download',filename) + [0].click() +} +function downloadFile(downloadUrl,fileName){ + var a = document.createElement('a') + a.href = downloadUrl + a.download = fileName + a.click() +} +function convertKbToHumanSize(theNumber){ + var amount = theNumber / 1048576 + var unit = amount / 1000 >= 1000 ? 'TB' : amount >= 1000 ? 'GB' : 'MB' + var number = (amount / 1000 >= 1000 ? amount / 1000000 : amount >= 1000 ? amount / 1000 : amount).toFixed(2) + return `${number} ${unit}` +} +function drawIndicatorBar(item){ + var html = `
    +
    +
    +
    + +
    +
    + ${item.label} +
    +
    + 0% +
    +
    +
    +
    +
    +
    +
    +
    +
    ` + $('.disk-indicator-bars').append(html) +} +function updateInterfaceStatus(data){ + // Updated status of interface in loaded Monitors + loadedMonitors[data.id].code = data.code + // Update counters in status bar + setInterfaceCounts() +} +function setInterfaceCounts(monitors){ + var data = monitors || Object.values(loadedMonitors) + var allCameraCount = data.length + var activeCameraCount = data.filter((monitor) => { + var monCode = parseInt(monitor.code) + return monCode === 9 || monCode === 2 || monCode === 3 + }).length + var percentActive = (activeCameraCount/allCameraCount)*100 + // Update Camera count in Monitors menu + $('.cameraCount').text(allCameraCount) + // Update Camera count in status bar + var el = $(`#indicator-activeCameraCount`) + var count = el.find('.indicator-percent') + count.text(`${activeCameraCount} / ${allCameraCount}`) + el.find('.progress-bar').css('width', `${percentActive}%`) +} +// on page load +var readyFunctions = [] +function onDashboardReady(theAction){ + readyFunctions.push(theAction) +} +function onDashboardReadyExecute(theAction){ + $.each(readyFunctions,function(n,theAction){ + theAction() + }) +} +function popImage(imageSrc){ + $('body').append(`
    `) +} +$(document).ready(function(){ + loadMonitorsIntoMemory(function(data){ + setInterfaceCounts(data) + openTab('initial') + onDashboardReadyExecute() + }) + $('body') + // .on('tab-away',function(){ + // + // }) + // .on('tab-close',function(){ + // + // }) + .on('click','.pop-image',function(){ + var imageSrc = $(this).attr('src') + if(!imageSrc){ + new PNotify({ + title: lang['Action Failed'], + text: lang['No Image'], + type: 'warning' + }) + return; + }; + popImage(imageSrc) + }) + .on('click','.popped-image',function(){ + $(this).remove() + }) + .on('click','.popped-image img',function(e){ + e.stopPropagation() + return false; + }) + .on('click','.go-home',goBackHome) + .on('click','.go-back',goBackOneTab) + .on('click','.delete-tab',function(e){ + e.preventDefault() + e.stopPropagation() + var tabName = $(this).parents(`[page-open]`).attr(`page-open`) + if(activeTabName === tabName){ + goBackOneTab() + } + deleteTab(tabName) + return false; + }) + .on('click','.delete-tab-dynamic',function(e){ + e.preventDefault() + e.stopPropagation() + var tabName = $(this).parents('.page-tab').attr('id').replace('tab-','') + goBackOneTab() + deleteTab(tabName) + return false; + }) + .on('click','[page-open]',function(){ + var el = $(this) + var pageChoice = el.attr('page-open') + var pageOptions = JSON.parse(el.attr('page-options') || '{}') + if(tabTree.name === pageChoice)return; + openTab(pageChoice,pageOptions) + }) + .on('click','[class_toggle]',function(){ + var el = $(this) + var targetElement = el.attr('data-target') + var classToToggle = el.attr('class_toggle') + var iconClassesToToggle = el.attr('icon-toggle').split(' ') + var iconTarget = el.attr('icon-child') + var iconTargetElement = el.find(el.attr('icon-child')) + var togglPosition = $(targetElement).hasClass(classToToggle) ? 0 : 1 + var classToggles = dashboardOptions().class_toggle || {} + classToggles[targetElement] = [classToToggle,togglPosition,iconClassesToToggle,iconTarget]; + dashboardOptions('class_toggle',classToggles) + $(targetElement).toggleClass(classToToggle) + iconTargetElement + .removeClass(iconClassesToToggle[togglPosition === 1 ? 0 : 1]) + .addClass(iconClassesToToggle[togglPosition]) + }) + .on('keyup','.search-parent .search-controller',function(){ + var _this = this; + var parent = $(this).parents('.search-parent') + $.each(parent.find(".search-body .search-row"), function() { + if($(this).text().toLowerCase().indexOf($(_this).val().toLowerCase()) === -1) + $(this).hide(); + else + $(this).show(); + }); + }) + .on('click','[tab-chooser]',function(){ + var el = $(this) + var parent = el.parents('[tab-chooser-parent]') + var tabName = el.attr('tab-chooser') + var allTabChoosersInParent = parent.find('[tab-chooser]') + var allTabsInParent = parent.find('[tab-section]') + allTabsInParent.hide() + allTabChoosersInParent.removeClass('active') + el.addClass('active') + parent.find(`[tab-section="${tabName}"]`).show() + }); + if(!isMobile){ + $('body').on('mousedown',"select[multiple]",function(e){ + e.preventDefault(); + var select = this; + var scroll = select .scrollTop; + e.target.selected = !e.target.selected; + setTimeout(function(){select.scrollTop = scroll;}, 0); + $(select).focus().change(); + }).on('mousemove',"select[multiple]",function(e){ + e.preventDefault() + }); + } + $('.logout').click(function(e){ + $.get(getApiPrefix() + '/logout/' + $user.ke + '/' + $user.uid,function(data){ + localStorage.removeItem('ShinobiLogin_'+location.host); + location.href = location.href.split('#')[0]; + }) + }) + // only binded on load + $('.form-section-header:not(.no-toggle-header)').click(function(e){ + var parent = $(this).parent('.form-group-group') + var boxWrapper = parent.attr('id') + parent.toggleClass('hide-box-wrapper') + var hideBoxWrapper = parent.hasClass('hide-box-wrapper') + boxWrappersHidden[boxWrapper] = hideBoxWrapper + dashboardOptions('boxWrappersHidden',boxWrappersHidden) + }) + $('[data-bs-target="#sidebarMenu"]').click(function(e){ + resizeMonitorIcons() + }) + if(!isMobile){ + var clicked = false, clickX, oldClickX; + var htmlBody = $('html') + pageTabLinks.on({ + 'mousemove': function(e) { + clicked && updateScrollPos(e); + }, + 'mousedown': function(e) { + e.preventDefault(); + clicked = true; + oldClickX = clickX + 0; + clickX = e.pageX; + }, + 'mouseup': function(e) { + if(oldClickX !== clickX){ + e.preventDefault() + } + clicked = false; + htmlBody.css('cursor', 'auto'); + } + }); + + var updateScrollPos = function(e) { + htmlBody.css('cursor', 'grabbing'); + pageTabLinks.scrollLeft(pageTabLinks.scrollLeft() + (clickX - e.pageX)); + } + } + +}) diff --git a/web/libs/js/dash2.detectorfilter.js b/web/assets/js/bs5.eventFilters.js similarity index 53% rename from web/libs/js/dash2.detectorfilter.js rename to web/assets/js/bs5.eventFilters.js index 7123f451..271c6a70 100644 --- a/web/libs/js/dash2.detectorfilter.js +++ b/web/assets/js/bs5.eventFilters.js @@ -1,9 +1,21 @@ +window.acceptableDetectorFilterOperators = { + 'indexOf': lang['Contains'], + '!indexOf': lang['Does Not Contain'], + '===': lang['Equal to'], + '!==': lang['Not Equal to'], + '>=': lang['Greater Than or Equal to'], + '>': lang['Greater Than'], + '<': lang['Less Than'], + '<=': lang['Less Than or Equal to'], + '||': lang['OR'], + '&&': lang['AND'], +}; $(document).ready(function(e){ //detector filters window - $.detectorFilters = {e:$('#detector_filter')}; - var detectorFiltersWindow = $('#detector_filter') + var detectorFiltersWindow = $('#tab-eventFilters') var detectorFiltersSelector = $('#detector_filters') var detectorFiltersConditionRows = $('#detector_filters_where') + var detectorFiltersMonitorsList = $('#event_filters_monitors') var detectorFiltersActions = detectorFiltersWindow.find('.actions') var detectorFiltersForm = detectorFiltersWindow.find('form'); var idField = detectorFiltersForm.find('[name="id"]') @@ -12,15 +24,25 @@ $(document).ready(function(e){ var getSelectedFilter = function(){ return detectorFiltersSelector.val() } + var getCurrentlySelectedMonitorId = function(){ + return detectorFiltersMonitorsList.val() + } var drawOptions = function(){ - detectorFiltersSelector.empty() + var html = '' $.each(loadedFilters,function(n,dFilter){ - $.ccio.tm('option',{auth_token:$user.auth_token,id:dFilter.id,name:dFilter.filter_name},'#detector_filters') + html += createOptionHtml({ + value: dFilter.id, + label: dFilter.filter_name + }) }) + detectorFiltersSelector.html(html) } var getFiltersFromMonitorInEditor = function(){ + var monitorId = getCurrentlySelectedMonitorId() + var monitorConfig = Object.assign({},loadedMonitors[monitorId]) + var dFilters = safeJsonParse(safeJsonParse(monitorConfig.details).detector_filters) try{ - return JSON.parse($.aM.e.find('[detail="detector_filters"]').val()) + return dFilters }catch(err){ return {} } @@ -52,80 +74,97 @@ $(document).ready(function(e){ form.filter_name = form.filter_name || 'New Filter' return form } - var closeFiltersToMonitorEditor = function(form){ - $.aM.e.find('[detail="detector_filters"]').val(JSON.stringify(loadedFilters)).change() - detectorFiltersWindow.modal('hide') + var closeFiltersToMonitorEditor = function(dFilters){ + var monitorId = getCurrentlySelectedMonitorId() + var monitorConfig = Object.assign({},loadedMonitors[monitorId]) + monitorConfig.details.detector_filters = JSON.stringify(loadedFilters) + monitorConfig.details = JSON.stringify(monitorConfig.details) + $.post(getApiPrefix(`configureMonitor`)+ '/' + monitorId,{ + data: JSON.stringify(monitorConfig) + },function(d){ + debugLog(d) + if(d.ok){ + + } + }) } var drawDetectorFilterFieldsRow = function(d){ if(!d)d = {}; - d.id = $('#filters_where .row').length; + d.id = detectorFiltersConditionRows.find('.where-row').length; if(!d.p1){d.p1='indifference'} if(!d.p2){d.p2='==='} if(!d.p3){d.p3=''} if(!d.p4){d.p4='&&'} - tmp = `
    -
    - + tmp = `
    +
    + +
    +
    +
    -
    - +
    +
    -
    - +
    +
    -
    - +
    +
    -
    +
    + +
    +
    ` detectorFiltersConditionRows.append(tmp); - detectorFiltersConditionRows.find('.row [where="p4"][disabled]').prop('disabled',false) - detectorFiltersConditionRows.find('.row:last [where="p1"]').val(d.p1) - detectorFiltersConditionRows.find('.row:last [where="p2"]').val(d.p2) - detectorFiltersConditionRows.find('.row:last [where="p3"]').val(d.p3) - detectorFiltersConditionRows.find('.row:last [where="p4"]').val(d.p4).prop('disabled',true) + detectorFiltersConditionRows.find('.where-row [where="p4"][disabled]').prop('disabled',false) + var lastRow = detectorFiltersConditionRows.find('.where-row:last') + lastRow.find('[where="openBracket"]').val(d.openBracket) + lastRow.find('[where="p1"]').val(d.p1) + lastRow.find('[where="p2"]').val(d.p2) + lastRow.find('[where="p3"]').val(d.p3) + lastRow.find('[where="closeBracket"]').val(d.closeBracket) + lastRow.find('[where="p4"]').val(d.p4).prop('disabled',true) } var createNewFilter = function(){ - var newId = $.ccio.gid(5) + var newId = generateId(5) idField.val(newId) detectorFiltersConditionRows.empty() drawDetectorFilterFieldsRow() @@ -165,7 +204,7 @@ $(document).ready(function(e){ detectorFiltersWindow.on('change','[where="p1"]',function(e){ var el = $(this) var p1v = el.val() - var parent = el.parents('.row') + var parent = el.parents('.where-row') var p3 = parent.find('[where="p3"]') var options = [] switch(p1v){ @@ -202,7 +241,7 @@ $(document).ready(function(e){ } p3.attr('placeholder',msg) }) - detectorFiltersWindow.on('shown.bs.modal',function(e){ + detectorFiltersMonitorsList.change(function(e){ loadedFilters = getFiltersFromMonitorInEditor() drawOptions() selectFirstOption() @@ -218,7 +257,7 @@ $(document).ready(function(e){ detectorFiltersWindow.on('click','.where .delete-condition',function(e){ if(detectorFiltersConditionRows.find('.where-row').length > 1){ $(this).parents('.where-row').remove() - detectorFiltersConditionRows.find('.row:last [where="p4"]').prop('disabled',true) + detectorFiltersConditionRows.find('.where-row:last [where="p4"]').prop('disabled',true) updateSelectedFilter() } }) @@ -261,5 +300,15 @@ $(document).ready(function(e){ closeFiltersToMonitorEditor() return false }) - $.detectorFilters.getLoadedFilters = function(){return loadedFilters} + addOnTabOpen('eventFilters', function () { + if(!detectorFiltersMonitorsList.val()){ + drawMonitorListToSelector(detectorFiltersMonitorsList,true) + } + }) + addOnTabReopen('eventFilters', function () { + var theSelected = `${detectorFiltersMonitorsList.val()}` + drawMonitorListToSelector(detectorFiltersMonitorsList) + detectorFiltersMonitorsList.val(theSelected) + }) + drawSubMenuItems('eventFilters',definitions['Event Filters']) }) diff --git a/web/assets/js/bs5.eventListWithPics.js b/web/assets/js/bs5.eventListWithPics.js new file mode 100644 index 00000000..829fa842 --- /dev/null +++ b/web/assets/js/bs5.eventListWithPics.js @@ -0,0 +1,176 @@ +$(document).ready(function(){ + var theWindow = $('#tab-eventListWithPics') + var monitorSelector = $('#eventListWithPics-monitors-list') + var rowContainer = $('#eventListWithPics-rows') + var dateRangeSelector = $('#eventListWithPics-daterange') + var theForm = theWindow.find('form') + var loadedEventsInMemory = {} + dateRangeSelector.daterangepicker({ + startDate: moment().subtract(moment.duration("5:00:00")), + endDate: moment().add(moment.duration("24:00:00")), + timePicker: true, + timePicker24Hour: true, + timePickerSeconds: true, + timePickerIncrement: 30, + locale: { + format: 'MM/DD/YYYY h:mm A' + } + },function(start, end, label){ + drawDataRows() + }) + function drawDataRows(){ + var selectedMonitorType = monitorSelector.val() + selectedMonitorType = selectedMonitorType === 'all' ? '' : selectedMonitorType + var currentDateRange = dateRangeSelector.data('daterangepicker'); + getEvents({ + monitorId: selectedMonitorType, + limit: 50, + startDate: currentDateRange.startDate, + endDate: currentDateRange.endDate, + },function(response){ + drawEventRowsToList(rowContainer,response.events) + }) + } + function applyVideosToEventsList(videos,events){ + var updatedVideos = videos.concat([]) + var currentEvents = events.concat([]) + currentEvents.forEach(function(theEvent,index){ + updatedVideos.forEach(function(video,videoIndex){ + var startTime = new Date(video.time) + var endTime = new Date(video.end) + var eventTime = new Date(theEvent.time) + if(eventTime >= startTime && eventTime <= endTime){ + currentEvents[index].videoAssociated = video + } + }) + }) + return currentEvents + } + function createEventRow(row,theVideoList){ + var matrices = row.details.matrices + var hasRows = matrices && matrices.length > 0 + var video = row.videoAssociated + var imagePath = `${formattedTimeForFilename(row.time,false,'YYYY-MM-DD')}/${formattedTimeForFilename(row.time,false,'YYYY-MM-DDTHH-mm-ss')}.jpg` + if(hasRows){false + var eventMatrixHtml = `` + var objectsFound = {} + eventMatrixHtml += `` + $.each(matrices,function(n,matrix){ + if(!objectsFound[matrix.tag])objectsFound[matrix.tag] = 1 + ++objectsFound[matrix.tag] + }) + $.each(objectsFound,function(tag,count){ + eventMatrixHtml += ` + + + ` + }) + eventMatrixHtml += `
    ${tag}${count}
    ` + } + var html = ` +
    +
    +
    +
    + ${loadedMonitors[row.mid] ? loadedMonitors[row.mid].name : row.mid} +
    + ${video ? ` +
    + + +
    + ` : ``} +
    +
    + +
    +
    +
    +
    +
    + ${moment(row.time).fromNow()} +
    +
    +
    +
    + ${formattedTime(row.time,true)} +
    +
    +
    +
    +
    + ${hasRows ? `` : ``} +
    +
    ` + theVideoList.append(html) + var theCard = theVideoList.find(`[data-mid="${row.mid}"][data-time="${row.time}"]`) + var theImage = theCard.find(`img`) + theImage.on('load',function(){ + theImage.show() + var cardObjectContainer = theCard.find(`.stream-objects`) + var videoHeight = cardObjectContainer.height() + var videoWidth = cardObjectContainer.width() + drawMatrices(row,{ + theContainer: cardObjectContainer, + height: videoHeight, + width: videoWidth, + }) + }).on('error',function(){ + theImage.remove() + }) + } + function drawEventRowsToList(targetElement,rows){ + var theVideoList = $(targetElement) + theVideoList.empty() + $.each(rows,function(n,row){ + createEventRow(row,theVideoList) + }) + liveStamp() + } + function getEvents(options,callback){ + loadedEventsInMemory = {} + options = options ? options : {} + var requestQueries = [] + var monitorId = options.monitorId + var limit = options.limit || 5000 + var eventStartTime + var eventEndTime + // var startDate = options.startDate + // var endDate = options.endDate + if(options.startDate){ + eventStartTime = formattedTimeForFilename(options.startDate,false) + requestQueries.push(`start=${eventStartTime}`) + } + if(options.endDate){ + eventEndTime = formattedTimeForFilename(options.endDate,false) + requestQueries.push(`end=${eventEndTime}`) + } + $.getJSON(`${getApiPrefix(`videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${limit}`]).join('&')}`,function(data){ + var videos = data.videos + $.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){ + var newEventList = applyVideosToEventsList(videos,eventData) + $.each(newEventList,function(n,event){ + loadedEventsInMemory[`${event.mid}${event.time}`] = event + }) + callback({events: newEventList}) + }) + }) + } + addOnTabOpen('eventListWithPics',function(){ + monitorSelector.find('optgroup option').remove() + var html = '' + $.each(loadedMonitors,function(n,v){ + html += createOptionHtml({ + value: v.mid, + label: v.name, + }) + }) + monitorSelector.find('optgroup').html(html) + drawDataRows() + }) + theForm.submit(function(e){ + e.preventDefault() + drawDataRows() + return false + }) +}) diff --git a/web/assets/js/bs5.extenders.js b/web/assets/js/bs5.extenders.js new file mode 100644 index 00000000..b1bdc647 --- /dev/null +++ b/web/assets/js/bs5.extenders.js @@ -0,0 +1,14 @@ +var accountSettings = { + onLoadFieldsExtensions: [], + onLoadFields: function(...extender){ + accountSettings.onLoadFieldsExtensions.push(...extender) + }, + onSaveFieldsExtensions: [], + onSaveFields: function(...extender){ + accountSettings.onSaveFieldsExtensions.push(...extender) + }, +} +var onToggleSideBarMenuHideExtensions = []; +function onToggleSideBarMenuHide(...extender){ + onToggleSideBarMenuHideExtensions.push(...extender) +} diff --git a/web/assets/js/bs5.fileBin.js b/web/assets/js/bs5.fileBin.js new file mode 100644 index 00000000..bfe37bdf --- /dev/null +++ b/web/assets/js/bs5.fileBin.js @@ -0,0 +1,176 @@ +$(document).ready(function(e){ + var theEnclosure = $('#tab-fileBinView') + var monitorsList = theEnclosure.find('.monitors_list') + var dateSelector = theEnclosure.find('.date_selector') + var fileBinDrawArea = $('#fileBin_draw_area') + var fileBinPreviewArea = $('#fileBin_preview_area') + var loadedFilesInMemory = {}; + function openFileBinView(monitorId,startDate,endDate){ + drawFileBinViewElements(monitorId,startDate,endDate) + } + function getSelectedTime(asUtc){ + var dateRange = dateSelector.data('daterangepicker') + var startDate = dateRange.startDate.clone() + var endDate = dateRange.endDate.clone() + if(asUtc){ + startDate = startDate.utc() + endDate = endDate.utc() + } + startDate = startDate.format('YYYY-MM-DDTHH:mm:ss') + endDate = endDate.format('YYYY-MM-DDTHH:mm:ss') + return { + startDate: startDate, + endDate: endDate + } + } + + dateSelector.daterangepicker({ + startDate: moment().utc().subtract(2, 'days'), + endDate: moment().utc(), + timePicker: true, + locale: { + format: 'YYYY/MM/DD hh:mm:ss A' + } + }, function(start, end, label) { + drawFileBinViewElements() + }) + monitorsList.change(function(){ + drawFileBinViewElements() + }) + function loadFileData(video){ + delete(video.f) + loadedFilesInMemory[`${video.mid}${video.name}`] = video + } + function drawFileBinViewElements(selectedMonitor,startDate,endDate){ + var dateRange = getSelectedTime(false) + if(!startDate)startDate = dateRange.startDate + if(!endDate)endDate = dateRange.endDate + if(!selectedMonitor)selectedMonitor = monitorsList.val() + var queryString = ['start=' + startDate,'end=' + endDate,'limit=0'] + var frameIconsHtml = '' + var apiURL = getApiPrefix('fileBin') + '/' + selectedMonitor; + var fileBinData = [] + loadedFilesInMemory = {} + $.getJSON(apiURL + '?' + queryString.join('&'),function(data){ + $.each(data.files,function(n,file){ + loadFileData(file) + }) + fileBinDrawArea.bootstrapTable('destroy') + fileBinDrawArea.bootstrapTable({ + pagination: true, + search: true, + columns: [ + { + field: 'monitorName', + title: lang['Monitor'] + }, + { + field: 'name', + title: lang['Filename'] + }, + { + field: 'time', + title: lang['Time Created'] + }, + { + field: 'size', + title: '' + }, + { + field: 'buttons', + title: '' + } + ], + data: data.files.map((file) => { + var href = getApiPrefix('fileBin') + '/' + selectedMonitor + '/' + file.name + var isVideo = file.name.includes('.mp4') || file.name.includes('.webm') + return { + monitorName: `${loadedMonitors[file.mid]?.name || file.mid}`, + name: file.name, + time: ` +
    ${lang.Created} ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}
    + ${file.details.start ? `
    ${lang.Started} ${formattedTime(file.details.start, 'DD-MM-YYYY hh:mm:ss AA')}
    ` : ''} + ${file.details.end ? `
    ${lang.Ended} ${formattedTime(file.details.end, 'DD-MM-YYYY hh:mm:ss AA')}
    ` : ''} + `, + size: convertKbToHumanSize(file.size), + buttons: ` +
    + + ${isVideo ? `` : ``} + ${permissionCheck('video_delete',file.mid) ? `` : ''} +
    + `, + } + }) + }) + }) + } + function drawPreviewVideo(href){ + fileBinPreviewArea.html(``) + } + function archiveFile(video,unarchive){ + return archiveVideo(video,unarchive,true) + } + async function archiveFiles(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await archiveFile(video,false) + } + } + function unarchiveFile(video){ + return archiveFile(video,true) + } + async function unarchiveFiles(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await unarchiveFile(video) + } + } + $('body') + .on('click','.open-fileBin-video',function(e){ + e.preventDefault() + var href = $(this).attr('href') + openTab(`fileBinView`,{},null) + drawPreviewVideo(href) + return false; + }); + theEnclosure + .on('click','.refresh-data',function(e){ + e.preventDefault() + drawFileBinViewElements() + return false; + }) + .on('click','.preview-video',function(e){ + e.preventDefault() + var href = $(this).attr('href') + drawPreviewVideo(href) + return false; + }) + .on('click','.archive-file',function(e){ + e.preventDefault() + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + var filename = el.attr('data-name') + var unarchive = $(this).hasClass('status-archived') + var file = loadedFilesInMemory[`${monitorId}${filename}`] + if(!file)return console.log(`No File`,monitorId,filename,unarchive,file); + if(unarchive){ + unarchiveFile(file) + }else{ + archiveFile(file) + } + return false; + }) + addOnTabOpen('fileBinView', function () { + drawMonitorListToSelector(monitorsList) + drawFileBinViewElements() + }) + addOnTabReopen('fileBinView', function () { + var theSelected = `${monitorsList.val()}` + drawMonitorListToSelector(monitorsList) + monitorsList.val(theSelected) + }) + addOnTabAway('fileBinView', function () { + fileBinPreviewArea.find('video')[0].pause() + }) +}) diff --git a/web/assets/js/bs5.help.js b/web/assets/js/bs5.help.js new file mode 100644 index 00000000..b79223eb --- /dev/null +++ b/web/assets/js/bs5.help.js @@ -0,0 +1,80 @@ +$(document).ready(function(){ + var helpWindow = $('#help_window') + var openMessage = null + function lessThanOneWeekAgo(date){ + const WEEK = 1000 * 60 * 60 * 24 * 7; + const aWeekAgo = Date.now() - WEEK; + + return date < aWeekAgo; + } + function showHelpNotice(){ + var buttonHtml = `` + $.each([ + { + icon: 'share-square-o', + color: 'default', + text: 'ShinobiShop Subscriptions', + href: 'https://licenses.shinobi.video/subscribe', + class: '' + }, + { + icon: 'paypal', + color: 'success', + text: 'Donate by PayPal', + href: 'https://www.paypal.me/ShinobiCCTV', + class: '' + }, + { + icon: 'bank', + color: 'default', + text: 'University of Zurich (UZH)', + href: 'https://www.zora.uzh.ch/id/eprint/139275/', + class: '' + }, + { + icon: 'cube', + color: 'danger', + text: lang[`Don't Show for 1 Week`], + href: '#', + class: 'hide_donate', + }, + ],function(n,button){ + buttonHtml += ` +
    +
    ${ button.text }
    +
    ` + }) + openMessage = new PNotify({ + title: `It's a proven fact`, + text: ` +
    Generosity makes you a happier person, please consider supporting the development.
    +
    If you are already supporting the development, please contact us or use your provided license key and we can get this popup to go away for you Cheers!
    + ${buttonHtml}`, + hide: false, + }) + } + function dontShowForOneWeek(){ + if(openMessage){ + openMessage.remove() + } + dashboardOptions('subscription_checked',new Date()); + } + if( + !userHasSubscribed && + ( + !dashboardOptions().subscription_checked || + lessThanOneWeekAgo(new Date(dashboardOptions().subscription_checked)) + ) + ){ + setTimeout(function(){ + showHelpNotice() + },1000 * 60 * 0.2) + } + $('body').on('click','.hide_donate',function(e){ + e.preventDefault() + dontShowForOneWeek() + return false; + }) + console.log('Please support the Shinobi development.') + console.log('https://licenses.shinobi.video/subscribe') +}) diff --git a/web/assets/js/bs5.initial.js b/web/assets/js/bs5.initial.js new file mode 100644 index 00000000..0df05a5d --- /dev/null +++ b/web/assets/js/bs5.initial.js @@ -0,0 +1,8 @@ +onWebSocketEvent(function (d){ + switch(d.f){ + case'monitor_status': + case'monitor_edit': + setInterfaceCounts() + break; + } +}) diff --git a/web/assets/js/bs5.liveGrid.js b/web/assets/js/bs5.liveGrid.js new file mode 100644 index 00000000..2d6d750e --- /dev/null +++ b/web/assets/js/bs5.liveGrid.js @@ -0,0 +1,1142 @@ +var loadedLiveGrids = {} +var monitorPops = {} +var liveGridElements = {} +var runningJpegStreams = {} +var liveGrid = $('#monitors_live') +var liveGridOpenCountElements = $('.liveGridOpenCount') +var liveGridOpenCount = 0 +// +var onLiveStreamInitiateExtensions = [] +function onLiveStreamInitiate(callback){ + onLiveStreamInitiateExtensions.push(callback) +} +var onLiveStreamCloseExtensions = [] +function onLiveStreamClose(callback){ + onLiveStreamCloseExtensions.push(callback) +} +var onSignalCheckLiveStreamExtensions = [] +function onSignalCheckLiveStream(callback){ + onSignalCheckLiveStreamExtensions.push(callback) +} +var onBuildStreamElementExtensions = [] +function onBuildStreamElement(callback){ + onBuildStreamElementExtensions.push(callback) +} +// +function setLiveGridOpenCount(addOrRemove){ + liveGridOpenCount += addOrRemove + liveGridOpenCountElements.text(liveGridOpenCount) +} +function getLiveGridData(){ + return liveGrid.data('gridstack') +} +function getMonitorsPerRow(){ + var x + switch(dashboardOptions().montage){ + case'1': + x = '12' + break; + case'2': + x = '6' + break; + case'3': + x = '4' + break; + case'4': + x = '3' + break; + case'5': + x = '5' + break; + case'6': + x = '2' + break; + default://3 + x = '4' + break; + } + return x +} +function saveLiveGridBlockPositions() { + var monitors = {} + liveGrid.find(" .monitor_item").each(function(n,v){ + var el = $(v) + var item = {} + item.ke = el.attr('data-ke') + item.mid = el.attr('data-mid') + item.x = el.attr('data-gs-x') + item.y = el.attr('data-gs-y') + item.height = el.attr('data-gs-height') + item.width = el.attr('data-gs-width') + monitors[item.ke+''+item.mid] = item + }) + $user.details.monitorOrder = monitors; + mainSocket.f({f:'monitorOrder',monitorOrder:monitors}) +} +function buildStreamElementHtml(streamType){ + var html = '' + if(window.jpegModeOn === true){ + html = ''; + }else{ + switch(streamType){ + case'hls':case'flv':case'mp4': + html = ``; + break; + case'mjpeg': + html = ''; + break; + case'jpeg': + html = ''; + break; + default://base64//h265 + html = ''; + break; + } + $.each(onBuildStreamElementExtensions,function(n,extender){ + var newHtml = extender(streamType) + html = newHtml ? newHtml : html + }) + } + return html +} +function resetMonitorCanvas(monitorId,initiateAfter,subStreamChannel){ + var monitor = loadedMonitors[monitorId] + var details = monitor.details + var streamType = subStreamChannel ? details.substream ? details.substream.output.stream_type : 'hls' : details.stream_type + if(!liveGridElements[monitorId])return; + var streamBlock = liveGridElements[monitorId].monitorItem.find('.stream-block') + closeLiveGridPlayer(monitorId,false) + streamBlock.find('.stream-element').remove() + streamBlock.append(buildStreamElementHtml(streamType)) + if(initiateAfter)initiateLiveGridPlayer(monitor,subStreamChannel) +} +function replaceMonitorInfoInHtml(htmlString,monitor){ + var monitorMutes = dashboardOptions().monitorMutes || {} + return htmlString + .replaceAll('$GROUP_KEY',monitor.ke) + .replaceAll('$MONITOR_ID',monitor.mid) + .replaceAll('$MONITOR_MODE',monitor.mode) + .replaceAll('$MONITOR_NAME',monitor.name) + .replaceAll('$MONITOR_MUTE_ICON',(monitorMutes[monitor.mid] !== 1 ? 'volume-up' : 'volume-off')); +} +function buildLiveGridBlock(monitor){ + if(monitor.mode === 'stop'){ + new PNotify({ + title: lang.sorryNo, + text: lang[`Cannot watch a monitor that isn't running.`], + type: 'danger' + }) + return + } + var monitorId = monitor.mid + var monitorDetails = safeJsonParse(monitor.details) + var monitorLiveId = `monitor_live_${monitor.mid}` + var subStreamChannel = monitor.subStreamChannel + var streamType = subStreamChannel ? monitorDetails.substream ? monitorDetails.substream.output.stream_type : 'hls' : monitorDetails.stream_type + var streamElement = buildStreamElementHtml(streamType) + var streamBlockInfo = definitions['Monitor Stream Window'] + if(!loadedLiveGrids[monitor.mid])loadedLiveGrids[monitor.mid] = {} + var quickLinkHtml = '' + $.each(streamBlockInfo.quickLinks,function(n,button){ + if(button.eval && !eval(button.eval))return; + quickLinkHtml += `` + }) + var baseHtml = `
    +
    +
    + ${streamBlockInfo.streamBlockPreHtml || ''} +
    +
    + ${streamBlockInfo.streamBlockHudHtml || ''} +
    + ${streamBlockInfo.streamBlockHudControlsHtml || ''} +
    +
    + ${streamElement} +
    +
    + ${(streamBlockInfo.gridBlockAfterContentHtml || '').replace(`$QUICKLINKS`,quickLinkHtml)} + +
    ` + return replaceMonitorInfoInHtml(baseHtml,monitor) +} +function drawPtzControlsOnLiveGridBlock(monitorId){ + var monitorItem = $('#monitor_live_' + monitorId) + var ptzControls = monitorItem.find('.PTZ_controls'); + var loadedMonitor = loadedMonitors[monitorId] + var stopCommandOnRelease = loadedMonitor.details.control_stop === '2' + if(ptzControls.length>0){ + ptzControls.remove() + }else{ + var html = `
    +
    +
    +
    +
    +
    +
    +
    +
    + + +
    +
    + + +
    + ${safeJsonParse(loadedMonitors[monitorId].details,{}).is_onvif === '1' ? ` + ` : ``} +
    ` + monitorItem.append(html) + } +} +function drawVideoCardToMiniList(monitorId,video,skipLimitCheck){ + var theVideoList = liveGridElements[monitorId].miniVideoList + if(!skipLimitCheck){ + var rowsDrawn = theVideoList.find('.video-row') + if(rowsDrawn.length > 10)rowsDrawn.last().remove() + } + theVideoList.prepend(createVideoRow(video,`col-12 mb-2`)) +} +function loadVideoMiniList(monitorId){ + getVideos({ + monitorId: monitorId, + limit: 10, + },function(data){ + var videos = data.videos + $.each(videos.reverse(),function(n,video){ + drawVideoCardToMiniList(monitorId,video,true) + }) + }) +} +function updateLiveGridElementHeightWidth(monitorId){ + var liveGridElement = liveGridElements[monitorId] + var streamElement = liveGridElement.streamElement + liveGridElement.width = streamElement.width() + liveGridElement.height = streamElement.height() + console.log(liveGridElement.width,liveGridElement.height) +} +function updateAllLiveGridElementsHeightWidth(monitorId){ + $.each(liveGridElements,function(monitorId){ + updateLiveGridElementHeightWidth(monitorId) + }) +} +function drawLiveGridBlock(monitorConfig,subStreamChannel){ + var monitorId = monitorConfig.mid + if($('#monitor_live_' + monitorId).length === 0){ + var x = 0; + var y = 0; + var monitorsPerRow = getMonitorsPerRow() + var width = monitorsPerRow + var height = width; + var isSmallMobile = isMobile && window.innerWidth <= 812; + var html = buildLiveGridBlock(monitorConfig) + var monitorOrderEngaged = dashboardOptions().switches.monitorOrder === 1; + if(monitorOrderEngaged && $user.details.monitorOrder && $user.details.monitorOrder[monitorConfig.ke+''+monitorId]){ + var saved = $user.details.monitorOrder[monitorConfig.ke+''+monitorId]; + x = saved.x; + y = saved.y; + width = saved.width; + height = saved.height; + } + liveGrid.data('gridstack').addWidget($(html), x, y, isSmallMobile ? 4 : height, isSmallMobile ? 4 : height, !monitorOrderEngaged); + var theBlock = $('#monitor_live_' + monitorId); + var streamElement = theBlock.find('.stream-element') + liveGridElements[monitorId] = { + monitorItem: theBlock, + streamElement: streamElement, + eventObjects: theBlock.find('.stream-objects'), + motionMeter: theBlock.find('.indifference .progress-bar'), + motionMeterText: theBlock.find('.indifference .progress-bar span'), + width: streamElement.width(), + height: streamElement.height(), + miniVideoList: theBlock.find('.videos-mini'), + } + try{ + if(safeJsonParse(monitorConfig.details).control === "1"){ + theBlock.find('[monitor="control_toggle"]').show() + }else{ + theBlock.find('.pad').remove(); + theBlock.find('[monitor="control_toggle"]').hide() + } + }catch(re){ + debugLog(re) + } + setCosmeticMonitorInfo(loadedMonitors[monitorId],subStreamChannel) + setLiveGridOpenCount(1) + } + initiateLiveGridPlayer(loadedMonitors[monitorId],subStreamChannel) +} +function initiateLiveGridPlayer(monitor,subStreamChannel){ + var livePlayerElement = loadedLiveGrids[monitor.mid] + var details = monitor.details + var groupKey = monitor.ke + var monitorId = monitor.mid + var loadedMonitor = loadedMonitors[monitorId] + var loadedPlayer = loadedLiveGrids[monitor.mid] + var websocketPath = checkCorrectPathEnding(location.pathname) + 'socket.io' + var containerElement = $(`#monitor_live_${monitor.mid}`) + var streamType = subStreamChannel ? details.substream ? details.substream.output.stream_type : 'hls' : details.stream_type + if(location.search === '?p2p=1'){ + websocketPath = '/socket.io' + // websocketQuery.machineId = machineId + } + switch(streamType){ + case'jpeg': + startJpegStream(monitorId) + break; + case'b64': + if(loadedPlayer.Base64 && loadedPlayer.Base64.connected){ + loadedPlayer.Base64.disconnect() + } + loadedPlayer.Base64 = io(location.origin,{ path: websocketPath, query: websocketQuery, transports: ['websocket'], forceNew: false}) + var ws = loadedPlayer.Base64 + var buffer + ws.on('diconnect',function(){ + console.log('Base64 Stream Disconnected') + }) + ws.on('connect',function(){ + ws.emit('Base64',{ + auth: $user.auth_token, + uid: $user.uid, + ke: monitor.ke, + id: monitor.mid, + channel: subStreamChannel + }) + if(!loadedPlayer.ctx || loadedPlayer.ctx.length === 0){ + loadedPlayer.ctx = containerElement.find('canvas'); + } + var ctx = loadedPlayer.ctx[0] + var ctx2d = ctx.getContext("2d") + loadedPlayer.image = new Image() + var image = loadedPlayer.image + image.onload = function() { + loadedPlayer.imageLoading = false + var x = 0 + var y = 0 + ctx.getContext("2d").drawImage(image,x,y,ctx.width,ctx.height) + URL.revokeObjectURL(loadedPlayer.imageUrl) + } + ws.on('data',function(imageData){ + try{ + if(loadedPlayer.imageLoading === true)return console.log('drop'); + loadedPlayer.imageLoading = true + var arrayBufferView = new Uint8Array(imageData); + var blob = new Blob( [ arrayBufferView ], { type: "image/jpeg" } ); + loadedPlayer.imageUrl = URL.createObjectURL( blob ); + loadedPlayer.image.src = loadedPlayer.imageUrl + loadedPlayer.last_frame = 'data:image/jpeg;base64,'+base64ArrayBuffer(imageData) + }catch(er){ + debugLog('base64 frame') + } + // $.ccio.init('signal',d); + }) + }) + break; + case'mp4': + setTimeout(function(){ + var stream = containerElement.find('.stream-element'); + var onPoseidonError = function(){ + // setTimeout(function(){ + // mainSocket.f({f:'monitor',ff:'watch_on',id:monitor.mid}) + // },5000) + } + if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0 + if(loadedPlayer.PoseidonErrorCount >= 5)return + if(subStreamChannel ? details.substream.output.stream_flv_type === 'ws' : monitor.details.stream_flv_type === 'ws'){ + if(loadedPlayer.Poseidon){ + loadedPlayer.Poseidon.stop() + revokeVideoPlayerUrl(monitorId) + } + try{ + loadedPlayer.Poseidon = new Poseidon({ + video: stream[0], + auth_token: $user.auth_token, + ke: monitor.ke, + uid: $user.uid, + id: monitor.mid, + url: location.origin, + path: websocketPath, + query: websocketQuery, + onError : onPoseidonError, + channel : subStreamChannel + }) + loadedPlayer.Poseidon.start(); + }catch(err){ + // onPoseidonError() + console.log('onTryPoseidonError',err) + } + }else{ + stream.attr('src',getApiPrefix(`mp4`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.mp4?time=' + (new Date()).getTime()) + stream[0].onerror = function(err){ + console.error(err) + } + } + },1000) + break; + case'flv': + if (flvjs.isSupported()) { + if(loadedPlayer.flv){ + loadedPlayer.flv.destroy() + revokeVideoPlayerUrl(monitorId) + } + var options = {}; + if(monitor.details.stream_flv_type==='ws'){ + if(monitor.details.stream_flv_maxLatency&&monitor.details.stream_flv_maxLatency!==''){ + monitor.details.stream_flv_maxLatency = parseInt(monitor.details.stream_flv_maxLatency) + }else{ + monitor.details.stream_flv_maxLatency = 20000; + } + options = { + type: 'flv', + isLive: true, + auth_token: $user.auth_token, + ke: monitor.ke, + uid: $user.uid, + id: monitor.mid, + maxLatency: monitor.details.stream_flv_maxLatency, + hasAudio:false, + url: location.origin, + path: websocketPath, + channel : subStreamChannel, + query: websocketQuery + } + }else{ + options = { + type: 'flv', + isLive: true, + url: getApiPrefix(`flv`)+'/'+monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '')+'/s.flv' + } + } + loadedPlayer.flv = flvjs.createPlayer(options); + loadedPlayer.flv.attachMediaElement(containerElement.find('.stream-element')[0]); + loadedPlayer.flv.on('error',function(err){ + console.log(err) + }); + loadedPlayer.flv.load(); + loadedPlayer.flv.play(); + }else{ + new PNotify({title:'Stream cannot be started',text:'FLV.js is not supported on this browser. Try another stream type.',type:'error'}); + } + break; + case'hls': + function createSteamNow(){ + clearTimeout(loadedPlayer.m3uCheck) + var url = getApiPrefix(`hls`) + '/' + monitor.mid + (subStreamChannel ? `/${subStreamChannel}` : '') + '/s.m3u8' + $.get(url,function(m3u){ + if(m3u == 'File Not Found'){ + loadedPlayer.m3uCheck = setTimeout(function(){ + createSteamNow() + },2000) + }else{ + var video = containerElement.find('.stream-element')[0] + if (isAppleDevice) { + video.src = url; + video.addEventListener('loadedmetadata', function() { + setTimeout(function(){ + video.play(); + },3000) + }, false); + }else{ + var hlsOptions = safeJsonParse(dashboardOptions().hlsOptions) || {} + if(hlsOptions instanceof String){ + hlsOptions = {} + new PNotify({ + title: lang['Invalid JSON'], + text: lang.hlsOptionsInvalid, + type: `warning`, + }) + } + if(loadedPlayer.hls){ + loadedPlayer.hls.destroy() + revokeVideoPlayerUrl(monitorId) + } + loadedPlayer.hls = new Hls(hlsOptions) + loadedPlayer.hls.loadSource(url) + loadedPlayer.hls.attachMedia(video) + loadedPlayer.hls.on(Hls.Events.MANIFEST_PARSED,function() { + if (video.paused) { + video.play(); + } + }); + } + } + }) + } + createSteamNow() + break; + case'mjpeg': + var liveStreamElement = containerElement.find('.stream-element') + var setSource = function(){ + liveStreamElement.attr('src',getApiPrefix(`mjpeg`)+'/'+monitorId + (subStreamChannel ? `/${subStreamChannel}` : '')) + liveStreamElement.unbind('ready') + liveStreamElement.ready(function(){ + setTimeout(function(){ + liveStreamElement.contents().find("body").append('') + },1000) + }) + } + setSource() + liveStreamElement.on('error',function(err){ + setTimeout(function(){ + setSource() + },4000) + }) + break; + } + $.each(onLiveStreamInitiateExtensions,function(n,extender){ + extender(streamType,monitor,loadedPlayer,subStreamChannel) + }) + var monitorMutes = dashboardOptions().monitorMutes || {} + if(dashboardOptions().switches.monitorMuteAudio === 1){ + containerElement.find('video').each(function(n,el){ + el.muted = "muted" + }) + }else{ + var hasFocus = windowFocus && window.hadFocus + $.each(loadedMonitors,function(frontId,monitor){ + setTimeout(() => { + var monitorId = monitor.mid + var muted = monitorMutes[monitorId] + try{ + var vidEl = $('.monitor_item[mid="' + monitorId + '"] video')[0] + if(vidEl.length === 0)return; + if(muted === 1){ + vidEl.muted = true + }else{ + if(hasFocus){ + vidEl.muted = false + }else{ + console.error('User must have window active to unmute.') + } + } + }catch(err){ + // console.log(err) + } + },2000) + }) + } + //initiate signal check + if(streamType !== 'useSubstream'){ + var signalCheckInterval = (isNaN(loadedMonitor.details.signal_check) ? 10 : parseFloat(loadedMonitor.details.signal_check)) * 1000 * 60 + if(signalCheckInterval > 0){ + clearInterval(loadedPlayer.signal) + loadedPlayer.signal = setInterval(function(){ + signalCheckLiveStream({ + mid: monitorId, + checkSpeed: 1000, + }) + },signalCheckInterval); + } + } +} +function revokeVideoPlayerUrl(monitorId){ + try{ + URL.revokeObjectURL(liveGridElements[monitorId].streamElement[0].src) + }catch(err){ + debugLog(err) + } +} +function closeLiveGridPlayer(monitorId,killElement){ + try{ + var livePlayerElement = loadedLiveGrids[monitorId] + if(livePlayerElement){ + if(livePlayerElement.hls){livePlayerElement.hls.destroy()} + if(livePlayerElement.Poseidon){livePlayerElement.Poseidon.stop()} + if(livePlayerElement.Base64){livePlayerElement.Base64.disconnect()} + if(livePlayerElement.dash){livePlayerElement.dash.reset()} + if(livePlayerElement.jpegInterval){ + stopJpegStream(monitorId) + } + $.each(onLiveStreamCloseExtensions,function(n,extender){ + extender(livePlayerElement) + }) + } + if(liveGridElements[monitorId])revokeVideoPlayerUrl(monitorId) + clearInterval(livePlayerElement.signal) + }catch(err){ + console.log(err) + } + if(killElement){ + var theElement = $('#monitor_live_'+monitorId) + if(theElement.length > 0){ + getLiveGridData().removeWidget(theElement) + setLiveGridOpenCount(-1) + delete(loadedLiveGrids[monitorId]) + delete(liveGridElements[monitorId]) + } + } +} +function callMonitorToLiveGrid(v){ + var watchedOn = dashboardOptions().watch_on || {} + if(watchedOn[v.ke] && watchedOn[v.ke][v.mid] === 1 && loadedMonitors[v.mid] && loadedMonitors[v.mid].mode !== 'stop'){ + mainSocket.f({f:'monitor',ff:'watch_on',id:v.mid}) + if(tabTree.name !== 'monitorSettings')openLiveGrid() + } +} +function loadPreviouslyOpenedLiveGridBlocks(){ + $.getJSON(getApiPrefix(`monitor`),function(data){ + $.each(data,function(n,v){ + callMonitorToLiveGrid(v) + }) + setTimeout(function(){ + sortListMonitors() + if(dashboardOptions().switches.jpegMode === 1){ + mainSocket.f({ + f: 'monitor', + ff: 'jpeg_on' + }) + } + },1000) + drawMonitorGroupList() + }) +} +function closeAllLiveGridPlayers(rememberClose){ + var watchedOn = dashboardOptions().watch_on || {} + $.each(watchedOn,function(n,groupOfMons){ + $.each(groupOfMons,function(monitorId,monitor){ + if(monitor === 1){ + mainSocket.f({f:'monitor',ff:'watch_off',id: monitorId}) + } + }) + }) +} +function saveLiveGridBlockOpenState(monitorId,groupKey,state){ + var openBlocks = dashboardOptions().watch_on || {} + openBlocks[groupKey] = openBlocks[groupKey] ? openBlocks[groupKey] : {} + openBlocks[groupKey][monitorId] = state || 0 + dashboardOptions('watch_on',openBlocks) +} +function openLiveGrid(){ + if(tabTree.name !== 'liveGrid'){ + openTab('liveGrid',{}) + } +} +function popOutMonitor(monitorId){ + var monitorPop = monitorPops[monitorId] + function finish(img){ + if(monitorPop){ + monitorPop.close() + } + monitorPop = window.open(getApiPrefix() + '/embed/' + $user.ke + '/' + monitorId + '/fullscreen|jquery|relative|gui','pop_' + monitorId + $user.auth_token,'height='+img.height+',width='+img.width); + } + if(loadedLiveGrids[monitorId]){ + getSnapshot(loadedMonitors[monitorId],function(url){ + $('#temp').html('') + var img=$('#temp img')[0] + img.onload = function(){ + finish(img) + } + img.src = url + }) + }else{ + var img = { + height: 720, + width: 1280 + } + finish(img) + } +} +function fullScreenLiveGridStream(monitorItem){ + var videoElement = monitorItem.find('.stream-element') + monitorItem.addClass('fullscreen') + if(videoElement.is('canvas')){ + var theBody = $('body') + videoElement.attr('height',theBody.height()) + videoElement.attr('width',theBody.width()) + } + fullScreenInit(videoElement[0]) +} +function toggleJpegMode(){ + var sendData = { + f: 'monitor', + ff: 'jpeg_on' + } + if(window.jpegModeOn === true){ + sendData.ff = 'jpeg_off' + } + mainSocket.f(sendData) +} +function startJpegStream(monitorId){ + if(loadedLiveGrids[monitorId]){ + var monitor = loadedMonitors[monitorId] + var loadedBlock = loadedLiveGrids[monitorId] + var jpegInterval = !isNaN(monitor.details.jpegInterval) ? parseFloat(monitor.details.jpegInterval) : 1 + resetMonitorCanvas(monitorId,false) + var streamElement = $('#monitor_live_' + monitorId + ' .stream-element'); + // stopJpegStream(monitorId) + var jpegUrl = getApiPrefix('jpeg') + '/' + monitorId + '/s.jpg?time=' + function drawNewFrame(){ + streamElement.attr('src',jpegUrl + (new Date()).getTime()) + } + streamElement.on('load',function(){ + loadedBlock.jpegInterval = setTimeout(drawNewFrame,1000/jpegInterval) + }).on('error',function(){ + loadedBlock.jpegInterval = setTimeout(drawNewFrame,1000/jpegInterval) + }) + drawNewFrame() + } +} +function stopJpegStream(monitorId){ + var livePlayerElement = loadedLiveGrids[monitorId] + if(!livePlayerElement)return; + try{ + liveGridElements[monitorId].streamElement.off('load').off('error') + clearTimeout(livePlayerElement.jpegInterval) + }catch(err){ + console.log(err) + console.log(monitorId) + } +} +function startAllJpegStreams(monitorId){ + $.each(loadedMonitors,function(n,monitor){ + startJpegStream(monitor.mid) + }) +} +function stopAllJpegStreams(monitorId){ + $.each(loadedMonitors,function(n,monitor){ + stopJpegStream(monitor.mid) + }) +} +function canBackgroundStream(){ + return tabTree.name === 'liveGrid' && dashboardOptions().switches.backgroundStream === 1 +} +function resetLiveGridDimensionsInMemory(monitorId){ + var theRef = liveGridElements[monitorId] + theRef.width = theRef.streamElement.width() + theRef.height = theRef.streamElement.height() +} +function resetAllLiveGridDimensionsInMemory(monitorId){ + $.each(liveGridElements,function(monitorId,data){ + resetLiveGridDimensionsInMemory(monitorId) + }) +} +function signalCheckLiveStream(options){ + try{ + var monitorId = options.mid + var monitorConfig = loadedMonitors[monitorId] + var liveGridData = liveGridElements[monitorId] + var monitorItem = liveGridData.monitorItem + var monitorDetails = monitorConfig.details + var checkCount = 0 + var base64Data = null; + var checkSpeed = options.checkSpeed || 1000 + var subStreamChannel = monitorConfig.subStreamChannel + var streamType = subStreamChannel ? monitorDetails.substream ? monitorDetails.substream.output.stream_type : 'hls' : monitorDetails.stream_type + function failedStreamCheck(){ + if(monitorConfig.signal_check_log == 1){ + logWriterDraw('[mid="'+monitorId+'"]',{ + log: { + type: 'Stream Check', + msg: lang.clientStreamFailedattemptingReconnect + } + }) + } + mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId}); + } + function succeededStreamCheck(){ + if(monitorConfig.signal_check_log == 1){ + logWriterDraw('[mid="'+monitorId+'"]',{ + log: { + type: 'Stream Check', + msg : lang.Success + } + }) + } + } + function executeCheck(){ + switch(streamType){ + case'b64': + monitorItem.resize() + break; + case'hls':case'flv':case'mp4': + if(monitorItem.find('video')[0].paused){ + failedStreamCheck() + }else{ + succeededStreamCheck() + } + break; + default: + if(dashboardOptions().jpeg_on === true){return} + getSnapshot({ + monitor: loadedMonitors[monitorId], + },function(url){ + base64Data = url; + setTimeout(function(){ + getSnapshot({ + monitor: loadedMonitors[monitorId], + },function(url){ + if(base64Data === url){ + if(checkCount < 3){ + ++checkCount; + setTimeout(function(){ + executeCheck(); + },checkSpeed) + }else{ + failedStreamCheck() + } + }else{ + succeededStreamCheck() + } + }); + },checkSpeed) + }); + break; + } + $.each(onSignalCheckLiveStreamExtensions,function(n,extender){ + extender(streamType,monitorItem) + }) + } + executeCheck(); + }catch(err){ + console.log(err) + var errorStack = err.stack; + function phraseFoundInErrorStack(x){return errorStack.indexOf(x) > -1} + if(phraseFoundInErrorStack("The HTMLImageElement provided is in the 'broken' state.")){ + mainSocket.f({f:'monitor',ff:'watch_on',id:monitorId}); + } + clearInterval(liveGridData.signal); + delete(liveGridData.signal); + } +} +$(document).ready(function(e){ + liveGrid + .on('dblclick','.stream-block',function(){ + var monitorItem = $(this).parents('[data-mid]'); + fullScreenLiveGridStream(monitorItem) + }) + $('body') + .resize(function(){ + resetAllLiveGridDimensionsInMemory() + }) + .on('click','.launch-live-grid-monitor',function(){ + var monitorId = $(this).parents('[data-mid]').attr('data-mid') + // if(isMobile){ + // createLivePlayerTab(loadedMonitors[monitorId]) + // }else{ + mainSocket.f({ + f: 'monitor', + ff: 'watch_on', + id: monitorId + }) + openLiveGrid() + // } + }) + .on('click','.reconnect-live-grid-monitor',function(){ + var monitorId = $(this).parents('[data-mid]').attr('data-mid') + mainSocket.f({ + f: 'monitor', + ff: 'watch_on', + id: monitorId + }) + }) + .on('click','.close-live-grid-monitor',function(){ + var monitorId = $(this).parents('[data-mid]').attr('data-mid') + mainSocket.f({ + f: 'monitor', + ff: 'watch_off', + id: monitorId + }) + setTimeout(function(){ + saveLiveGridBlockOpenState(monitorId,$user.ke,0) + },1000) + }) + .on('click','.snapshot-live-grid-monitor',function(){ + var monitorId = $(this).parents('[data-mid]').attr('data-mid') + getSnapshot({ + monitor: loadedMonitors[monitorId], + },function(url){ + $('#temp').html('a').find('a')[0].click(); + }) + }) + .on('click','.toggle-live-grid-monitor-logs',function(){ + var monitorItem = $(this).parents('[data-mid]') + var monitorId = monitorItem.attr('data-mid') + monitorItem.toggleClass('show_data') + var dataBlocks = monitorItem.find('.stream-block,.mdl-data_window') + if(monitorItem.hasClass('show_data')){ + loadVideoMiniList(monitorId) + dataBlocks.addClass('col-md-6').removeClass('col-md-12') + }else{ + dataBlocks.addClass('col-md-12').removeClass('col-md-6') + } + }) + .on('click','.toggle-live-grid-monitor-ptz-controls',function(){ + var monitorItem = $(this).parents('[data-mid]').attr('data-mid') + drawPtzControlsOnLiveGridBlock(monitorItem) + }) + .on('click','.toggle-live-grid-monitor-menu,.mdl-overlay-menu-backdrop',function(){ + var monitorItem = $(this).parents('[data-mid]') + var monitorId = monitorItem.attr('data-mid') + monitorItem.find('.mdl-overlay-menu-backdrop').toggleClass('hidden') + }) + .on('click','.mdl-overlay-menu',function(e){ + e.stopPropagation() + return false; + }) + .on('click','.toggle-live-grid-monitor-fullscreen',function(){ + var monitorItem = $(this).parents('[data-mid]') + fullScreenLiveGridStream(monitorItem) + }) + .on('click','.run-live-grid-monitor-pop',function(){ + var monitorId = $(this).parents('[data-mid]').attr('data-mid') + popOutMonitor(monitorId) + }) + .on('click','.toggle-monitor-substream',function(){ + var monitorId = $(this).parents('[data-mid]').attr('data-mid') + toggleSubStream(monitorId) + }) + .on('click','.run-live-grid-monitor-ptz',function(){ + var el = $(this) + var monitorId = el.parents('[data-mid]').attr('data-mid') + var switchChosen = el.attr('data-ptz-control') + runPtzCommand(monitorId,switchChosen) + }) + .on('mousedown','.run-live-grid-monitor-ptz-move',function(){ + var el = $(this) + var monitorId = el.parents('[data-mid]').attr('data-mid') + var switchChosen = el.attr('data-ptz-control') + runPtzMove(monitorId,switchChosen,true) + }) + .on('mouseup','.run-live-grid-monitor-ptz-move',function(){ + var el = $(this) + var monitorId = el.parents('[data-mid]').attr('data-mid') + var switchChosen = el.attr('data-ptz-control') + runPtzMove(monitorId,switchChosen,false) + }) + .on('click','.run-monitor-detection-trigger-test',function(){ + var el = $(this) + var monitorId = el.parents('[data-mid]').attr('data-mid') + runTestDetectionTrigger(monitorId) + }) + $('.open-all-monitors').click(function(){ + $.each(loadedMonitors,function(monitorId,monitor){ + mainSocket.f({ + f: 'monitor', + ff: 'watch_on', + id: monitor.mid + }) + openLiveGrid() + }) + }) + $('.close-all-monitors').click(function(){ + $.each(loadedMonitors,function(monitorId,monitor){ + mainSocket.f({ + f: 'monitor', + ff: 'watch_off', + id: monitor.mid + }) + setTimeout(function(){ + saveLiveGridBlockOpenState(monitorId,$user.ke,0) + },1000) + }) + }) + liveGrid + .gridstack({ + cellHeight: 80, + verticalMargin: 0, + }) + .on('dragstop', function(event,ui){ + setTimeout(function(){ + saveLiveGridBlockPositions() + },700) + }) + .on('gsresizestop', function(){ + // resetAllLiveGridDimensionsInMemory() + saveLiveGridBlockPositions() + }); + addOnTabReopen('liveGrid', function () { + loadPreviouslyOpenedLiveGridBlocks() + }) + addOnTabAway('liveGrid', function () { + closeAllLiveGridPlayers() + }) + onInitWebsocket(function (d){ + loadPreviouslyOpenedLiveGridBlocks() + }) + onToggleSideBarMenuHide(function (isHidden){ + setTimeout(updateAllLiveGridElementsHeightWidth,2000) + }) + onWebSocketEvent(function (d){ + switch(d.f){ + case'init_success': + // loadPreviouslyOpenedLiveGridBlocks() + break; + case'video_build_success': + d.status = 1 + d.mid = d.id || d.mid + var monitorId = d.mid + var videoTime = d.time + loadedVideosInMemory[`${monitorId}${videoTime}`] = d + if(liveGridElements[monitorId] && liveGridElements[monitorId].streamElement)drawVideoCardToMiniList(monitorId,createVideoLinks(d),false) + break; + case'monitor_watch_off':case'monitor_stopping': + var monitorId = d.mid || d.id + closeLiveGridPlayer(monitorId,(d.f === 'monitor_watch_off')) + break; + case'monitor_status': + if( + tabTree.name === 'liveGrid' && + ( + d.code === 2 || + d.code === 3 + ) + ){ + var monitorId = d.mid || d.id + setTimeout(function(){ + callMonitorToLiveGrid(loadedMonitors[monitorId]) + },2000) + } + break; + case'substream_start': + loadedMonitors[d.mid].subStreamChannel = d.channel + setTimeout(() => { + resetMonitorCanvas(d.mid,true,d.channel) + },3000) + break; + case'substream_end': + loadedMonitors[d.mid].subStreamChannel = null + resetMonitorCanvas(d.mid,true,null) + break; + case'monitor_watch_on': + var monitorId = d.mid || d.id + var loadedMonitor = loadedMonitors[monitorId] + var subStreamChannel = d.subStreamChannel + if(!loadedMonitor.subStreamChannel && loadedMonitor.details.stream_type === 'useSubstream'){ + toggleSubStream(monitorId,function(){ + drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel) + saveLiveGridBlockOpenState(monitorId,$user.ke,1) + }) + }else{ + drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel) + saveLiveGridBlockOpenState(monitorId,$user.ke,1) + } + break; + case'mode_jpeg_off': + window.jpegModeOn = false + $.each(loadedMonitors,function(n,v){ + stopJpegStream(v.mid) + resetMonitorCanvas(v.mid) + initiateLiveGridPlayer(v) + }) + $('body').removeClass('jpegMode') + break; + case'mode_jpeg_on': + window.jpegModeOn = true + startAllJpegStreams() + $('body').addClass('jpegMode') + break; + case'detector_trigger': + var monitorId = d.id + var liveGridElement = liveGridElements[monitorId] + if(!window.dontShowDetection && liveGridElement){ + var monitorElement = liveGridElement.monitorItem + var livePlayerElement = loadedLiveGrids[monitorId] + if(d.doObjectDetection === true){ + monitorElement.addClass('doObjectDetection') + clearTimeout(livePlayerElement.detector_trigger_doObjectDetection_timeout) + livePlayerElement.detector_trigger_doObjectDetection_timeout = setTimeout(function(){ + monitorElement.removeClass('doObjectDetection') + },3000) + }else{ + monitorElement.removeClass('doObjectDetection') + } + if(d.details.matrices&&d.details.matrices.length>0){ + drawMatrices(d,{ + theContainer: liveGridElement.eventObjects, + height: liveGridElement.height, + width: liveGridElement.width, + }) + } + if(d.details.confidence){ + var eventConfidence = d.details.confidence + if(eventConfidence > 100)eventConfidence = 100 + liveGridElement.motionMeter.css('width',eventConfidence + '%'); + liveGridElement.motionMeterText[0].innerHtml = d.details.confidence+'% change in '+d.details.name+'' + } + monitorElement.addClass('detector_triggered') + clearTimeout(livePlayerElement.detector_trigger_timeout); + livePlayerElement.detector_trigger_timeout = setTimeout(function(){ + monitorElement.removeClass('detector_triggered'); + liveGridElement.eventObjects.find('.stream-detected-object,.stream-detected-point').remove() + },800); + playAudioAlert() + var monitorPop = monitorPops[monitorId] + if($user.details.event_mon_pop === '1' && (!monitorPop || monitorPop.closed === true)){ + popOutMonitor(monitorId) + } + // console.log({ + // ke: d.ke, + // mid: monitorId, + // log: { + // type: lang['Event Occurred'], + // msg: d.details, + // } + // }) + } + break; + } + }) + $(window).focus(function(){ + if(canBackgroundStream()){ + loadPreviouslyOpenedLiveGridBlocks() + } + }).blur(function(){ + if(canBackgroundStream()){ + closeAllLiveGridPlayers() + } + }) + dashboardSwitchCallbacks.monitorOrder = function(toggleState){ + if(toggleState !== 1){ + $('.monitor_item').attr('data-gs-auto-position','yes') + }else{ + $('.monitor_item').attr('data-gs-auto-position','no') + } + } + dashboardSwitchCallbacks.dontMonStretch = function(toggleState){ + var theBody = $('body') + if(toggleState !== 1){ + theBody.addClass('dont-stretch-monitors') + }else{ + theBody.removeClass('dont-stretch-monitors') + } + } + dashboardSwitchCallbacks.dontShowDetection = function(toggleState){ + if(toggleState !== 1){ + window.dontShowDetection = false + }else{ + window.dontShowDetection = true + } + } + dashboardSwitchCallbacks.monitorMuteAudio = function(toggleState){ + var monitorMutes = dashboardOptions().monitorMutes || {} + $('.monitor_item video').each(function(n,vidEl){ + var el = $(this) + var monitorId = el.parents('[data-mid]').attr('data-mid') + if(toggleState === 1){ + el.attr('muted','muted') + }else{ + if(monitorMutes[monitorId] !== 1){ + el.removeAttr('muted') + } + } + }) + } + dashboardSwitchCallbacks.jpegMode = toggleJpegMode +}) diff --git a/web/assets/js/bs5.livePlayer.js b/web/assets/js/bs5.livePlayer.js new file mode 100644 index 00000000..7c788e5e --- /dev/null +++ b/web/assets/js/bs5.livePlayer.js @@ -0,0 +1,387 @@ +var loadedLivePlayers = {} +var runningJpegStreams = {} +function createLivePlayerTab(monitor){ + if(monitor.mode === 'stop'){ + new PNotify({ + title: lang.sorryNo, + text: lang[`Cannot watch a monitor that isn't running.`], + type: 'danger' + }) + return + } + var monitorDetails = safeJsonParse(monitor.details) + var newTabId = `livePlayer-${monitor.mid}` + var tabLabel = `${lang['Stream']} : ${monitor.name}
    ${monitor.mid}` + var streamElement + if(!loadedLivePlayers[monitor.mid])loadedLivePlayers[monitor.mid] = {} + switch(monitorDetails.stream_type){ + case'hls':case'flv':case'mp4': + streamElement = ``; + break; + case'mjpeg': + streamElement = ''; + break; + case'jpeg': + streamElement = ''; + break; + default://base64//h265 + streamElement = ''; + break; + } + var baseHtml = `
    +
    +
    +
    ${tabLabel}
    +
    +
    +
    + ${streamElement} +
    +
    +
    + ${lang.Host} +
    ${monitor.host}
    +
    + + ${lang['Back']} + +
    +
    +
    ` + var tabCreateResponse = createNewTab(newTabId,tabLabel,baseHtml,{},null,'livePlayer') + if(!tabCreateResponse.existAlready){ + initiateLivePlayer(monitor) + } +} +function initiateLivePlayer(monitor){ + var livePlayerElement = loadedLivePlayers[monitor.mid] + var details = monitor.details + var loadedPlayer = loadedLivePlayers[monitor.mid] + var websocketPath = checkCorrectPathEnding(location.pathname) + 'socket.io' + var newTabId = `livePlayer-${monitor.mid}` + var containerElement = $(`#tab-${newTabId}`) + if(location.search === '?p2p=1'){ + websocketPath = '/socket.io' + // websocketQuery.machineId = machineId + } + switch(details.stream_type){ + case'jpeg': + console.log('Stream Type : JPEG Mode') + break; + case'b64': + if(loadedPlayer.Base64 && loadedPlayer.Base64.connected){ + loadedPlayer.Base64.disconnect() + } + loadedPlayer.Base64 = io(location.origin,{ path: websocketPath, query: websocketQuery, transports: ['websocket'], forceNew: false}) + var ws = loadedPlayer.Base64 + var buffer + ws.on('diconnect',function(){ + console.log('Base64 Stream Disconnected') + }) + ws.on('connect',function(){ + ws.emit('Base64',{ + auth: $user.auth_token, + uid: $user.uid, + ke: monitor.ke, + id: monitor.mid, +// channel: channel + }) + if(!loadedPlayer.ctx || loadedPlayer.ctx.length === 0){ + loadedPlayer.ctx = containerElement.find('canvas'); + } + var ctx = loadedPlayer.ctx[0] + var ctx2d = ctx.getContext("2d") + loadedPlayer.image = new Image() + var image = loadedPlayer.image + image.onload = function() { + loadedPlayer.imageLoading = false + var x = 0 + var y = 0 + ctx.getContext("2d").drawImage(image,x,y,ctx.width,ctx.height) + URL.revokeObjectURL(loadedPlayer.imageUrl) + } + ws.on('data',function(imageData){ + try{ + if(loadedPlayer.imageLoading === true)return console.log('drop'); +// var base64Frame = 'data:image/jpeg;base64,'+$.ccio.base64ArrayBuffer(imageData) + loadedPlayer.imageLoading = true +// loadedPlayer.image.src = base64Frame + var arrayBufferView = new Uint8Array(imageData); + var blob = new Blob( [ arrayBufferView ], { type: "image/jpeg" } ); + loadedPlayer.imageUrl = URL.createObjectURL( blob ); + loadedPlayer.image.src = loadedPlayer.imageUrl + loadedPlayer.last_frame = 'data:image/jpeg;base64,'+$.ccio.base64ArrayBuffer(imageData) + }catch(er){ + console.log(er) + $.ccio.log('base64 frame') + } + $.ccio.init('signal',d); + }) + }) + break; + case'mp4': + setTimeout(function(){ + var stream = containerElement.find('.stream-element'); + var onPoseidonError = function(){ + // setTimeout(function(){ + // $.ccio.cx({f:'monitor',ff:'watch_on',id:monitor.mid},user) + // },5000) + } + if(!loadedPlayer.PoseidonErrorCount)loadedPlayer.PoseidonErrorCount = 0 + if(loadedPlayer.PoseidonErrorCount >= 5)return + if(monitor.details.stream_flv_type==='ws'){ + if(loadedPlayer.Poseidon){ + loadedPlayer.Poseidon.stop() + } + try{ + loadedPlayer.Poseidon = new Poseidon({ + video: stream[0], + auth_token: $user.auth_token, + ke: monitor.ke, + uid: $user.uid, + id: monitor.mid, + url: location.origin, + path: websocketPath, + query: websocketQuery, + onError : onPoseidonError + }) + loadedPlayer.Poseidon.start(); + }catch(err){ + // onPoseidonError() + console.log('onTryPoseidonError',err) + } + }else{ + stream.attr('src',getApiPrefix(`mp4`)+'/'+monitor.mid+'/s.mp4') + stream[0].onerror = function(err){ + console.error(err) + } + } + },1000) + break; + case'flv': + if (flvjs.isSupported()) { + if(loadedPlayer.flv){ + loadedPlayer.flv.destroy() + } + var options = {}; + if(monitor.details.stream_flv_type==='ws'){ + if(monitor.details.stream_flv_maxLatency&&monitor.details.stream_flv_maxLatency!==''){ + monitor.details.stream_flv_maxLatency = parseInt(monitor.details.stream_flv_maxLatency) + }else{ + monitor.details.stream_flv_maxLatency = 20000; + } + options = { + type: 'flv', + isLive: true, + auth_token: $user.auth_token, + ke: monitor.ke, + uid: $user.uid, + id: monitor.mid, + maxLatency: monitor.details.stream_flv_maxLatency, + hasAudio:false, + url: location.origin, + path: websocketPath, + query: websocketQuery + } + }else{ + options = { + type: 'flv', + isLive: true, + url: getApiPrefix(`flv`)+'/'+monitor.mid+'/s.flv' + } + } + loadedPlayer.flv = flvjs.createPlayer(options); + loadedPlayer.flv.attachMediaElement(containerElement.find('.stream-element')[0]); + loadedPlayer.flv.on('error',function(err){ + console.log(err) + }); + loadedPlayer.flv.load(); + loadedPlayer.flv.play(); + }else{ + $.ccio.init('note',{title:'Stream cannot be started',text:'FLV.js is not supported on this browser. Try another stream type.',type:'error'}); + } + break; + case'hls': + function createSteamNow(){ + clearTimeout(loadedPlayer.m3uCheck) + var url = getApiPrefix(`hls`) + '/' + monitor.mid + '/s.m3u8' + $.getJSON(url,function(m3u){ + if(m3u == 'File Not Found'){ + loadedPlayer.m3uCheck = setTimeout(function(){ + createSteamNow() + },2000) + }else{ + var video = containerElement.find('.stream-element')[0] + console.log(containerElement) + if (isAppleDevice) { + video.src = url; + video.addEventListener('loadedmetadata', function() { + setTimeout(function(){ + video.play(); + },3000) + }, false); + }else{ + loadedPlayer.hlsGarbageCollector=function(){ + if(loadedPlayer.hls){ + loadedPlayer.hls.destroy() + URL.revokeObjectURL(video.src) + } + loadedPlayer.hls = new Hls() + loadedPlayer.hls.loadSource(url) + loadedPlayer.hls.attachMedia(video) + loadedPlayer.hls.on(Hls.Events.MANIFEST_PARSED,function() { + if (video.paused) { + video.play(); + } + }); + } + loadedPlayer.hlsGarbageCollector() + loadedPlayer.hlsGarbageCollectorTimer=setInterval(loadedPlayer.hlsGarbageCollector,1000*60*20) + } + } + }) + } + createSteamNow() + break; + case'mjpeg': + var liveStreamElement = containerElement.find('.stream-element') + var setSource = function(){ + liveStreamElement.attr('src',getApiPrefix(`mjpeg`)+'/'+d.id) + liveStreamElement.unbind('ready') + liveStreamElement.ready(function(){ + setTimeout(function(){ + liveStreamElement.contents().find("body").append('') + },1000) + }) + } + setSource() + liveStreamElement.on('error',function(err){ + setTimeout(function(){ + setSource() + },4000) + }) + break; + case'h265': + var player = loadedPlayer.h265Player + var video = containerElement.find('.stream-element')[0] + if (player) { + player.stop() + } + loadedPlayer.h265Player = new libde265.RawPlayer(video) + var player = loadedPlayer.h265Player + player.set_status_callback(function(msg, fps) { + }) + player.launch() + if(loadedPlayer.h265Socket && loadedPlayer.h265Socket.connected){ + loadedPlayer.h265Socket.disconnect() + } + if(loadedPlayer.h265HttpStream && loadedPlayer.abort){ + loadedPlayer.h265HttpStream.abort() + } + if(monitor.details.stream_flv_type==='ws'){ + loadedPlayer.h265Socket = io(location.origin,{ path: websocketPath, query: websocketQuery, transports: ['websocket'], forceNew: false}) + var ws = loadedPlayer.h265Socket + ws.on('diconnect',function(){ + console.log('h265Socket Stream Disconnected') + }) + ws.on('connect',function(){ + ws.emit('h265',{ + auth: $user.auth_token, + uid: $user.uid, + ke: d.ke, + id: d.id, +// channel: channel + }) + ws.on('data',function(imageData){ + player._handle_onChunk(imageData) + }) + }) + }else{ + var url = getApiPrefix(`h265`) + '/' + d.id + '/s.hevc'; + loadedPlayer.h265HttpStream = player.createHttpStream(url) + } + break; + } +} +function closeLivePlayer(tabId){ + try{ + var monitorId = tabId.replace('livePlayer-','') + var livePlayerElement = loadedLivePlayers[monitorId] + if(livePlayerElement.hls){livePlayerElement.hls.destroy()} + if(livePlayerElement.Poseidon){livePlayerElement.Poseidon.stop()} + if(livePlayerElement.Base64){livePlayerElement.Base64.disconnect()} + if(livePlayerElement.h265Socket){livePlayerElement.h265Socket.disconnect()} + if(livePlayerElement.h265Player){livePlayerElement.h265Player.stop()} + if(livePlayerElement.dash){livePlayerElement.dash.reset()} + if(livePlayerElement.h265HttpStream && livePlayerElement.abort){ + livePlayerElement.h265HttpStream.abort() + } + }catch(err){ + console.log(err) + } +} +function stopJpegStream(monitorId){ + console.log('stopJpegStream') + // clearTimeout(loadedMonitors[monitorId].jpegInterval) + // delete(loadedMonitors[monitorId].jpegInterval) + // $(`#monitor_live_${monitorId} .stream-element`).unbind('load') +} +function startJpegStream(monitorId){ + console.log('startJpegStream') + // var loadedMonitor = loadedMonitors[monitorId] + // var monitorDetails = loadedMonitor.details + // var jpegInterval = parseFloat(monitorDetails.jpegInterval) + // if(!monitorDetails.jpegInterval||monitorDetails.jpegInterval===''||isNaN(monitorDetails.jpegInterval)){monitorDetails.jpegInterval=1} + // $.ccio.tm('stream-element',$.ccio.mon[d.ke+d.mid+user.auth_token]); + // monitorDetails.e=$('#monitor_live_'+d.mid+user.auth_token+' .stream-element'); + // $.ccio.init('jpegModeStop',d,user); + // monitorDetails.run=function(){ + // monitorDetails.e.attr('src',$.ccio.init('location',user)+user.auth_token+'/jpeg/'+d.ke+'/'+d.mid+'/s.jpg?time='+(new Date()).getTime()) + // } + // monitorDetails.e.on('load',function(){ + // $.ccio.mon[d.ke+d.mid+user.auth_token].jpegInterval=setTimeout(monitorDetails.run,1000/monitorDetails.jpegInterval); + // }).on('error',function(){ + // $.ccio.mon[d.ke+d.mid+user.auth_token].jpegInterval=setTimeout(monitorDetails.run,1000/monitorDetails.jpegInterval); + // }) + // monitorDetails.run() +} + +$(document).ready(function(e){ + mainSocket.on('f',function (d){ + switch(d.f){ + case'monitor_watch_off':case'monitor_stopping': + // // FOR GRID/MONTAGE + // // destroyLivePlayerBlock + // if(user===$user){ + // d.chosen_set='watch_on' + // }else{ + // d.chosen_set='watch_on_links' + // } + // d.o=dashboardOptions()[d.chosen_set]; + // if(!d.o[d.ke]){d.o[d.ke]={}};d.o[d.ke][d.id]=0;dashboardOptions(d.chosen_set,d.o); + // $.ccio.destroyStream(d,user,(d.f === 'monitor_watch_off')) + break; + case'monitor_watch_on': + // // FOR GRID/MONTAGE + // initLivePlayerBlock() + break; + case'mode_jpeg_off': + // // FOR GRID/MONTAGE + // dashboardOptions('jpeg_on',"0"); + // $.each($.ccio.mon,function(n,v,x){ + // $.ccio.init('jpegModeStop',v); + // if(v.watch===1){ + // $.ccio.cx({f:'monitor',ff:'watch_on',id:v.mid},user) + // } + // }); + // $('body').removeClass('jpegMode') + break; + case'mode_jpeg_on': + // // FOR GRID/MONTAGE + // dashboardOptions('jpeg_on',true); + // $.ccio.init('jpegModeAll'); + // $('body').addClass('jpegMode') + break; + } + }) +}) diff --git a/web/assets/js/bs5.logViewer.js b/web/assets/js/bs5.logViewer.js new file mode 100644 index 00000000..6380a661 --- /dev/null +++ b/web/assets/js/bs5.logViewer.js @@ -0,0 +1,73 @@ +$(document).ready(function(e){ + var theWindow = $('#tab-logViewer') + var logTypeSelector = $('#log_monitors') + var dateRangeSelector = $('#logs_daterange') + var savedLogRows = $('#saved-logs-rows') + var theForm = theWindow.find('form') + var logViewerDataInMemory = {} + //log viewer + dateRangeSelector.daterangepicker({ + startDate: moment().subtract(moment.duration("5:00:00")), + endDate: moment().add(moment.duration("24:00:00")), + timePicker: true, + timePicker24Hour: true, + timePickerSeconds: true, + timePickerIncrement: 30, + locale: { + format: 'MM/DD/YYYY h:mm A' + } + },function(start, end, label){ + drawLogRows() + }); + addOnTabOpen('logViewer',function(){ + logTypeSelector.find('optgroup option').remove() + var html = '' + $.each(loadedMonitors,function(n,v){ + html += createOptionHtml({ + value: v.mid, + label: v.name, + }) + }) + logTypeSelector.find('optgroup').html(html) + drawLogRows() + }) + function drawLogRows(){ + var html = '' + var selectedLogType = logTypeSelector.val() + selectedLogType = selectedLogType === 'all' ? '' : selectedLogType + var currentDateRange = dateRangeSelector.data('daterangepicker'); + var apiEndpoint = getApiPrefix(`logs`) + '/' + selectedLogType + '?start=' + formattedTimeForFilename(currentDateRange.startDate) + '&end=' + formattedTimeForFilename(currentDateRange.endDate) + $.getJSON(apiEndpoint,function(rows){ + logViewerDataInMemory = { + startDate: currentDateRange.startDate, + endDate: currentDateRange.endDate, + url: apiEndpoint, + query: selectedLogType, + rows: rows, + } + if(rows.length === 0){ + html = ''+lang.NoLogsFoundForDateRange+'' + }else{ + $.each(rows,function(n,v){ + html += buildLogRow(v) + }) + } + savedLogRows.html(html) + }) + } + logTypeSelector.change(drawLogRows) + theForm.submit(function(e){ + e.preventDefault() + drawLogRows() + return false + }) + theWindow.find('[download]').click(function(e){ + e.preventDefault() + if(!logViewerDataInMemory.rows){ + console.log('No Logs Found for Download') + return + } + downloadJSON(logViewerDataInMemory,'Shinobi_Logs_'+(new Date())+'.json') + return false; + }) +}) diff --git a/web/assets/js/bs5.login.js b/web/assets/js/bs5.login.js new file mode 100644 index 00000000..e0556ee6 --- /dev/null +++ b/web/assets/js/bs5.login.js @@ -0,0 +1,75 @@ +var loginForm = $('#login-form') +var cachedLoginInfo = localStorage.getItem('ShinobiLogin_'+location.host) +var cachedMachineId = localStorage.getItem('ShinobiAuth_'+location.host) +function generateId(x){ + if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for( var i=0; i < x; i++ ) + t += p.charAt(Math.floor(Math.random() * p.length)); + return t; +} +function onSelectorChange(el){ + try{ + var theParam = el.attr('selector') + var theValue = el.val() + var theSelected = el.find('option:selected').text() + loginForm.find(`.${theParam}_input`).hide() + loginForm.find(`.${theParam}_${theValue}`).show() + loginForm.find(`.${theParam}_text`).text(theSelected) + }catch(err){ + console.log(err) + } +} +if(!cachedMachineId){ + cachedMachineId = generateId(20) + localStorage.setItem('ShinobiAuth_'+location.host,cachedMachineId) +} +$(document).ready(function(){ + $('#machineID').val(cachedMachineId) + $('[selector]').change(function(){ + var el = $(this) + onSelectorChange(el) + }).change(); +}) +loginForm.submit(function(e){ + $('#login-message').remove() + var formValues = loginForm.serializeObject() + if(formValues.remember){ + localStorage.setItem('ShinobiLogin_'+location.host,JSON.stringify({ + mail: formValues.mail, + pass: formValues.pass, + function: formValues.function + })) + }else{ + localStorage.removeItem('ShinobiLogin_'+location.host) + } + if(googleSignIn)googleSignOut(); +}) +if(cachedLoginInfo){ + cachedLoginInfo = JSON.parse(cachedLoginInfo) + $.each(cachedLoginInfo,function(n,v){ + var el = loginForm.find('[name="'+n+'"]') + if(el.attr('type') === 'checkbox'){ + el.prop('checked',true) + }else{ + el.val(v) + } + }) + loginForm.find('[name="remember"]').prop('checked',true) + loginForm.submit() +} +$('[name="function"]').change(function(){ + var removeClass='btn-danger btn-primary btn-success btn-warning' + var addClass='btn-success' + switch($(this).val()){ + case'streamer': + addClass='btn-warning' + break; + case'admin': + addClass='btn-primary' + break; + case'super': + addClass='btn-danger' + break; + } + $('#login-submit').removeClass(removeClass).addClass(addClass) +}) diff --git a/web/libs/js/dash2.monitoredit.js b/web/assets/js/bs5.monitorSettings.js similarity index 57% rename from web/libs/js/dash2.monitoredit.js rename to web/assets/js/bs5.monitorSettings.js index 055bd144..a000caa9 100644 --- a/web/libs/js/dash2.monitoredit.js +++ b/web/assets/js/bs5.monitorSettings.js @@ -1,8 +1,9 @@ +var monitorEditorSelectedMonitor = null $(document).ready(function(e){ //Monitor Editor -$.aM = {} -var monitorEditorWindow = $('#add_monitor') +var monitorEditorWindow = $('#tab-monitorSettings') +var monitorEditorTitle = $('#tab-monitorSettings-title') var monitorsForCopy = $('#copy_settings_monitors') var monitorSectionInputMaps = $('#monSectionInputMaps') var monitorStreamChannels = $('#monSectionStreamChannels') @@ -10,15 +11,17 @@ var monSectionPresets = $('#monSectionPresets') var copySettingsSelector = $('#copy_settings') var monitorPresetsSelection = $('#monitorPresetsSelection') var monitorPresetsNameField = $('#monitorPresetsName') +var monitorGroupSelectors = $('#monitor_groups') +var monitorsList = monitorEditorWindow.find('.monitors_list') +var monitorGroupMutliTriggerSelectContainer = $('#monitor_group_detector_multi') var editorForm = monitorEditorWindow.find('form') var fieldsLoaded = {} var sections = {} var loadedPresets = {} -monitorEditorWindow.find('.follow-list ul').affix(); -$.aM.generateDefaultMonitorSettings = function(){ +function generateDefaultMonitorSettings(){ return { "mode": "start", - "mid": $.ccio.gid(), + "mid": generateId(), "name": "Some Stream", "type": "h264", "host": "", @@ -44,7 +47,7 @@ $.aM.generateDefaultMonitorSettings = function(){ "skip_ping": null, "is_onvif": null, "onvif_port": "", - "primary_input": "0:0", + "primary_input": "0", "aduration": "1000000000", "probesize": "1000000000", "stream_loop": "0", @@ -127,16 +130,15 @@ $.aM.generateDefaultMonitorSettings = function(){ "detector_http_api": null, "detector_send_frames": "1", "detector_lock_timeout": "", - "detector_save": "0", + "detector_save": "1", "detector_fps": "", "detector_scale_x": "640", "detector_scale_y": "480", "detector_record_method": "sip", "detector_trigger": "1", - "detector_trigger_record_fps": "", - "detector_timeout": "10", + "detector_timeout": "0.5", "detector_send_video_length": "", - "watchdog_reset": "0", + "watchdog_reset": "1", "detector_delete_motionless_videos": "0", "det_multi_trig": null, "group_detector_multi": "", @@ -213,7 +215,43 @@ $.aM.generateDefaultMonitorSettings = function(){ "detector_cascades": "", "stream_channels": "", "input_maps": "", - "input_map_choices": "" + "input_map_choices": "", + "substream": { + "input": { + "type": "h264", + "fulladdress": "", + "sfps": "", + "aduration": "", + "probesize": "", + "stream_loop": null, + "rtsp_transport": "", + "accelerator": "0", + "hwaccel": null, + "hwaccel_vcodec": "", + "hwaccel_device": "", + "cust_input": "" + }, + "output": { + "stream_type": "hls", + "rtmp_server_url": "", + "rtmp_stream_key": "", + "stream_mjpeg_clients": "", + "stream_vcodec": "copy", + "stream_acodec": "no", + "hls_time": "", + "hls_list_size": "", + "preset_stream": "", + "stream_quality": "", + "stream_v_br": "", + "stream_a_br": "", + "stream_fps": "", + "stream_scale_x": "640", + "stream_scale_y": "480", + "stream_rotate": null, + "svf": "", + "cust_stream": "" + } + } }, "shto": "[]", "shfr": "[]" @@ -234,29 +272,47 @@ var getHumanizedMonitorConfig = function(monitor){ return humanizedMonitorKeys } var getSelectedMonitorInfo = function(){ - var groupKey = monitorEditorWindow.attr('ke') - var monitorId = monitorEditorWindow.attr('mid') + var groupKey = monitorEditorWindow.attr('data-ke') + var monitorId = monitorEditorWindow.attr('data-mid') return { ke: groupKey, mid: monitorId, auth: $user.auth_token, } } +function getMonitorGroupsSelected(){ + var monitorGroupsInSelection = [] + monitorGroupSelectors.find('input:checked').each(function(n,v){ + var monitorId = $(v).val() + monitorGroupsInSelection.push(monitorId) + }) + return monitorGroupsInSelection +} +function getMonitorTriggerGroupsSelected(){ + var monitorGroupsInSelection = [] + monitorGroupMutliTriggerSelectContainer.find('input:checked').each(function(n,v){ + var groupId = $(v).val() + monitorGroupsInSelection.push(groupId) + }) + return monitorGroupsInSelection +} var differentiateMonitorConfig = function(firstConfig,secondConfig){ console.log(firstConfig,secondConfig) var diffedConfig = {} - var firstConfigEditable = Object.assign(firstConfig,{details:$.parseJSON(firstConfig.details)}) - var secondConfigEditable = Object.assign(secondConfig,{details:$.parseJSON(secondConfig.details)}) - return diffObject(firstConfigEditable,secondConfigEditable) + var firstConfigEditable = Object.assign(firstConfig,{details:safeJsonParse(firstConfig.details)}) + var secondConfigEditable = Object.assign(secondConfig,{details:safeJsonParse(secondConfig.details)}) + var theDiff = diffObject(firstConfigEditable,secondConfigEditable) + console.log(theDiff) + return theDiff } var copyMonitorSettingsToSelected = function(monitorConfig){ - var monitorDetails = $.parseJSON(monitorConfig.details); + var monitorDetails = safeJsonParse(monitorConfig.details); var copyMonitors = monitorsForCopy.val(); var chosenSections = []; var chosenMonitors = {}; if(!copyMonitors||copyMonitors.length===0){ - $.ccio.init('note',{title:lang['No Monitors Selected'],text:lang.monSavedButNotCopied}) + new PNotify({title:lang['No Monitors Selected'],text:lang.monSavedButNotCopied}) return } @@ -267,7 +323,7 @@ var copyMonitorSettingsToSelected = function(monitorConfig){ } }) var alterSettings = function(settingsToAlter,monitor){ - monitor.details = $.parseJSON(monitor.details); + monitor.details = safeJsonParse(monitor.details); var searchElements = [] if(settingsToAlter.indexOf('field=') > -1){ var splitSettingsToAlter = settingsToAlter.split('=') @@ -303,7 +359,7 @@ var copyMonitorSettingsToSelected = function(monitorConfig){ var monitor if(monitorConfig.id === id)return; if(id === '$New'){ - monitor = $.aM.generateDefaultMonitorSettings(); + monitor = generateDefaultMonitorSettings(); //connection monitor.name = monitorConfig.name+' - '+monitor.mid monitor.type = monitorConfig.type @@ -328,18 +384,18 @@ var copyMonitorSettingsToSelected = function(monitorConfig){ monitor.details.hwaccel_vcodec = monitorDetails.hwaccel_vcodec monitor.details.hwaccel_device = monitorDetails.hwaccel_device }else{ - monitor = Object.assign({},$.ccio.init('cleanMon',$.ccio.mon[$user.ke+id+$user.auth_token])); + monitor = Object.assign({},loadedMonitors[id]); } $.each(chosenSections,function(n,section){ monitor = alterSettings(section,monitor) }) $.post(getApiPrefix()+'/configureMonitor/'+$user.ke+'/'+monitor.mid,{data:JSON.stringify(monitor)},function(d){ - $.ccio.log(d) + debugLog(d) }) chosenMonitors[monitor.mid] = monitor; }) } -var getMonitorEditFormFields = function(){ +window.getMonitorEditFormFields = function(){ var response = {ok: true} var monitorConfig = editorForm.serializeObject() var errorsFound = [] @@ -357,6 +413,15 @@ var getMonitorEditFormFields = function(){ } } if(monitorConfig.name == ''){errorsFound.push('Monitor Name cannot be blank')} + //edit details + monitorConfig.details = safeJsonParse(monitorConfig.details) + monitorConfig.details.substream = getSubStreamChannelFields() + monitorConfig.details.groups = getMonitorGroupsSelected() + monitorConfig.details.group_detector_multi = getMonitorTriggerGroupsSelected() + monitorConfig.details.input_map_choices = monitorSectionInputMapsave() + // TODO : Input Maps and Stream Channels (does old way at the moment) + + // if(monitorConfig.protocol=='rtsp'){monitorConfig.ext='mp4',monitorConfig.type='rtsp'} if(errorsFound.length > 0){ response.ok = false @@ -366,69 +431,131 @@ var getMonitorEditFormFields = function(){ response.monitorConfig = monitorConfig return response } -var addSection = function(section){ - sections[section.name] = { - id: section.id, - color: section.color - } - if(section.info){ - $.each(section.info,function(m,block){ - if(block.isFormGroupGroup === true){ - addSection(block) - }else if(block.name){ - fieldsLoaded[block.name] = block - } - }) - } - if(section.blocks){ - $.each(section.blocks,function(m,block){ - addSection(block) - }) - } + +function drawMonitorSettingsSubMenu(){ + drawSubMenuItems('monitorSettings',definitions['Monitor Settings']) } -$.each($.ccio.definitions['Monitor Settings'].blocks,function(n,section){ - addSection(section) + +function getAdditionalInputMapFields(tempID,channelId){ + var fieldInfo = monitorSettingsAdditionalInputMapFieldHtml.replaceAll('$[TEMP_ID]',tempID).replaceAll('$[NUMBER]',channelId) + return fieldInfo +} + +function getAdditionalStreamChannelFields(tempID,channelId){ + var fieldInfo = monitorSettingsAdditionalStreamChannelFieldHtml.replaceAll('$[TEMP_ID]',tempID).replaceAll('$[NUMBER]',channelId) + return fieldInfo +} + +addOnTabOpen('monitorSettings', function () { + setFieldVisibility() + drawMonitorSettingsSubMenu() }) -var drawList = function(){ - var list = monitorEditorWindow.find('.follow-list ul') - var html = '' - $.each(sections,function(sectionName,section){ - var sectionId = section.id - var el = $('#' + sectionId + ':visible') - if(el.length > 0){ - html += `
  • ${sectionName}
  • ` - } + +addOnTabReopen('monitorSettings', function () { + setFieldVisibility() + drawMonitorSettingsSubMenu() +}) +function drawInputMapHtml(options){ + var tmp = '' + var tempID = generateId() + options = options ? options : {} + if(!options.channel){ + var numberOfChannelsDrawn = $('#monSectionInputMaps .input-map').length + options.channel = numberOfChannelsDrawn+1 + } + tmp+=getAdditionalInputMapFields(tempID,options.channel) + monitorSectionInputMaps.append(tmp) + monitorSectionInputMaps.find('.input-map').last().find('[map-detail="aduration"]').change() + return tempID; +} +function drawStreamChannelHtml(options){ + var tmp = '' + var tempID = generateId() + options = options ? options : {} + if(!options.channel){ + var numberOfChannelsDrawn = $('#monSectionStreamChannels .stream-channel').length + options.channel=numberOfChannelsDrawn + } + tmp+=`${getAdditionalStreamChannelFields(tempID,options.channel)}` + monitorStreamChannels.append(tmp) + monitorStreamChannels.find('.stream-channel').last().find('[channel-detail="stream_vcodec"]').change() + return tempID; +} +function replaceMap(string,mapNumber){ + var newString = string.split(':') + newString[0] = `${mapNumber}` + return newString.join(':') +} +function replaceMapInName(string,mapNumber){ + var newString = string.split('(') + newString[1] = replaceMap(newString[1],mapNumber) + var lastIndex = newString.length - 1 + if(!newString[lastIndex].endsWith(')')){ + newString[lastIndex] = newString + ')' + } + return newString.join('(') +} +function buildMapSelectorOptionsBasedOnAddedMaps(){ + var baseOptionSet = definitions['Monitor Settings'].blocks.Input.info.find((item) => {return item.name === 'detail=primary_input'}).possible + var newOptGroup = [baseOptionSet] + var addedInputMaps = monitorEditorWindow.find('.input-map') + $.each(addedInputMaps,function(n){ + var mapNumber = n + 1 + var newOptionSet = [] + $.each(baseOptionSet,function(nn,option){ + newOptionSet.push({ + "name": replaceMapInName(option.name,mapNumber), + "value": replaceMap(option.value,mapNumber) + }) + }) + newOptGroup[mapNumber] = newOptionSet }) - list.html(html) + return newOptGroup } -monitorEditorWindow.on('shown.bs.modal', function () { - triggerSecondaryHideCheckOnAll() - drawList() -}) -$.aM.import = function(options){ +function drawInputMapSelectorHtml(options,parent){ + if(!options.map)options.map = ''; + var availableInputMapSelections = buildMapSelectorOptionsBasedOnAddedMaps() + var html = `
    +
    + +
    +
    +    +
    +
    ` + parent.prepend(html) +} +function importIntoMonitorEditor(options){ var monitorConfig = options.values || options $.get(getApiPrefix()+'/hls/'+monitorConfig.ke+'/'+monitorConfig.mid+'/detectorStream.m3u8',function(data){ $('#monEditBufferPreview').html(data) }) monitorEditorWindow.find('.edit_id').text(monitorConfig.mid); - monitorEditorWindow.attr('mid',monitorConfig.mid).attr('ke',monitorConfig.ke).attr('auth',monitorConfig.auth || $user.auth_token) + monitorEditorWindow.attr('data-mid',monitorConfig.mid).attr('data-ke',monitorConfig.ke) $.each(monitorConfig,function(n,v){ monitorEditorWindow.find('[name="'+n+'"]').val(v).change() }) - var monitorDetails = $.parseJSON(monitorConfig.details); + var monitorDetails = safeJsonParse(monitorConfig.details); //get maps monitorSectionInputMaps.empty() if(monitorDetails.input_maps && monitorDetails.input_maps !== ''){ - var input_maps - try{ - input_maps = $.parseJSON(monitorDetails.input_maps) - }catch(er){ - input_maps = monitorDetails.input_maps; - } + var input_maps = safeJsonParse(monitorDetails.input_maps) if(input_maps.length > 0){ showInputMappingFields() $.each(input_maps,function(n,v){ - var tempID = $.ccio.tm('input-map') + var tempID = drawInputMapHtml() var parent = $('#monSectionMap'+tempID) $.each(v,function(m,b){ parent.find('[map-detail="'+m+'"]').val(b).change() @@ -443,12 +570,12 @@ $.aM.import = function(options){ if(monitorDetails.stream_channels&&monitorDetails.stream_channels!==''){ var stream_channels try{ - stream_channels = $.parseJSON(monitorDetails.stream_channels) + stream_channels = safeJsonParse(monitorDetails.stream_channels) }catch(er){ stream_channels = monitorDetails.stream_channels; } $.each(stream_channels,function(n,v){ - var tempID = $.ccio.tm('stream-channel') + var tempID = drawStreamChannelHtml() var parent = $('#monSectionChannel'+tempID) $.each(v,function(m,b){ parent.find('[channel-detail="'+m+'"]').val(b) @@ -460,17 +587,29 @@ $.aM.import = function(options){ if(monitorDetails.input_map_choices&&monitorDetails.input_map_choices!==''){ var input_map_choices try{ - input_map_choices = $.parseJSON(monitorDetails.input_map_choices) + input_map_choices = safeJsonParse(monitorDetails.input_map_choices) }catch(er){ input_map_choices = monitorDetails.input_map_choices; } $.each(input_map_choices,function(n,v){ $.each(v,function(m,b){ var parent = $('[input-mapping="'+n+'"] .choices') - $.ccio.tm('input-map-selector',b,parent) + drawInputMapSelectorHtml(b,parent) }) }) } + // substream + $.each(['input','output'],function(n,direction){ + // detail-substream-input + // detail-substream-output + var keyName = `detail-substream-${direction}` + monitorEditorWindow.find(`[${keyName}]`).each(function(n,el){ + var key = $(el).attr(keyName); + var value = monitorDetails.substream && monitorDetails.substream[direction] ? monitorDetails.substream[direction][key] : '' + monitorEditorWindow.find(`[${keyName}="${key}"]`).val(value).change(); + }) + }) + // monitorEditorWindow.find('[detail]').each(function(n,v){ v=$(v).attr('detail');if(!monitorDetails[v]){monitorDetails[v]=''} }) @@ -483,7 +622,7 @@ $.aM.import = function(options){ }); $.each(monitorDetails,function(n,v){ try{ - var variable=$.parseJSON(v) + var variable=safeJsonParse(v) }catch(err){ var variable=v } @@ -511,27 +650,23 @@ $.aM.import = function(options){ }); try{ $.each(['groups','group_detector_multi'],function(m,b){ - var tmp='' + var html = '' $.each($user.mon_groups,function(n,v){ - tmp+='
  • '; - tmp+=''; - tmp+=v.name; - tmp+=''; - tmp+=''; - tmp+=''; - tmp+=''; - tmp+='
  • '; + var isSelected = monitorDetails[b] && monitorDetails[b].indexOf(v.id) > -1 + html += `
    +
    +
    + ${v.name} (${v.id}) +
    +
    + +
    +
    +
    ` }) - $('#monitor_'+b).html(tmp) + $('#monitor_'+b).html(html) }) - componentHandler.upgradeAllRegistered() + console.log(`!!!!!!!\ncomponentHandler.upgradeAllRegistered\n!!!!!!!`) }catch(er){ console.log(er) //no group, this 'try' will be removed in future. @@ -541,14 +676,17 @@ $.aM.import = function(options){ drawPresetsSection() var tmp = ''; - $.each($.ccio.mon,function(n,v){ - if(v.ke === $user.ke){ - tmp += $.ccio.tm('option',{auth_token:$user.auth_token,id:v.mid,name:v.name},null,$user); + $.each(loadedMonitors,function(n,monitor){ + if(monitor.ke === $user.ke){ + tmp += createOptionHtml({ + value: monitor.mid, + label: monitor.name + }) } }) monitorsForCopy.find('optgroup').html(tmp) - triggerSecondaryHideCheckOnAll() - drawList() + setFieldVisibility() + drawMonitorSettingsSubMenu() } //parse "Automatic" field in "Input" Section monitorEditorWindow.on('change','.auto_host_fill input,.auto_host_fill select',function(e){ @@ -618,20 +756,24 @@ monitorEditorWindow.on('change','[detail="auto_host"]',function(e){ delete(parsedURL) } }) -monitorEditorWindow.find('.refresh_cascades').click(function(e){ - $.ccio.cx({f:'ocv_in',data:{f:'refreshPlugins',ke:$user.ke}}) -}) editorForm.submit(function(e){ e.preventDefault(); var validation = getMonitorEditFormFields() if(!validation.ok){ var errorsFound = validation.errors $.sM.e.find('.msg').html(errorsFound.join('
    ')); - $.ccio.init('note',{title:'Configuration Invalid',text:errorsFound.join('
    '),type:'error'}); + new PNotify({title:'Configuration Invalid',text:errorsFound.join('
    '),type:'error'}); } var monitorConfig = validation.monitorConfig $.post(getApiPrefix()+'/configureMonitor/'+$user.ke+'/'+monitorConfig.mid,{data:JSON.stringify(monitorConfig)},function(d){ - $.ccio.log(d) + if(d.ok === false){ + new PNotify({ + title: lang['Action Failed'], + text: d.msg, + type: 'danger' + }) + } + debugLog(d) }) // if(copySettingsSelector.val() === '1'){ @@ -649,7 +791,7 @@ var mapPlacementInit = function(){ }) } var monitorSectionInputMapsave = function(){ - var mapContainers = $('[input-mapping]'); + var mapContainers = monitorEditorWindow.find('[input-mapping]'); var stringForSave = {} mapContainers.each(function(q,t){ var mapRowElement = $(t).find('.map-row'); @@ -663,7 +805,7 @@ var monitorSectionInputMapsave = function(){ }); stringForSave[$(t).attr('input-mapping')] = mapRow; }); - monitorEditorWindow.find('[detail="input_map_choices"]').val(JSON.stringify(stringForSave)).change(); + return stringForSave } monitorSectionInputMaps.on('click','.delete',function(){ $(this).parents('.input-map').remove() @@ -690,15 +832,10 @@ monitorEditorWindow.on('change','[map-detail]',function(){ monitorEditorWindow.find('[detail="input_maps"]').val(JSON.stringify(selectedMaps)).change() }) monitorEditorWindow.on('click','[input-mapping] .add_map_row',function(){ - $.ccio.tm('input-map-selector',{},$(this).parents('[input-mapping]').find('.choices')) - monitorSectionInputMapsave() + drawInputMapSelectorHtml({},$(this).parents('[input-mapping]').find('.choices')) }) monitorEditorWindow.on('click','[input-mapping] .delete_map_row',function(){ $(this).parents('.map-row').remove() - monitorSectionInputMapsave() -}) -monitorEditorWindow.on('change','[map-input]',function(){ - monitorSectionInputMapsave() }) ////////////////// //Stream Channels @@ -720,9 +857,29 @@ var channelPlacementInit = function(){ _this.attr('stream-channel',n) _this.find('.place').text(n) _this.find('[input-mapping]').attr('input-mapping','stream_channel-'+n) - monitorSectionInputMapsave() }) } +var getSubStreamChannelFields = function(){ + var selectedChannels = { + input: getPseudoFields('detail-substream-input'), + output: getPseudoFields('detail-substream-output') + } + return selectedChannels +} +var getPseudoFields = function(fieldKey,parent){ + parent = parent || monitorEditorWindow + fieldKey = fieldKey || 'detail-substream-input' + var fields = {} + var fieldsAssociated = parent.find(`[${fieldKey}]`) + $.each(fieldsAssociated,function(m,b){ + var el = $(b); + var paramKey = el.attr(fieldKey) + var value = el.val() + fields[paramKey] = value + }); + console.log(fields) + return fields +} var buildMonitorURL = function(){ var user = monitorEditorWindow.find('[detail="muser"]').val(); var pass = monitorEditorWindow.find('[detail="mpass"]').val(); @@ -737,7 +894,7 @@ var buildMonitorURL = function(){ if(host.indexOf('@') === -1 && user !== ''){ host = user + ':' + pass + '@' + host; } - url = $.ccio.init('url',{ + url = compileConnectUrl({ user: user, pass: pass, host: host, @@ -757,27 +914,65 @@ var showInputMappingFields = function(showMaps){ }else{ el.hide() } - triggerSecondaryHideCheckOnAll() - drawList() + setFieldVisibility() + drawMonitorSettingsSubMenu() } -var triggerSecondaryHideCheck = function(el){ - var key = el.attr('selector') - var value = el.val(); - var triggerChange = el.attr('triggerchange') - var triggerChangeIgnore = el.attr('triggerChangeIgnore') - editorForm.find('.' + key + '_input').hide() - editorForm.find('.' + key + '_' + value).show(); - editorForm.find('.' + key + '_text').text($(this).find('option:selected').text()) - if(triggerChange && triggerChange !== '' && !triggerChangeIgnore || (triggerChangeIgnore && triggerChangeIgnore.split(',').indexOf(value) === -1)){ - console.log(triggerChange) - $(triggerChange).trigger('change') +function setFieldVisibilityNewWay(){ + var validation = getMonitorEditFormFields() + if(!validation.ok){ + return console.log('Failed setFieldVisibilityNewWay',new Error(),validation) + } + var monitorConfig = validation.monitorConfig + var monitorDetails = safeJsonParse(monitorConfig.details) + var commonChecks = { + streamSectionCopyModeVisibilities: `monitorDetails.stream_vcodec === 'libx264' || + monitorDetails.stream_vcodec === 'libx265' || + monitorDetails.stream_vcodec === 'h264_nvenc' || + monitorDetails.stream_vcodec === 'hevc_nvenc' || + monitorDetails.stream_vcodec === 'no' || + + monitorDetails.stream_type === 'mjpeg' || + monitorDetails.stream_type === 'b64' || + ((monitorDetails.stream_type === 'hls' || monitorDetails.stream_type === 'mp4') && monitorDetails.stream_vcodec !== 'copy') || + monitorDetails.stream_type === 'gif' || + monitorDetails.stream_type === 'flv'` + } + editorForm.find('[visibility-conditions]').each(function(n,v){ + var el = $(v) + var visibilityConditions = el.attr('visibility-conditions') + var response = true + var commonCheck = commonChecks[visibilityConditions] + if(commonCheck){ + response = eval(commonCheck) + }else{ + response = eval(visibilityConditions) + } + if(response){ + el.show() + }else{ + el.hide() + } + }) +} +function setFieldVisibilityOldWay(formElement){ + var listToShow = [] + formElement.find('[selector]').each(function(n,v){ + var el = $(this) + var keyName = el.attr('selector') + var value = el.val() + var toShow = `${keyName}_${value}` + listToShow.push(toShow) + formElement.find(`.${keyName}_input`).hide() + }) + for (let i = 0; i < listToShow.length; i++) { + var item = listToShow[i]; + var elements = formElement.find(`[class*="${item}"]`) + elements.show() } } -var triggerSecondaryHideCheckOnAll = function(){ - monitorEditorWindow.find('[selector]').each(function(){ - var el = $(this); - triggerSecondaryHideCheck(el) - }) +function setFieldVisibility(){ + setFieldVisibilityOldWay(editorForm) + setFieldVisibilityNewWay() } monitorStreamChannels.on('click','.delete',function(){ $(this).parents('.stream-channel').remove() @@ -787,83 +982,9 @@ monitorStreamChannels.on('click','.delete',function(){ monitorEditorWindow.on('change','[channel-detail]',function(){ monitorStreamChannelsave() }) -////////////////// -monitorEditorWindow.on('change','[groups]',function(){ - var e={}; - var el = monitorEditorWindow.find('[groups]:checked'); - var selectedGroups = []; - el.each(function(n,v){ - selectedGroups.push($(v).val()) - }); - monitorEditorWindow.find('[detail="groups"]').val(JSON.stringify(selectedGroups)).change() -}) -monitorEditorWindow.on('change','[group_detector_multi]',function(){ - var e={}; - var el = monitorEditorWindow.find('[group_detector_multi]:checked'); - var selectedMultiTrigger=[]; - el.each(function(n,v){ - selectedMultiTrigger.push($(v).val()) - }); - monitorEditorWindow.find('[detail="group_detector_multi"]').val(JSON.stringify(selectedMultiTrigger)).change() -}) -monitorEditorWindow.on('change','.detector_cascade_selection',function(){ - var e={}; - var el = monitorEditorWindow.find('.detector_cascade_selection:checked'); - var selectedCascades = {}; - el.each(function(n,v){ - selectedCascades[$(v).val()]={} - }); - monitorEditorWindow.find('[detail="detector_cascades"]').val(JSON.stringify(selectedCascades)).change() -}) -//monitorEditorWindow.on('change','.detector_cascade_selection',function(){ -// var e={}; -// e.details=monitorEditorWindow.find('[name="details"]') -// try{ -// e.detailsVal=$.parseJSON(e.details.val()) -// }catch(err){ -// e.detailsVal={} -// } -// e.detailsVal.detector_cascades=[]; -// var el = monitorEditorWindow.find('.detector_cascade_selection:checked'); -// el.each(function(n,v){ -// e.detailsVal.detector_cascades.push($(v).val()) -// }); -// e.details.val(JSON.stringify(e.detailsVal)) -//}) -monitorEditorWindow.find('.probe_config').click(function(){ +monitorEditorWindow.find('.probe-monitor-settings').click(function(){ $.pB.submit(buildMonitorURL(),true) }) -monitorEditorWindow.find('.import_config').click(function(e){ - var el = $(this); - var monitorId = el.parents('[mid]').attr('mid'); - $.confirm.create({ - title: lang['Import Monitor Configuration'], - body: lang.ImportMonitorConfigurationText+'
    ', - clickOptions: { - title: 'Import', - class: 'btn-primary' - }, - clickCallback: function(){ - try{ - var monitorConfig = $.parseJSON($.confirm.e.find('textarea').val()); - $.aM.import(monitorConfig) - monitorEditorWindow.modal('show') - }catch(err){ - $.ccio.log(err) - $.ccio.init('note',{title:lang['Invalid JSON'],text:lang.InvalidJSONText,type:'error'}) - } - } - }) - $.confirm.e.find('.upload').change(function(e){ - var files = e.target.files; // FileList object - f = files[0]; - var reader = new FileReader(); - reader.onload = function(ee) { - $.confirm.e.find('textarea').val(ee.target.result); - } - reader.readAsText(f); - }); -}); monitorEditorWindow.find('.save_config').click(function(e){ //export monior config in view var el = $(this); @@ -872,20 +993,15 @@ monitorEditorWindow.find('.save_config').click(function(e){ if(!monitorId||monitorId===''){ monitorId='NewMonitor' } - form.details = $.parseJSON(form.details) - e.dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(form)); - $('#temp').html('') - .find('a') - .attr('href',e.dataStr) - .attr('download','Shinobi_'+monitorId+'_config.json') - [0].click() + form.details = safeJsonParse(form.details) + downloadJSON(form,'Shinobi_'+monitorId+'_config.json') }); -monitorEditorWindow.find('.add_map').click(function(e){ +monitorEditorWindow.find('.add-input-to-monitor-settings').click(function(e){ showInputMappingFields() - $.ccio.tm('input-map') + drawInputMapHtml() }); -monitorEditorWindow.find('.add_channel').click(function(e){ - $.ccio.tm('stream-channel') +monitorEditorWindow.find('.add-channel-to-monitor-settings').click(function(e){ + drawStreamChannelHtml() }); editorForm.find('[detail="stream_type"]').change(function(e){ var el = $(this); @@ -895,11 +1011,14 @@ editorForm.find('[name="type"]').change(function(e){ var el = $(this); if(el.val()==='h264')editorForm.find('[name="protocol"]').val('rtsp').change() }) -editorForm.find('[detail]').change($.ccio.form.details) +editorForm.find('[detail]').change(function(){ + onDetailFieldChange(this) +}) editorForm.on('change','[selector]',function(){ var el = $(this); - triggerSecondaryHideCheck(el) - drawList() + onSelectorChange(el,editorForm) + setFieldVisibilityNewWay() + drawMonitorSettingsSubMenu() }); editorForm.find('[name="type"]').change(function(e){ var el = $(this); @@ -914,34 +1033,34 @@ editorForm.find('[name="type"]').change(function(e){ break; } }); - $.aM.connectedDetectorPlugins = {} - $.aM.addDetectorPlugin = function(name,d){ - $.aM.connectedDetectorPlugins[d.plug] = { + var connectedDetectorPlugins = {} + function addDetectorPlugin(name,d){ + connectedDetectorPlugins[d.plug] = { id: d.id, plug: d.plug, notice: d.notice, connectionType: d.connectionType } - $.aM.drawPluginElements() + drawPluginElements() } - $.aM.removeDetectorPlugin = function(name){ - delete($.aM.connectedDetectorPlugins[name]) - $.aM.drawPluginElements(name) + function removeDetectorPlugin(name){ + delete(connectedDetectorPlugins[name]) + drawPluginElements(name) } - $.aM.drawPluginElements = function(){ - if(Object.keys($.aM.connectedDetectorPlugins).length === 0){ + function drawPluginElements(){ + if(Object.keys(connectedDetectorPlugins).length === 0){ $('.stream-objects .stream-detected-object').remove() $('.shinobi-detector').hide() $('.shinobi-detector-msg').empty() $('.shinobi-detector_name').empty() $('.shinobi-detector_plug').hide() $('.shinobi-detector-invert').show() - triggerSecondaryHideCheckOnAll() - drawList() + setFieldVisibility() + drawMonitorSettingsSubMenu() }else{ var pluginTitle = [] var pluginNotice = [] - $.each($.aM.connectedDetectorPlugins,function(name,d){ + $.each(connectedDetectorPlugins,function(name,d){ pluginTitle.push(name) if(d.notice){ pluginNotice.push('' + d.plug + ' : ' + d.notice) @@ -952,13 +1071,13 @@ editorForm.find('[name="type"]').change(function(e){ $('.shinobi-detector-invert').hide() $('.shinobi-detector_name').text(pluginTitle.join(', ')) if(pluginNotice.length > 0)$('.shinobi-detector-msg').text(pluginNotice.join('
    ')) - triggerSecondaryHideCheckOnAll() - drawList() + setFieldVisibility() + drawMonitorSettingsSubMenu() } } // presets var loadPresets = function(callback){ - $.get(getApiPrefix() + '/monitorStates/' + $user.ke,function(d){ + $.getJSON(getApiPrefix() + '/monitorStates/' + $user.ke,function(d){ var presets = d.presets loadedPresets = {} $.each(presets,function(n,preset){ @@ -981,37 +1100,42 @@ editorForm.find('[name="type"]').change(function(e){ humanizedMonitorKeys = getHumanizedMonitorConfig(monitor) } }) - html += `
  • - - ${preset.name} ${presetMonitors.length} Monitor${presetMonitors.length > 1 ? 's' : ''}   - ${hasSelectedMonitor ? `
      ${$.ccio.init('jsontoblock',humanizedMonitorKeys)}
    ` : ''} -
    - - - - -
  • ` + html += `
    +
    +
    + ${preset.name} +
    +
    + ${presetMonitors.length} Monitor${presetMonitors.length > 1 ? 's' : ''} + ${hasSelectedMonitor ? `
      ${jsonToHtmlBlock(humanizedMonitorKeys)}
    ` : ''} +
    +
    + +
    +
    + +
    +
    +
    ` }) monitorPresetsSelection.html(html) - componentHandler.upgradeAllRegistered() + console.log(`!!!!!!!\ncomponentHandler.upgradeAllRegistered\n!!!!!!!`) } var addNewPreset = function(callback){ var newName = monitorPresetsNameField.val() if(newName === ''){ - return $.ccio.init('note',{title:lang['Invalid Data'],text:lang['Name cannot be empty.'],type:'error'}) + return new PNotify({title:lang['Invalid Data'],text:lang['Name cannot be empty.'],type:'error'}) } var data = JSON.stringify({ monitors: [] }) $.post(getApiPrefix() + '/monitorStates/' + $user.ke + '/' + newName + '/insert',{data:data},function(d){ - $.ccio.log(d) + debugLog(d) if(d.ok === true){ loadPresets(function(presets){ if(callback)callback(d) }) - $.ccio.init('note',{title:lang.Success,text:d.msg,type:'success'}) + new PNotify({title:lang.Success,text:d.msg,type:'success'}) } }) } @@ -1019,7 +1143,7 @@ editorForm.find('[name="type"]').change(function(e){ var preset = loadedPresets[presetName] var monitorsAssociated = `${lang.Presets}

    ` $.each(preset.details.monitors,function(n,monitorConfigPartial){ - monitorsAssociated += `
    ${$.ccio.init('jsontoblock',getHumanizedMonitorConfig(monitorConfigPartial))}
    ` + monitorsAssociated += `
    ${jsonToHtmlBlock(getHumanizedMonitorConfig(monitorConfigPartial))}
    ` }) monitorsAssociated += '
    ' $.confirm.create({ @@ -1031,12 +1155,12 @@ editorForm.find('[name="type"]').change(function(e){ }, clickCallback: function(){ $.post(getApiPrefix() + '/monitorStates/' + $user.ke + '/' + presetName + '/delete',function(d){ - $.ccio.log(d) + debugLog(d) if(d.ok === true){ loadPresets(function(presets){ if(callback)callback(d) }) - $.ccio.init('note',{title:lang.Success,text:d.msg,type:'success'}) + new PNotify({title:lang.Success,text:d.msg,type:'success'}) } }) } @@ -1060,7 +1184,7 @@ editorForm.find('[name="type"]').change(function(e){ } var monitorConfig = validation.monitorConfig console.log(monitorConfig.mid) - var inMemoryMonitorConfig = Object.assign({},$.ccio.init('cleanMon',$.ccio.mon[$user.ke+monitorConfig.mid+$user.auth_token])); + var inMemoryMonitorConfig = Object.assign({},loadedMonitors[monitorConfig.mid]); var currentPreset = loadedPresets[presetName] var presetMonitors = currentPreset.details.monitors || [] var newMonitorsArray = [].concat(presetMonitors) @@ -1072,7 +1196,7 @@ editorForm.find('[name="type"]').change(function(e){ //validateMonitorPreset var monitorPresetValidation = validateMonitorPreset(monitorPartialToAdd) if(!monitorPresetValidation.ok){ - $.ccio.init('note',{title:lang.monitorStatesError,text:monitorPresetValidation.msg,type:'warning'}) + new PNotify({title:lang.monitorStatesError,text:monitorPresetValidation.msg,type:'warning'}) callback(true) return } @@ -1086,12 +1210,12 @@ editorForm.find('[name="type"]').change(function(e){ monitors: newMonitorsArray }) $.post(getApiPrefix() + '/monitorStates/' + $user.ke + '/' + presetName + '/edit',{data:data},function(d){ - $.ccio.log(d) + debugLog(d) if(d.ok === true){ loadPresets(function(presets){ callback(null,d) }) - $.ccio.init('note',{title:lang.Success,text:d.msg,type:'success'}) + new PNotify({title:lang.Success,text:d.msg,type:'success'}) } }) } @@ -1113,12 +1237,12 @@ editorForm.find('[name="type"]').change(function(e){ monitors: newMonitorsArray }) $.post(getApiPrefix() + '/monitorStates/' + $user.ke + '/' + presetName + '/edit',{data:data},function(d){ - $.ccio.log(d) + debugLog(d) if(d.ok === true){ loadPresets(function(presets){ callback(d) }) - $.ccio.init('note',{title:lang.Success,text:d.msg,type:'success'}) + new PNotify({title:lang.Success,text:d.msg,type:'success'}) } }) } @@ -1132,10 +1256,117 @@ editorForm.find('[name="type"]').change(function(e){ }, clickCallback: function(){ var monitorConfigPartial = preset.details.monitors.find(monitor => monitor.mid === monitorId) || {}; - var copyCurrentConfig = $.ccio.init('cleanMon',$.ccio.mon[$user.ke+monitorId+$user.auth_token]) - copyCurrentConfig.details = $.parseJSON(copyCurrentConfig.details) + var copyCurrentConfig = loadedMonitors[monitorConfig.mid] + copyCurrentConfig.details = safeJsonParse(copyCurrentConfig.details) var monitorObjectToLoad = mergeDeep(copyCurrentConfig,monitorConfigPartial); - $.aM.import(monitorObjectToLoad) + importIntoMonitorEditor(monitorObjectToLoad) + } + }) + } + window.openMonitorEditorPage = function(monitorId){ + var monitorConfigToLoad; + monitorEditorWindow.find('.am_notice').hide() + monitorEditorWindow.find('[detailcontainer="detector_cascades"]').prop('checked',false).parents('.mdl-js-switch').removeClass('is-checked') + if(!loadedMonitors[monitorId]){ + //new monitor + monitorEditorWindow.find('.am_notice_new').show() + monitorEditorWindow.find('[monitor="delete"]').hide() + monitorEditorTitle.find('span').text(lang['Add New']) + monitorEditorTitle.find('i').attr('class','fa fa-plus') + monitorConfigToLoad = generateDefaultMonitorSettings() + }else{ + //edit monitor + monitorConfigToLoad = loadedMonitors[monitorId] + monitorEditorWindow.find('.am_notice_edit').show() + monitorEditorWindow.find('[monitor="delete"]').show() + monitorEditorTitle.find('span').html(`${monitorConfigToLoad.name} ${monitorConfigToLoad.mid}`) + monitorEditorTitle.find('i').attr('class','fa fa-wrench') + } + monitorEditorSelectedMonitor = monitorConfigToLoad + importIntoMonitorEditor(monitorConfigToLoad) + openTab(`monitorSettings`,{},null) + } + function onMonitorEdit(d){ + var monitorId = d.mid || d.id + var newMonitorData = d.mon + var loadedMonitor = loadedMonitors[monitorId] + clearMonitorTimers(monitorId) + var montageElement = $('#monitor_live_' + monitorId); + montageElement.find('.stream-detected-object').remove() + var watchOnInfo = dashboardOptions()['watch_on'] || {}; + if(newMonitorData.details.cords instanceof Object){ + newMonitorData.details.cords = JSON.stringify(newMonitorData.details.cords) + } + newMonitorData.details = JSON.stringify(newMonitorData.details); + if(!loadedMonitor){ + loadedMonitors[monitorId] = {} + loadedMonitor = loadedMonitors[monitorId] + } + loadedMonitor.previousStreamType = newMonitorData.details.stream_type + $.each(newMonitorData,function(n,v){ + loadedMonitor[n] = n === 'details' ? safeJsonParse(v) : v + }) + if(d.new === true){ + drawMonitorIconToMenu(newMonitorData) + } + switch(newMonitorData.mode){ + case'start':case'record': + if(watchOnInfo[newMonitorData.ke] && watchOnInfo[newMonitorData.ke][newMonitorData.mid] === 1){ + mainSocket.f({ + f: 'monitor', + ff: 'watch_on', + id: monitorId + }) + } + break; + } + setCosmeticMonitorInfo(newMonitorData) + drawMonitorGroupList() + if(!d.silenceNote){ + new PNotify({ + title: 'Monitor Saved', + text: ''+newMonitorData.name+' '+newMonitorData.mid+' has been saved.', + type: 'success' + }) + } + } + function clearMonitorTimers(monitorId){ + var theMonitor = loadedMonitors[monitorId] + if(theMonitor){ + clearTimeout(theMonitor._signal); + clearInterval(theMonitor.hlsGarbageCollectorTimer) + clearTimeout(theMonitor.jpegInterval); + clearInterval(theMonitor.signal); + clearInterval(theMonitor.m3uCheck); + if(theMonitor.Base64 && theMonitor.Base64.connected){ + theMonitor.Base64.disconnect() + } + if(theMonitor.Poseidon){ + theMonitor.Poseidon.stop() + } + } + } + function resetMonitorEditor(){ + $.confirm.create({ + title: lang.wannaReset, + body: lang.undoAllUnsaveChanges, + clickOptions: { + title: lang['Reset'], + class:'btn-danger' + }, + clickCallback: function(){ + openMonitorEditorPage() + } + }) + } + window.writeToMonitorSettingsWindow = function(monitorValues){ + $.each(monitorValues,function(key,value){ + if(key === `details`){ + $.each(value,function(dkey,dvalue){ + monitorEditorWindow.find(`[detail="${dkey}"]`).val(dvalue).change() + }) + }else{ + monitorEditorWindow.find(`[name="${key}"]`).val(value).change() } }) } @@ -1170,8 +1401,123 @@ editorForm.find('[name="type"]').change(function(e){ }) } }) - // - $.aM.channels = monitorStreamChannels - $.aM.maps = monitorSectionInputMaps - $.aM.e = monitorEditorWindow + monitorsList.change(function(){ + var monitorId = monitorsList.val() + openMonitorEditorPage(monitorId ? monitorId : null) + }) + $('body') + .on('tab-open-monitorSettings',function(){ + console.log('Opened Account Settings') + if(!monitorEditorSelectedMonitor){ + openMonitorEditorPage() + } + }) + .on('click','.reset-monitor-settings-form',function(){ + resetMonitorEditor() + }) + .on('click','.import-into-monitor-settings-window',function(){ + launchImportMonitorWindow(function(monitor){ + var monitorConfig = null + if(monitor.monitor){ + mergeZoneMinderZonesIntoMonitors(monitor) + monitorConfig = importZoneMinderMonitor(monitor.monitor.Monitor) + }else + //zoneminder multiple monitors + if(monitor.monitors){ + mergeZoneMinderZonesIntoMonitors(monitor) + monitorConfig = importZoneMinderMonitor(monitor.monitors[0].Monitor) + }else{ + if(monitor[0] && monitor.mid){ + monitorConfig = monitor[0]; + }else if(monitor.mid){ + monitorConfig = monitor; + } + } + if(monitorConfig){ + importIntoMonitorEditor(monitorConfig) + } + + }) + }) + .on('click','.delete-monitor-in-settings-window',function(){ + var validation = getMonitorEditFormFields() + var monitorConfig = validation.monitorConfig + if(loadedMonitors[monitorConfig.mid]){ + deleteMonitors([monitorConfig],function(){ + openMonitorEditorPage() + goBackOneTab() + }) + }else{ + resetMonitorEditor() + } + }) + .on('click','.export-from-monitor-settings-window',function(){ + var validation = getMonitorEditFormFields() + var monitorConfig = validation.monitorConfig + monitorConfig.details = safeJsonParse(monitorConfig.details) + downloadJSON(monitorConfig,`${monitorConfig.name}_${monitorConfig.mid}_${formattedTime(new Date(),true)}.json`) + }) + .on('click','.open-monitor-settings',function(){ + var monitorId + var thisEl = $(this) + var doNew = thisEl.attr('do-new') + var monitorId = thisEl.attr('data-mid') + if(doNew !== 'true' && !monitorId){ + var el = thisEl.parents('[data-mid]') + monitorId = el.attr('data-mid') + } + openMonitorEditorPage(doNew === 'true' ? null : monitorId) + }) + + mainSocket.on('f',function (d){ + // new PNotify({ + // title: lang['Settings Changed'], + // text: lang.SettingsChangedText, + // type: 'success' + // }) + switch(d.f){ + case'monitor_status': + loadedMonitors[d.id].code = parseInt(`${d.code}`) + loadedMonitors[d.id].status = `${d.status}` + $('[data-mid="'+d.id+'"] .monitor_status').html(monitorStatusCodes[d.code] || d.code || d.status); + break; + case'monitor_delete': + $('[data-mid="'+d.mid+'"]:not(#tab-monitorSettings)').remove(); + clearMonitorTimers(d.mid) + delete(loadedMonitors[d.mid]); + setMonitorCountOnUI() + break; + case'monitor_edit': + setMonitorCountOnUI() + onMonitorEdit(d) + break; + case'detector_plugged': + addDetectorPlugin(d.plug,d) + break; + case'detector_unplugged': + removeDetectorPlugin(d.plug) + break; + } + }) + function checkToOpenSideMenu(){ + if(isSideBarMenuCollapsed()){ + sideMenuCollapsePoint.collapse('show') + } + } + addOnTabAway('monitorSettings', function(){ + if(isSideBarMenuCollapsed()){ + sideMenuCollapsePoint.collapse('hide') + } + }) + addOnTabOpen('monitorSettings', function () { + drawMonitorListToSelector(monitorsList.find('optgroup'),false,'host') + checkToOpenSideMenu() + }) + addOnTabReopen('monitorSettings', function () { + var theSelected = `${monitorsList.val()}` + drawMonitorListToSelector(monitorsList.find('optgroup'),false,'host') + monitorsList.val(theSelected) + checkToOpenSideMenu() + }) + window.generateDefaultMonitorSettings = generateDefaultMonitorSettings }) diff --git a/web/libs/js/dash2.monitorStates.js b/web/assets/js/bs5.monitorStates.js similarity index 71% rename from web/libs/js/dash2.monitorStates.js rename to web/assets/js/bs5.monitorStates.js index d0d8aac0..2d90b2a1 100644 --- a/web/libs/js/dash2.monitorStates.js +++ b/web/assets/js/bs5.monitorStates.js @@ -1,25 +1,24 @@ $(document).ready(function(){ - $.monitorStates = { - e: $('#monitorStates'), - selector: $('#monitorStatesSelector'), - monitors: $('#monitorStatesMonitors'), - loaded: {} - } - $.monitorStates.f = $.monitorStates.e.find('form') - $.monitorStates.loadPresets = function(callback){ - $.get($.ccio.init('location',$user) + $user.auth_token + '/monitorStates/' + $user.ke,function(d){ + var monitorStatesLoaded = {} + var monitorStatesPage = $('#tab-monitorStates') + var monitorStatesSelector = $('#monitorStatesSelector') + var monitorStatesMonitors = $('#monitorStatesMonitors') + var theForm = monitorStatesPage.find('form') + function loadMonitorStatesPresets(callback){ + $.getJSON(getApiPrefix(`monitorStates`),function(d){ var html = '' $.each(d.presets,function(n,v){ - $.monitorStates.loaded[v.name] = v + monitorStatesLoaded[v.name] = v html += '' }) - $.monitorStates.selector.find('optgroup').html(html) + monitorStatesSelector.find('optgroup').html(html) if(callback)callback() }) } - $.monitorStates.e.on('shown.bs.modal', function (e) { - if($.monitorStates.selector.val() === '')$.monitorStates.loadPresets() + addOnTabReopen('monitorStates', function () { + if(monitorStatesSelector.val() === '')loadMonitorStatesPresets() }) + loadMonitorStatesPresets() var buildOptions = function(field,possiblities,fieldData){ if(!field)console.error('field',field) var fieldElement = '' @@ -139,26 +138,24 @@ $(document).ready(function(){ } return html } - $.monitorStates.drawMonitor = function(preloadedData){ - var MonitorSettings = $.ccio.definitions['Monitor Settings'] + var drawMonitor = function(preloadedData){ + var MonitorSettings = definitions['Monitor Settings'] var html = '' console.log(MonitorSettings) Object.keys(MonitorSettings.blocks).forEach(function(blockKey){ var block = MonitorSettings.blocks[blockKey] html += drawBlock(block,preloadedData) }) - var monitorSelect = `` + $.each(loadedMonitors,function(n,monitor){ monitorSelect += `` }) monitorSelect += `` var fullHtml = `
    -
    +
    ${monitorSelect} -
    - -    - +
    +
    @@ -167,12 +164,14 @@ $(document).ready(function(){
    ` return fullHtml } - $.monitorStates.e.on('click','.add-monitor',function(e){ + monitorStatesPage.on('click','.add-monitor',function(e){ + e.stopPropagation() var el = $(this) - var html = $.monitorStates.drawMonitor() - $.monitorStates.monitors.append(html) + var html = drawMonitor() + monitorStatesMonitors.append(html) + return false; }) - $.monitorStates.e.on('click','.state-monitor-row-fields-container .list-group-item',function(e){ + monitorStatesPage.on('click','.state-monitor-row-fields-container .list-group-item',function(e){ var el = $(this) var listGroupParent = el.parents('.list-group') var fieldContainer = el.find('.state-monitor-row-fields-field') @@ -186,11 +185,11 @@ $(document).ready(function(){ fieldContainer.show() } }) - $.monitorStates.e.on('click','.state-monitor-row-fields-container .form-control',function(e){ + monitorStatesPage.on('click','.state-monitor-row-fields-container .form-control',function(e){ e.preventDefault() return false }) - $.monitorStates.e.on('change','.json',function(e){ + monitorStatesPage.on('change','.json',function(e){ var el = $(this) var val = el.val() try{ @@ -199,25 +198,25 @@ $(document).ready(function(){ el.val(JSON.stringify(parsed,null,3)) }catch(err){ el.css('border-color','red') - return $.ccio.init('note',{title:lang['Invalid JSON'],text:lang.InvalidJSONText,type:'error'}) + return new PNotify({title:lang['Invalid JSON'],text:lang.InvalidJSONText,type:'error'}) } }) - $.monitorStates.e.on('click','.delete',function(e){ + monitorStatesPage.on('click','.delete',function(e){ $.confirm.e.modal('show'); $.confirm.title.text(lang['Delete Monitor States Preset']); $.confirm.body.html(lang.deleteMonitorStateText1); $.confirm.click({title:'Delete',class:'btn-danger'},function(){ - var form = $.monitorStates.f.serializeObject() - $.post($.ccio.init('location',$user) + $user.auth_token + '/monitorStates/' + $user.ke + '/' + form.name + '/delete',function(d){ - $.ccio.log(d) + var form = theForm.serializeObject() + $.post(getApiPrefix(`monitorStates`) + '/' + form.name + '/delete',function(d){ + debugLog(d) if(d.ok === true){ - $.monitorStates.loadPresets() - $.ccio.init('note',{title:lang.Success,text:d.msg,type:'success'}) + loadMonitorStatesPresets() + new PNotify({title:lang.Success,text:d.msg,type:'success'}) } }) }) }) - $.monitorStates.e.on('click','.delete-monitor',function(e){ + monitorStatesPage.on('click','.delete-monitor',function(e){ var el = $(this).parents('.state-monitor-row') $.confirm.e.modal('show'); $.confirm.title.text(lang['Delete Monitor State']); @@ -226,44 +225,27 @@ $(document).ready(function(){ el.remove() }) }) - $.monitorStates.selector.change(function(e){ + monitorStatesSelector.change(function(e){ var selected = $(this).val() - var loaded = $.monitorStates.loaded[selected] - var namespace = $.monitorStates.e.find('[name="name"]') - var deleteButton = $.monitorStates.e.find('.delete') + var loaded = monitorStatesLoaded[selected] + var namespace = monitorStatesPage.find('[name="name"]') + var deleteButton = monitorStatesPage.find('.delete') if(loaded){ namespace.val(loaded.name) var html = '' $.each(loaded.details.monitors,function(n,v){ - html += $.monitorStates.drawMonitor(v) + html += drawMonitor(v) }) - $.monitorStates.monitors.html(html) + monitorStatesMonitors.html(html) deleteButton.show() }else{ namespace.val('') - $.monitorStates.monitors.empty() + monitorStatesMonitors.empty() deleteButton.hide() } }) - $.monitorStates.getFormValuesFromJson = function(){ - var rows = $.monitorStates.monitors.find('.state-monitor-row') - var monitors = [] - rows.each(function(n,v){ - var el = $(v) - var textarea = el.find('textarea') - try{ - var json = JSON.parse(el.find('.json').val()) - textarea.css('border-color','green') - if(json.mid)monitors.push(json) - }catch(err){ - textarea.css('border-color','red') - $.ccio.init('note',{title:lang['Invalid JSON'],text:lang.InvalidJSONText,type:'error'}) - } - }) - return monitors - } - $.monitorStates.getFormValues = function(){ - var rows = $.monitorStates.monitors.find('.state-monitor-row') + function getFormValues(){ + var rows = monitorStatesMonitors.find('.state-monitor-row') var monitors = [] rows.each(function(n,v){ var monitorJson = { @@ -291,27 +273,27 @@ $(document).ready(function(){ }) return monitors } - $.monitorStates.f.submit(function(e){ + theForm.submit(function(e){ e.preventDefault() var el = $(this) var form = el.serializeObject() - var monitors = $.monitorStates.getFormValues() + var monitors = getFormValues() if(form.name === ''){ - return $.ccio.init('note',{title:lang['Invalid Data'],text:lang['Name cannot be empty.'],type:'error'}) + return new PNotify({title:lang['Invalid Data'],text:lang['Name cannot be empty.'],type:'error'}) } if(monitors.length === 0){ - return $.ccio.init('note',{title:lang['Invalid Data'],text:lang['Must be atleast one row'],type:'error'}) + return new PNotify({title:lang['Invalid Data'],text:lang['Must be atleast one row'],type:'error'}) } var data = { monitors: monitors } - $.post($.ccio.init('location',$user) + $user.auth_token + '/monitorStates/' + $user.ke + '/' + form.name + '/insert',{data:data},function(d){ - $.ccio.log(d) + $.post(getApiPrefix(`monitorStates`) + '/' + form.name + '/insert',{data:data},function(d){ + debugLog(d) if(d.ok === true){ - $.monitorStates.loadPresets(function(){ - $.monitorStates.selector.val(form.name) + loadMonitorStatesPresets(function(){ + monitorStatesSelector.val(form.name) }) - $.ccio.init('note',{title:lang.Success,text:d.msg,type:'success'}) + new PNotify({title:lang.Success,text:d.msg,type:'success'}) } }) return false; diff --git a/web/assets/js/bs5.monitorsList.js b/web/assets/js/bs5.monitorsList.js new file mode 100644 index 00000000..5f390517 --- /dev/null +++ b/web/assets/js/bs5.monitorsList.js @@ -0,0 +1,260 @@ +$(document).ready(function(){ + var selectedApiKey = `${$user.auth_token}` + var theBlock = $('#tab-monitorsList') + var theList = $('#monitorsListRows') + var apiKeySelector = $('#multi_mon_api_key_selector') + var multiMonitorSelect = $('#multimon_select_all') + function drawRowToList(row){ + var streamUrl = libURL + buildStreamUrl(row.mid).replace($user.auth_token,selectedApiKey) + theList.append(` +
    +
    +
    + ${buildMiniMonitorCardBody(loadedMonitors[row.mid],null,`
    +
    +
    +
    + ${row.name} +
    +
    + +
    +
    + ${row.host}
    + ${row.status || lang.Stopped} +
    +
    + +
    +
    + + +
    +
    +
    +
    `,true)} +
    +
    `) + } + function createMonitorVideosTab(monitor){ + var startDate = moment().subtract(32, 'hour').utc() + var endDate = moment().add(1, 'hour').utc() + var newTabId = `monitorVideos-${monitor.mid}` + var tabLabel = `${lang['Videos']} : ${monitor.name}` + var baseHtml = `
    +
    +
    +
    ${lang['Videos']} : ${monitor.name}
    +
    +
    +
    + +
    +
    +
    ` + createNewTab(newTabId,tabLabel,baseHtml,{},null,'videosList') + getVideos({ + monitorId: monitor.mid, + startDate: startDate._d, + endDate: endDate._d, + },function(data){ + var videos = data.videos + if(videos.length === 0){ + getVideos({ + monitorId: monitor.mid, + limit: 20, + },function(data){ + var videos = data.videos + drawVideoRowsToList(`#tab-monitorVideos-${monitor.mid} .video-list`,videos) + }) + }else{ + drawVideoRowsToList(`#tab-monitorVideos-${monitor.mid} .video-list`,videos) + } + }) + $(`#daterange-${newTabId}`).daterangepicker({ + timePicker: true, + startDate: startDate, + endDate: endDate, + locale: { + format: 'YYYY-MM-DD hh:mm:ss A' + } + }, function(start, end, label) { + var startDate = start.clone().utc() + var endDate = end.clone().utc() + getVideos({ + monitorId: monitor.mid, + startDate: startDate._d, + endDate: endDate._d, + },function(data){ + var videos = data.videos + drawVideoRowsToList(`#tab-monitorVideos-${monitor.mid} .video-list`,videos) + }) + }) + } + function loadMonitorsFromMemory(options,callback){ + theList.empty(); + $.each(loadedMonitors,function(n,row){ + drawRowToList(row) + }) + } + function getSelectedMonitors(){ + var monitorsSelected = []; + theList.find('.monitor-list-select').each(function(n,v){ + var el = $(v) + if(el.is(':checked')){ + var key = el.attr('name') + monitorsSelected.push(getDbColumnsForMonitor(loadedMonitors[key])) + } + }) + return monitorsSelected; + } + function toggleMonitorListSelectAll(isChecked){ + var nameField = theList.find('input[type=checkbox][name]') + if(isChecked === true){ + nameField.prop('checked',true) + }else{ + nameField.prop('checked',false) + } + } + function drawMonitorsListApiKeyList(){ + $.getJSON(getApiPrefix(`api`) + '/list',function(d){ + var html = '' + $.each(d.keys || [],function(n,key){ + console.log(key) + html += createOptionHtml({ + value: key.code, + label: key.code, + }) + }) + apiKeySelector.find('optgroup').html(html) + }) + } + function correctDropdownPosition(dropdownElement){ + var p = dropdownElement.offset(); + if (p.top < 0){ + dropdownElement[0].style = `transform:translate(0px, ${-p.top + 20}px)!important;` + } + } + var monitorListMenuDropdownOpen = null + var monitorListScrollTimeout = null + theBlock.on('mouseup','[data-bs-toggle="dropdown"]',function(){ + var dropdownElement = $(this).next() + monitorListMenuDropdownOpen = dropdownElement + setTimeout(function(){ + correctDropdownPosition(dropdownElement) + },500) + }) + $('body') + .on('click','.create-live-player',function(){ + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + createLivePlayerTab(loadedMonitors[monitorId]) + }) + .on('click','[set-mode]',function(){ + var thisEl = $(this) + var el = thisEl.parents('[data-mid]') + var monitorId = el.attr('data-mid') + var mode = thisEl.attr('set-mode') + $.getJSON(`${getApiPrefix('monitor')}/${monitorId}/${mode}`,function(data){ + console.log(data) + }) + }) + .on('click','.open-videos',function(){ + var monitorId = getRowsMonitorId(this) + var monitor = loadedMonitors[monitorId] + createMonitorVideosTab(monitor) + console.log(monitorId) + }) + .on('click','.export-this-monitor-settings',function(){ + var monitorId = getRowsMonitorId(this) + downloadMonitorConfigurationsToDisk([ + monitorId + ]) + }) + theBlock + .find('.export-selected-monitor-settings').click(function(){ + var monitorsSelected = getSelectedMonitors() + if(monitorsSelected.length === 0){ + new PNotify({ + title: lang['No Monitors Selected'], + type: 'error' + }); + return + } + downloadMonitorConfigurationsToDisk(monitorsSelected) + }) + theBlock + .find('.delete-selected-monitor-settings').click(function(){ + var monitorsSelected = getSelectedMonitors() + if(monitorsSelected.length === 0){ + new PNotify({ + title: lang['No Monitors Selected'], + text: lang['Select atleast one monitor to delete'], + type: 'error' + }); + return + } + deleteMonitors(monitorsSelected) + }) + theList + .on('click','.copy-stream-url',function(e){ + e.preventDefault() + var el = $(this) + var href = getFullOrigin() + el.attr('href') + copyToClipboard(href) + new PNotify({ + title: lang['Copied'], + text: lang['Copied to Clipboard'], + type: 'success' + }) + return false + }) + multiMonitorSelect.change(function(){ + var el = $(this); + var isChecked = el.prop('checked') + toggleMonitorListSelectAll(isChecked) + }) + apiKeySelector.change(function(){ + var value = $(this).val() + selectedApiKey = `${value}` + loadMonitorsFromMemory() + multiMonitorSelect.prop('checked',false) + }) + theBlock.find('.import-monitor-settings').click(function(){ + launchImportMonitorWindow() + }) + addOnTabOpen('monitorsList', function () { + loadMonitorsFromMemory() + drawMonitorsListApiKeyList() + }) + addOnTabReopen('monitorsList', function () { + loadMonitorsFromMemory() + drawMonitorsListApiKeyList() + }) + // $('#monitors_list_search').keyup(function(){ + // var monitorBlocks = $('.monitor_block'); + // var searchTerms = $(this).val().toLowerCase().split(' ') + // if(searchTerms.length === 0 || searchTerms[0] === ''){ + // monitorBlocks.show() + // return + // } + // monitorBlocks.hide() + // $.each($.ccio.mon,function(n,monitor){ + // var searchThis = JSON.stringify($.ccio.init('cleanMon',monitor)).toLowerCase().replace('"',''); + // $.each(searchTerms,function(m,term){ + // if(searchThis.indexOf(term) >-1 ){ + // $('.monitor_block[ke="'+monitor.ke+'"][mid="'+monitor.mid+'"]').show() + // } + // }) + // }) + // }) +}) diff --git a/web/assets/js/bs5.monitorsUtils.js b/web/assets/js/bs5.monitorsUtils.js new file mode 100644 index 00000000..7061b7b0 --- /dev/null +++ b/web/assets/js/bs5.monitorsUtils.js @@ -0,0 +1,1040 @@ +var availableMonitorGroups = {} +var monitorGroupSelections = $('#monitor-group-selections') +var onGetSnapshotByStreamExtensions = [] +function onGetSnapshotByStream(callback){ + onGetSnapshotByStreamExtensions.push(callback) +} +var onBuildStreamUrlExtensions = [] +function onBuildStreamUrl(callback){ + onBuildStreamUrlExtensions.push(callback) +} +function humanReadableModeLabel(mode){ + var humanMode = lang['Disabled'] + switch(mode){ + case'idle': + humanMode = lang['Idle'] + break; + case'stop': + humanMode = lang['Disabled'] + break; + case'record': + humanMode = lang['Record'] + break; + case'start': + humanMode = lang['Watch Only'] + break; + } + return humanMode +} +function setCosmeticMonitorInfo(monitorConfig){ + var monitorId = monitorConfig.mid + var monitorElements = $('.glM' + monitorId) + if(safeJsonParse(monitorConfig.details).vcodec !=='copy' && monitorConfig.mode == 'record'){ + monitorElements.find('.monitor_not_record_copy').show() + }else{ + monitorElements.find('.monitor_not_record_copy').hide() + } + var humanReadableMode = humanReadableModeLabel(monitorConfig.mode) + monitorElements.find('.monitor_name').text(monitorConfig.name) + monitorElements.find('.monitor_mid').text(monitorId) + monitorElements.find('.monitor_ext').text(monitorConfig.ext) + monitorElements.find('.monitor_mode').text(humanReadableMode) + monitorElements.find('.monitor_status').html(definitions['Monitor Status Codes'][monitorConfig.code] || monitorConfig.status || '') + monitorElements.attr('mode',humanReadableMode) + monitorElements.find('.lamp').attr('title',humanReadableMode) + if(monitorConfig.details.control=="1"){ + monitorElements.find('[monitor="control_toggle"]').show() + }else{ + monitorElements.find('.pad').remove() + monitorElements.find('[monitor="control_toggle"]').hide() + } +} + +function getSnapshot(options,cb){ + var image_data + var url + var monitor = options.mon || options.monitor || options + var targetElement = $(options.targetElement || `[data-mid="${monitor.mid}"].monitor_item .stream-element`) + var details = safeJsonParse(monitor.details) + var streamType = details.stream_type; + if(window.jpegModeOn !== true){ + function completeAction(image_data,width,height){ + var len = image_data.length + var arraybuffer = new Uint8Array( len ) + for (var i = 0; i < len; i++) { + arraybuffer[i] = image_data.charCodeAt(i) + } + try { + var blob = new Blob([arraybuffer], {type: 'application/octet-stream'}) + } catch (e) { + var bb = new (window.WebKitBlobBuilder || window.MozBlobBuilder) + bb.append(arraybuffer); + var blob = bb.getBlob('application/octet-stream'); + } + url = (window.URL || window.webkitURL).createObjectURL(blob) + cb(url,image_data,width,height) + try{ + setTimeout(function(){ + URL.revokeObjectURL(url) + },10000) + }catch(er){} + } + switch(streamType){ + case'hls': + case'flv': + case'mp4': + getVideoSnapshot(targetElement[0],function(base64,video_data,width,height){ + completeAction(video_data,width,height) + }) + break; + case'mjpeg': + $('#temp').html('') + var c = $('#temp canvas')[0] + var img = $('img',targetElement.contents())[0] + c.width = img.width + c.height = img.height + var ctx = c.getContext('2d') + ctx.drawImage(img, 0, 0,c.width,c.height) + completeAction(atob(c.toDataURL('image/jpeg').split(',')[1]),c.width,c.height) + break; + case'b64': + var c = targetElement[0] + var ctx = c.getContext('2d') + completeAction(atob(c.toDataURL('image/jpeg').split(',')[1]),c.width,c.height) + break; + case'jpeg': + url = targetElement.attr('src') + image_data = new Image() + image_data.src = url + cb(url,image_data,image_data.width,image_data.height) + break; + } + $.each(onGetSnapshotByStreamExtensions,function(n,extender){ + extender(streamType,targetElement,completeAction,cb) + }) + }else{ + url = targetElement.attr('src') + image_data = new Image() + image_data.src = url + cb(url,image_data,image_data.width,image_data.height) + } +} +function getVideoSnapshot(videoElement,cb){ + var image_data + var base64 + $('#temp').html('') + var c = $('#temp canvas')[0] + var img = videoElement + c.width = img.videoWidth + c.height = img.videoHeight + var ctx = c.getContext('2d') + ctx.drawImage(img, 0, 0,c.width,c.height) + base64=c.toDataURL('image/jpeg') + image_data=atob(base64.split(',')[1]) + var arraybuffer = new ArrayBuffer(image_data.length) + var view = new Uint8Array(arraybuffer) + for (var i=0; i-1){ + row[m]=b; + } + }) + return row +} + +function downloadMonitorConfigurationsToDisk(monitorIds){ + var selectedMonitors = [] + $.each(monitorIds,function(n,monitorId){ + var monitor = monitorId instanceof Object ? monitorId : loadedMonitors[monitorId] + if(monitor)selectedMonitors.push(monitor) + }) + var dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(selectedMonitors)); + $('#temp').html('') + .find('a') + .attr('href',dataStr) + .attr('download',`${applicationName}_Monitors_${$user.ke}_${new Date()}.json`) + [0].click() +} + +function importM3u8Playlist(textData){ + var m3u8List = textData.replace('#EXTM3U','').trim().split('\n') + var parsedList = {} + var currentName + m3u8List.forEach(function(line){ + if(line.indexOf('#EXTINF:-1,') > -1){ + currentName = line.replace('#EXTINF:-1,','').trim() + }else{ + parsedList[currentName] = line.trim() + } + }) + $.each(parsedList,function(name,url){ + var link = getUrlParts(url) + var newMon = generateDefaultMonitorSettings() + newMon.details = safeJsonParse(newMon.details) + newMon.mid = 'HLS' + name.toLowerCase() + newMon.name = name + newMon.port = link.port + newMon.host = link.hostname + newMon.path = link.pathname + newMon.details.tv_channel = '1' + newMon.details.tv_channel_id = name + newMon.details.auto_host_enable = '1' + newMon.details.auto_host = url + newMon.details.stream_quality = '1' + newMon.details.stream_fps = '' + newMon.details.stream_vcodec = 'libx264' + newMon.details.stream_acodec = 'aac' + newMon.details.stream_type = 'hls' + newMon.details.hls_time = '10' + newMon.type = 'mp4' + newMon.details = JSON.stringify(newMon.details) + postMonitor(newMon) + }) +} +function convertZoneMinderZonesToCords(rows,width,height){ + var coordinates = {} + const defaultDetectionWidth = 640 + const defaultDetectionHeight = 480 + rows.forEach((row) => { + const zone = row.Zone || row + let widthRatio = width > defaultDetectionWidth ? defaultDetectionWidth / width : 1 + let heightRatio = height > defaultDetectionHeight ? defaultDetectionHeight / height : 1 + const points = zone.Coords.split(' ').map((item) => { + let points = item.split(',').map((num) => parseInt(num)); + points[0] = parseInt(points[0] * widthRatio); + points[1] = parseInt(points[1] * heightRatio); + return points + }) + const monitorId = zone.MonitorId + coordinates[generateId(5)] = { + "name": `${zone.Id}-${monitorId}`, + "sensitivity": "5", + "max_sensitivity": "", + "threshold": 1, + "color_threshold": 9, + "points": points + } + }) + return coordinates +} +function mergeZoneMinderZonesIntoMonitors(data){ + const monitors = data.monitors + const singleMonitor = data.monitor && data.monitor.Monitor ? data.monitor.Monitor : null + const zones = data.zones + const targetMonitors = singleMonitor ? [singleMonitor] : monitors + targetMonitors.forEach((row) => { + const monitor = row.Monitor + const width = parseInt(monitor.Width) + const height = parseInt(monitor.Height) + const monitorZones = zones.filter(zone => { + return zone.Zone.MonitorId === monitor.Id + }) + monitor.Zones = convertZoneMinderZonesToCords(monitorZones,width,height) || {} + }) + if(singleMonitor){ + data.monitor.Monitor = targetMonitors[0] + } +} +function importZoneMinderMonitor(Monitor){ + var newMon = generateDefaultMonitorSettings() + newMon.details = safeJsonParse(newMon.details) + switch(Monitor.Type.toLowerCase()){ + case'ffmpeg':case'libvlc': + const url = getUrlParts(Monitor.Path) + const username = url.username || Monitor.User || Monitor.ONVIF_Username + const password = url.password || Monitor.Pass || Monitor.ONVIF_Password + const host = addCredentialsToUrl(url.origin,username,password) + const monitorIdSuffix = removeSpecialCharacters(Monitor.Name).toLowerCase().substring(0,15) + newMon.name = Monitor.Name + ` (ZM)` + newMon.mid = `zm${monitorIdSuffix}`; + newMon.host = host + newMon.protocol = `${url.protocol}//` + newMon.port = url.port || Monitor.Port + newMon.path = url.pathname + url.search + newMon.details.auto_host_enable = '1' + newMon.details.auto_host = Monitor.Path + newMon.details.muser = username + newMon.details.mpass = password + newMon.details.stream_type = 'hls' + newMon.details.detector_buffer_acodec = 'auto' + newMon.type = 'h264' + if(Monitor.Zones){ + newMon.details.cords = JSON.stringify(Monitor.Zones) + } + switch(Monitor.Function){ + case'None': + // The monitor is currently disabled. + newMon.mode = 'stop' + break; + case'Monitor': + // The monitor is only available for live streaming. + // No image analysis is done so no alarms or events will be generated + // nothing will be recorded. + newMon.mode = 'start' + newMon.details.detector = '0' + break; + case'Modect': + // (Monitor) or MOtion DEteCTtion. + // All captured images will be analysed + // events generated with recorded video where motion is detected. + newMon.mode = 'start' + newMon.details.detector = '1' + newMon.details.detector_http_api = '1' + newMon.details.detector_send_frames = '1' + break; + case'Record': + // The monitor will be continuously recorded. + // No motion detection takes place in this mode. + newMon.mode = 'record' + newMon.details.detector = '0' + break; + case'Mocord': + // The monitor will be continuously recorded + // motion being highlighted within those events. + newMon.mode = 'record' + newMon.details.detector = '1' + newMon.details.detector_http_api = '1' + newMon.details.detector_send_frames = '1' + break; + case'Nodect': + // (Mocord) or No DEteCTtion. + // This is a special mode designed to be used with external triggers. + // In Nodect no motion detection takes place but events are recorded if external triggers require it. + newMon.mode = 'start' + newMon.details.detector = '1' + newMon.details.detector_send_frames = '0' + newMon.details.detector_http_api = '1' + break; + } + if( + url.protocol === 'rtsp:' || + url.protocol === 'rtmp:' || + url.protocol === 'rtmps:' + ){ + newMon.type = 'h264' + }else{ + new PNotify({ + title: lang['Please Check Your Settings'], + text: lang.migrateText1,type:'error' + }) + } + break; + default: + new PNotify({ + title: lang['Please Check Your Settings'], + text: lang.migrateText1,type:'error' + }) + break; + } + newMon.details = JSON.stringify(newMon.details) + return newMon +} + +function importMonitor(textData){ + try{ + var parsedData = textData instanceof Object ? textData : safeJsonParse(mergeConcattedJsonString(textData)) + function postMonitor(v){ + var monitorId = v.mid + $.post(`${getApiPrefix('configureMonitor')}/${monitorId}`,{ + data: JSON.stringify(v,null,3) + },function(d){ + debugLog(d) + }) + } + //zoneminder one monitor + if(parsedData.monitor){ + mergeZoneMinderZonesIntoMonitors(parsedData) + postMonitor(importZoneMinderMonitor(parsedData.monitor.Monitor)) + }else + //zoneminder multiple monitors + if(parsedData.monitors){ + mergeZoneMinderZonesIntoMonitors(parsedData) + $.each(parsedData.monitors,function(n,v){ + postMonitor(importZoneMinderMonitor(v.Monitor)) + }) + }else + //shinobi one monitor + if(parsedData.mid){ + postMonitor(parsedData) + }else + //shinobi multiple monitors + if(parsedData[0] && parsedData[0].mid){ + $.each(parsedData,function(n,v){ + postMonitor(v) + }) + } + }catch(err){ + //#EXTM3U + if(textData instanceof String && textData.indexOf('#EXTM3U') > -1 && textData.indexOf('{"') === -1){ + importM3u8Playlist(textData) + }else{ + debugLog(err) + new PNotify({ + title: lang['Invalid JSON'], + text: lang.InvalidJSONText, + type: 'error' + }) + } + } +} +function deleteMonitors(monitorsSelected,afterDelete){ + $.confirm.create({ + title: lang['Delete']+' '+lang['Monitors'], + body: '

    '+lang.DeleteMonitorsText+'

    ', + clickOptions: [ + { + title:lang['Delete']+' '+lang['Monitors'], + class:'btn-danger', + callback:function(){ + $.each(monitorsSelected,function(n,monitor){ + $.getJSON(`${getApiPrefix(`configureMonitor`)}/${monitor.mid}/delete`,function(data){ + console.log(data) + if(monitorsSelected.length === n + 1){ + //last + if(afterDelete)afterDelete(monitorsSelected) + } + }) + }) + } + }, + { + title:lang['Delete Monitors and Files'], + class:'btn-danger', + callback:function(){ + $.each(monitorsSelected,function(n,monitor){ + $.getJSON(`${getApiPrefix(`configureMonitor`)}/${monitor.mid}/delete?deleteFiles=true`,function(data){ + console.log(data) + if(monitorsSelected.length === n + 1){ + //last + if(afterDelete)afterDelete(monitorsSelected) + } + }) + }) + } + } + ] + }) +} +function launchImportMonitorWindow(callback){ + var html = `${lang.ImportMultiMonitorConfigurationText} +
    +
    + +
    + +
    ` + $.confirm.create({ + title: lang['Import Monitor Configuration'], + body: html, + clickOptions: [ + { + title: lang['Import'], + class: 'btn-primary', + callback: function(){ + var textData = safeJsonParse(mergeConcattedJsonString($.confirm.e.find('textarea').val())) + if(callback){ + callback(textData) + }else{ + importMonitor(textData) + } + } + }, + // { + // title: lang['Upload'], + // class: 'btn-primary', + // callback: function(e){ + // var files = e.target.files; // FileList object + // f = files[0]; + // var reader = new FileReader(); + // reader.onload = function(ee) { + // $.confirm.e.find('textarea').val(ee.target.result); + // } + // reader.readAsText(f); + // } + // } + ], + }) + $.confirm.e.find('.upload').change(function(e){ + var files = e.target.files; // FileList object + f = files[0]; + var reader = new FileReader(); + reader.onload = function(ee) { + $.confirm.e.find('textarea').val(ee.target.result); + } + reader.readAsText(f); + }); +} +function drawMatrices(event,options){ + var theContainer = options.theContainer + var height = options.height + var width = options.width + var widthRatio = width / event.details.imgWidth + var heightRatio = height / event.details.imgHeight + var objectTagGroup = event.details.reason === 'motion' ? 'motion' : event.details.name + theContainer.find(`.stream-detected-object[name="${objectTagGroup}"]`).remove() + var html = '' + $.each(event.details.matrices,function(n,matrix){ + html += `
    ` + if(matrix.tag)html += `${matrix.tag}` + html += '
    ' + }) + theContainer.append(html) +} +function setMonitorCountOnUI(){ + $('.cameraCount').text(Object.keys(loadedMonitors).length) +} +function muteMonitorAudio(monitorId,buttonEl){ + var masterMute = dashboardOptions().switches.monitorMuteAudio + var monitorMutes = dashboardOptions().monitorMutes || {} + monitorMutes[monitorId] = monitorMutes[monitorId] === 1 ? 0 : 1 + dashboardOptions('monitorMutes',monitorMutes) + var vidEl = $('.monitor_item[data-mid="' + monitorId + '"] video')[0] + try{ + if(monitorMutes[monitorId] === 1){ + vidEl.muted = true + }else{ + if(masterMute !== 1){ + if(windowFocus && hadFocus){ + vidEl.muted = false + }else{ + console.error('User must have window active to unmute.') + } + } + } + }catch(err){ + console.log(err) + } + var volumeIcon = monitorMutes[monitorId] !== 1 ? 'volume-up' : 'volume-off' + if(buttonEl)buttonEl.find('i').removeClass('fa-volume-up fa-volume-off').addClass('fa-' + volumeIcon) +} +function getAvailableMonitorGroups(){ + var theGroups = {} + $.each(loadedMonitors,function(n,monitor){ + var monitorsGroups = safeJsonParse(monitor.details.groups) + $.each(monitorsGroups,function(m,groupId){ + if(!theGroups[groupId])theGroups[groupId]={} + theGroups[groupId][monitor.mid] = monitor + }) + }) + availableMonitorGroups = theGroups + return theGroups; +} +function drawMonitorGroupList(){ + var html = '' + getAvailableMonitorGroups() + $.each(availableMonitorGroups,function(groupId,v){ + if($user.mon_groups[groupId]){ + html += `
  • ${$user.mon_groups[groupId].name}
  • ` + } + }) + monitorGroupSelections.html(html) +} +function loadMonitorGroup(groupId){ + $.each(dashboardOptions().watch_on,function(groupKey,groupData){ + $.each(groupData,function(monitorId,isOn){ + if(isOn)mainSocket.f({ + f: 'monitor', + ff: 'watch_off', + id: monitorId, + ke: $user.ke + }) + }) + }) + $.each(availableMonitorGroups[groupId],function(n,monitor){ + mainSocket.f({ + f: 'monitor', + ff: 'watch_on', + id: monitor.mid, + ke: $user.ke + }) + }) +} +function buildDefaultMonitorMenuItems(){ + return ` +
  • ${lang['Live Grid']}
  • +
  • ${lang['Pop']}
  • +
  • ${lang['Toggle Substream']}
  • +
  • +
  • ${lang['Videos List']}
  • + +
  • ${lang['Time-lapse']}
  • +
  • +
  • ${lang['Monitor Settings']}
  • +
  • ${lang['Export']}
  • +
  • +
  • ${lang['Set Mode']}
  • +
  • ${lang.Disable}
  • +
  • ${lang['Watch-Only']}
  • +
  • ${lang.Record}
  • ` +} +function magnifyStream(options){ + if(!options.p && !options.parent){ + var el = $(this), + parent = el.parents('[mid]') + }else{ + parent = options.p || options.parent + } + if(!options.attribute){ + options.attribute = '' + } + if(options.animate === true){ + var zoomGlassAnimate = 'animate' + }else{ + var zoomGlassAnimate = 'css' + } + if(!options.magnifyOffsetElement){ + options.magnifyOffsetElement = '.stream-block' + } + if(!options.targetForZoom){ + options.targetForZoom = '.stream-element' + } + if(options.auto === true){ + var streamBlockOperator = 'position' + }else{ + var streamBlockOperator = 'offset' + } + var magnifiedElement + if(!options.videoUrl){ + if(options.useCanvas === true){ + magnifiedElement = 'canvas' + }else{ + magnifiedElement = 'iframe' + } + }else{ + magnifiedElement = 'video' + } + if(!options.mon && !options.monitor){ + var groupKey = parent.attr('ke')//group key + var monitorId = parent.attr('mid')//monitor id + var sessionKey = parent.attr('auth')//authkey + var monitor = $.ccio.mon[groupKey + monitorId + sessionKey]//monitor configuration + }else{ + var monitor = options.mon || options.monitor + var groupKey = monitor.ke//group key + var monitorId = monitor.mid//monitor id + var sessionKey = monitor.auth//authkey + } + if(options.zoomAmount)zoomAmount = 3 + if(!zoomAmount)zoomAmount = 3 + var realHeight = parent.attr('realHeight') + var realWidth = parent.attr('realWidth') + var height = parseFloat(realHeight) * zoomAmount//height of stream + var width = parseFloat(realWidth) * zoomAmount//width of stream + var targetForZoom = parent.find(options.targetForZoom) + zoomGlass = parent.find(".zoomGlass") + var zoomFrame = function(){ + var magnify_offset = parent.find(options.magnifyOffsetElement)[streamBlockOperator]() + var mx = options.pageX - magnify_offset.left + var my = options.pageY - magnify_offset.top + var rx = Math.round(mx/targetForZoom.width()*width - zoomGlass.width()/2)*-1 + var ry = Math.round(my/targetForZoom.height()*height - zoomGlass.height()/2)*-1 + var px = mx - zoomGlass.width()/2 + var py = my - zoomGlass.height()/2 + zoomGlass[zoomGlassAnimate]({left: px, top: py}).find(magnifiedElement)[zoomGlassAnimate]({left: rx, top: ry}) + } + var commit = function(height,width){ + zoomGlass.find(magnifiedElement).css({ + height: height, + width: width + }) + zoomFrame() + } + if(!height || !width || zoomGlass.length === 0){ + zoomGlass = parent.find(".zoomGlass") + var zoomGlassShell = function(contents){return `
    ${contents}
    `} + if(!options.videoUrl){ + $.ccio.snapshot(monitor,function(url,buffer,w,h){ + parent.attr('realWidth',w) + parent.attr('realHeight',h) + if(zoomGlass.length === 0){ + if(options.useCanvas === true){ + parent.append(zoomGlassShell('')) + }else{ + parent.append(zoomGlassShell('
    ` + } + }) + streamCarouselBlockInner.html(html) + } + function clearCarouselFrames(){ + $.each(loadedCarouselBlocks,function(n,monitorId){ + deloadMonitorInCarousel(monitorId) + }) + } + function loadMonitorInCarousel(monitorId){ + currentCarouselMonitorId = `${monitorId}` + var loadedMonitor = loadedMonitors[monitorId] + var monitorName = loadedMonitor.name + var monitorStatus = loadedMonitor.status + streamCarouselBlockLabel.html(`${monitorName} : ${monitorStatus}`) + streamCarouselBlockInner + .find(`.carousel-block[data-mid="${monitorId}"]`) + .addClass('active-block') + .find('iframe') + .attr('src',`${getApiPrefix(`embed`)}/${monitorId}/fullscreen|jquery|relative`) + } + function deloadMonitorInCarousel(monitorId){ + streamCarouselBlockInner + .find(`.carousel-block[data-mid="${monitorId}"]`) + .removeClass('active-block') + .find('iframe') + .attr('src',`about:blank`) + } + function goNextCarouselBlock(amountToMove,lateDeload){ + var numberOfBlocks = loadedCarouselBlocks.length - 1; + var oldId = `${currentCarouselMonitorId}` + var indexToMoveTo = loadedCarouselBlocks.indexOf(currentCarouselMonitorId) + amountToMove + var nextId = loadedCarouselBlocks[indexToMoveTo] + if(indexToMoveTo > numberOfBlocks){ + console.log('above',loadedCarouselBlocks[0],indexToMoveTo,numberOfBlocks) + nextId = loadedCarouselBlocks[0] + console.log('above',nextId) + }else if(indexToMoveTo < 0){ + console.log('below',loadedCarouselBlocks[numberOfBlocks],indexToMoveTo,numberOfBlocks) + nextId = loadedCarouselBlocks[numberOfBlocks] + console.log('below',nextId) + } + if(oldId === nextId){ + clearInterval(changeTimer) + }else{ + loadMonitorInCarousel(nextId) + if(lateDeload){ + setTimeout(function(){ + deloadMonitorInCarousel(oldId) + },2000) + } + } + } + function setAutoChangerInterval(){ + stopAutoChangerInterval() + changeTimer = setInterval(function(){ + goNextCarouselBlock(1,true) + },20000) + } + function stopAutoChangerInterval(){ + clearTimeout(changeTimer) + } + function initCarousel(){ + drawCarouselSet() + if(loadedCarouselBlocks[0]){ + loadMonitorInCarousel(currentCarouselMonitorId || loadedCarouselBlocks[0]) + setAutoChangerInterval() + } + } + addOnTabReopen('initial', function () { + initCarousel() + }) + addOnTabAway('initial', function () { + clearCarouselFrames() + stopAutoChangerInterval() + }) + onDashboardReady(function(){ + initCarousel() + }) + onWebSocketEvent(function(d){ + switch(d.f){ + case'detector_trigger': + var monitorId = d.id + if(tabTree.name === 'initial' && currentCarouselMonitorId !== monitorId){ + loadMonitorInCarousel(monitorId) + setAutoChangerInterval() + } + break; + } + }) + streamCarouselBlock.find('[stream-carousel-go]').click(function(){ + var el = $(this) + var direction = parseInt(el.attr('stream-carousel-go')) + console.log(direction) + deloadMonitorInCarousel(currentCarouselMonitorId) + goNextCarouselBlock(direction) + setAutoChangerInterval() + }) + $(window).focus(function(){ + if(canBackgroundCarousel()){ + initCarousel() + } + }).blur(function(){ + if(canBackgroundCarousel()){ + clearCarouselFrames() + stopAutoChangerInterval() + } + }) +}) diff --git a/web/libs/js/dash2.subAccountManager.js b/web/assets/js/bs5.subAccountManager.js similarity index 74% rename from web/libs/js/dash2.subAccountManager.js rename to web/assets/js/bs5.subAccountManager.js index efd855df..639801c7 100644 --- a/web/libs/js/dash2.subAccountManager.js +++ b/web/assets/js/bs5.subAccountManager.js @@ -1,23 +1,25 @@ $(document).ready(function(){ - var apiPrefix = getAdminApiPrefix() + var apiPrefix = getFullOrigin(true) + getAdminApiPrefix() var theWindow = $('#subAccountManager'); var accountTable = $('#subAccountsList tbody'); var theWindowForm = $('#monSectionAccountInformation'); var permissionsSection = $('#monSectionAccountPrivileges'); var permissionsMonitorSection = $('#sub_accounts_permissions'); + var currentlyActiveUsersList = $('#currently-active-users'); var submitButtons = theWindow.find('.submit-form') var loadedSubAccounts = {} var clearTable = function(){ accountTable.empty() loadedSubAccounts = {} } - var getSubAccounts = function(){ + var getSubAccounts = function(callback){ $.get(`${apiPrefix}accounts/${$user.ke}`,function(data){ clearTable() $.each(data.accounts,function(n,account){ loadedSubAccounts[account.uid] = account; drawSubAccountRow(account) }) + callback() }) } var deleteSubAccount = function(email,uid){ @@ -38,7 +40,7 @@ $(document).ready(function(){ var notifyColor = 'info' if(data.ok){ loadedSubAccounts[uid] = null; - accountTable.find('tr[uid="' + uid + '"]').remove() + accountTable.find('[uid="' + uid + '"]').remove() }else{ notifyTitle = lang.accountActionFailed notifyText = lang.contactAdmin @@ -96,19 +98,20 @@ $(document).ready(function(){ mail: form.mail, data: form },function(data){ + console.log(data) if(data.ok){ $.each(form,function(n,v){ account[n] = v }); - accountTable.find(`[uid="${account.uid}"] .mail`).text(form.mail) + accountTable.find(`[uid="${uid}"] .mail`).text(form.mail) new PNotify({ - title : 'Account Edited', + title : lang['Account Edited'], text : '' + account.mail + ' has been updated.', type : 'success' }) }else{ new PNotify({ - title : 'Failed to Add Account', + title : lang['Failed to Edit Account'], text : data.msg, type : 'error' }) @@ -117,20 +120,16 @@ $(document).ready(function(){ }) } var drawSubAccountRow = function(account){ - var html = ` - - ${account.mail} - - - ${account.uid} - - - - - - - - `; + var html = `
    +
    + ${account.mail}
    + ${account.uid}
    +
    + +
    `; accountTable.prepend(html) } var permissionTypeNames = [ @@ -153,7 +152,7 @@ $(document).ready(function(){ ]; var drawSelectableForPermissionForm = function(){ var html = '' - $.each($.ccio.mon,function(n,monitor){ + $.each(loadedMonitors,function(n,monitor){ html += `
    ` html += `
    ` html += `
    ' + moment(fileInfo.time).format('YYYY-MM-DD HH:mm:ss') + '
    ' + currentPlaylist[fileInfo.filename] = fileInfo + }) + currentPlaylistArray = data + frameIcons.html(frameIconsHtml) + frameIcons.find(`.frame:first`).click() + // getLiveStream() + resetFilmStripPositions() + loadVisibleTimelapseFrames() + }else{ + frameIconsHtml = `
    ${lang['No Data']}
    ` + frameIcons.html(frameIconsHtml) + } + }) + } + var resetFilmStripPositions = function(){ + var numberOfFrames = Object.keys(currentPlaylist).length + var fieldHolderHeight = fieldHolder.height() + fieldHolderCssHeightModifier + frameIcons.css({height:"calc(100% - 15px - " + fieldHolderHeight + "px)"}) + } + var setPlayBackFrame = function(href,callback){ + playBackViewImage + .off('load').on('load',function(){ + playBackViewImage.off('error') + if(callback)callback() + }) + .off('error').on('error',function(){ + if(callback)callback() + }) + playBackViewImage[0].src = href + } + var startPlayLoop = function(){ + var selectedFrame = currentPlaylist[frameSelected] + var selectedFrameNumber = currentPlaylist[frameSelected].number + setPlayBackFrame(selectedFrame.href,function(){ + frameIcons.find(`.frame.selected`).removeClass('selected') + frameIcons.find(`.frame[data-filename="${selectedFrame.filename}"]`).addClass('selected') + clearTimeout(playIntervalTimer) + playIntervalTimer = setTimeout(function(){ + if(!canPlay)return + ++selectedFrameNumber + var newSelectedFrame = currentPlaylistArray[selectedFrameNumber] + if(!newSelectedFrame)return + frameSelected = newSelectedFrame.filename + startPlayLoop() + },1000/parseInt(fpsSelector.val(),10)) + }) + } + var playTimelapse = function(){ + var playPauseText = timelapseWindow.find('.playPauseText') + canPlay = true + playPauseText.html(` ${lang.Pause}`) + startPlayLoop() + } + var destroyTimelapse = function(){ + playBackViewImage.off('load') + frameSelected = null + pauseTimelapse() + frameIcons.empty() + setPlayBackFrame(null) + allowKeepChecking = false + } + var pauseTimelapse = function(){ + var playPauseText = timelapseWindow.find('.playPauseText') + canPlay = false + playPauseText.html(` ${lang.Play}`) + clearTimeout(playIntervalTimer) + playIntervalTimer = null + } + var togglePlayPause = function(){ + if(canPlay){ + pauseTimelapse() + }else{ + playTimelapse() + } + } + var iconHtml = function(iconClasses,withSpace){ + if(withSpace === undefined)withSpace = true + return `` + (withSpace ? ' ' : '') + } + var setDownloadButtonLabel = function(text,icon){ + downloadButton.html(icon ? iconHtml(icon) + text : text) + } + function deleteFrame(frame){ + return new Promise((resolve,reject) => { + $.getJSON((frame.href || frame) + '/delete',function(response){ + resolve(response) + }) + }) + } + async function deleteFrames(frameHrefs){ + for (let i = 0; i < frameHrefs.length; i++) { + const frameHref = frameHrefs[i] + await deleteFrame(frameHref) + } + } + function deleteSelectedFrames(){ + var checkedBoxes = frameIcons.serializeObject() + var frameHrefs = Object.keys(checkedBoxes) + var fileNames = Object.values(checkedBoxes) + $.confirm.create({ + title: lang['Delete selected'], + body: lang.DeleteTheseMsg + `

    `, + clickOptions: { + class: 'btn-danger', + title: lang.Delete, + }, + clickCallback: function(){ + deleteFrames(frameHrefs) + fileNames.forEach((filename) => { + frameIcons.find(`[frame-container="${filename}"]`).remove() + }) + } + }) + } + function toggleSelectOnAllFrames(){ + var isMasterToggleSelected = selectAllBox.is(':checked') + var checkBoxes = frameIcons.find('input[type="checkbox"]') + if(isMasterToggleSelected){ + checkBoxes.prop('checked',true) + }else{ + checkBoxes.prop('checked',false) + } + } + timelapseWindow.on('click','.frame',function(){ + pauseTimelapse() + var selectedFrame = $(this).attr('data-filename') + if(selectedFrame === frameSelected){ + return togglePlayPause() + } + frameSelected = selectedFrame + frameIcons.find(`.frame.selected`).removeClass('selected') + frameIcons.find(`.frame[data-filename="${selectedFrame}"]`).addClass('selected') + var href = currentPlaylist[selectedFrame].href + setPlayBackFrame(href) + }) + timelapseWindow.on('click','.playPause',function(){ + togglePlayPause() + }) + timelapseWindow.on('click','.frame .delete',function(e){ + e.stopPropagation() + var el = $(this).parents('.frame') + var filename = el.attr('data-filename') + var frame = currentPlaylist[filename] + $.confirm.create({ + title: lang['Delete Timelapse Frame'], + body: lang.DeleteThisMsg + `

    `, + clickOptions: { + class: 'btn-danger', + title: lang.Delete, + }, + clickCallback: async function(){ + const response = await deleteFrame(frame) + if(response.ok){ + el.parent().remove() + } + } + }) + }) + timelapseWindow.on('click','.delete-selected',function(e){ + deleteSelectedFrames() + }) + selectAllBox.click(function(e){ + toggleSelectOnAllFrames() + }) + timelapseWindow.on('click','.frame input',function(e){ + e.stopPropagation() + const checked = $(this).is(':checked') + if(!checked){ + selectAllBox.prop('checked',false) + } + }) + downloadButton.click(function(){ + var fps = fpsSelector.val() + var dateRange = getSelectedTime(false) + var startDate = dateRange.startDate + var endDate = dateRange.endDate + var selectedMonitor = monitorsList.val() + window.askedForTimelapseVideoBuild = true + var parsedFrames = currentPlaylistArray.map(function(frame){ + return { + mid: frame.mid, + ke: frame.ke, + filename: frame.filename, + } + }); + mainSocket.f({ + f: 'timelapseVideoBuild', + mid: selectedMonitor, + frames: parsedFrames, + fps: fps, + }) + }) + function isElementVisible (el) { + const holder = frameIcons[0] + const { top, bottom, height } = el.getBoundingClientRect() + const holderRect = holder.getBoundingClientRect() + + return top <= holderRect.top + ? holderRect.top - top <= height + : bottom - holderRect.bottom <= height + } + function loadVisibleTimelapseFrames(){ + frameIcons.find('[frame-container-unloaded]').each(function(n,v){ + if(isElementVisible(v)){ + var el = $(v) + var imgSrc = el.attr('frame-container-unloaded') + el.removeAttr('frame-container-unloaded').attr('style',`background-image:url(${imgSrc})`) + } + }) + } + function buildFileBinUrl(data){ + return apiBaseUrl + '/fileBin/' + data.ke + '/' + data.mid + '/' + data.name + } + function downloadTimelapseVideo(data){ + var downloadUrl = buildFileBinUrl(data) + downloadFile(downloadUrl,data.name) + } + function onTimelapseVideoBuildComplete(data){ + var saveBuiltVideo = dashboardOptions().switches.timelapseSaveBuiltVideo + if(saveBuiltVideo === 1){ + new PNotify({ + title: lang['Timelapse Video Build Complete'], + text: lang.yourFileDownloadedShortly, + type: 'success', + }) + }else if(tabTree.name !== 'timelapseViewer'){ + new PNotify({ + title: lang['Timelapse Video Build Complete'], + text: `${lang['File Download Ready']}.

    ${lang.Download}`, + type: 'success', + }) + } + } + function drawTimelapseVideoProgressBar(data){ + var fileBinUrl = buildFileBinUrl(data) + var html = `
  • +
    + +
    + ${lang.Building}... +
    +
    +
    +
    ${data.percent}%
    +
    +
    + + + +
    +
  • ` + sideLinkListBox.append(html) + } + onWebSocketEvent(function(data){ + switch(data.f){ + case'timelapse_build_requested': + console.log(data) + var response = data.buildResponse; + setDownloadButtonLabel(response.msg, '') + new PNotify({ + title: lang['Timelapse Frames Video'], + text: response.msg, + type: response.fileExists ? 'success' : 'info' + }); + if(response.fileExists && window.askedForTimelapseVideoBuild)downloadTimelapseVideo(response); + window.askedForTimelapseVideoBuild = false + break; + case'fileBin_item_added': + var saveBuiltVideo = dashboardOptions().switches.timelapseSaveBuiltVideo + let statusText = `${lang['Done!']}` + onTimelapseVideoBuildComplete(data) + if(data.timelapseVideo && saveBuiltVideo === 1){ + downloadTimelapseVideo(data) + statusText = lang['Downloaded!'] + } + setDownloadButtonLabel(statusText, '') + var progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`) + progressItem.find('.row-status').text(statusText) + progressItem.find('.dot').removeClass('dot-orange').addClass('dot-green') + progressItem.find('.download-button').show() + break; + case'timelapse_build_percent': + var progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`) + if(progressItem.length === 0){ + drawTimelapseVideoProgressBar(data) + }else{ + progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`) + progressItem.find('.progress-bar').css('width',`${data.percent}%`).text(`${data.percent}%`) + progressItem.find('.percent').text(data.percent) + } + console.log(data) + break; + } + }) + frameIcons.on('scroll',loadVisibleTimelapseFrames) + $('body') + .on('click','.open-timelapse-viewer',function(){ + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + openTab(`timelapseViewer`,{},null) + monitorsList.val(monitorId).change() + }); + sideLinkListBox + .on('click','.remove-row',function(){ + var el = $(this).parents('[data-mid]') + el.remove() + }) + setDownloadButtonLabel(lang['Build Video'], 'database') + addOnTabOpen('timelapseViewer', function () { + if(!monitorsList.val()){ + drawMonitorListToSelector(monitorsList) + drawTimelapseWindowElements() + } + }) + addOnTabReopen('timelapseViewer', function () { + var theSelected = `${monitorsList.val()}` + drawMonitorListToSelector(monitorsList) + monitorsList.val(theSelected) + resetFilmStripPositions() + }) + addOnTabAway('timelapseViewer',function(){ + pauseTimelapse() + }) +}) diff --git a/web/assets/js/bs5.videoPlayer.js b/web/assets/js/bs5.videoPlayer.js new file mode 100644 index 00000000..19aae7e6 --- /dev/null +++ b/web/assets/js/bs5.videoPlayer.js @@ -0,0 +1,159 @@ +$(document).ready(function(){ + var theBlock = $('#tab-videoPlayer') + window.getVideoPlayerTabId = function(video){ + return `videoPlayer-${video.mid}-${moment(video.time).format('YYYY-MM-DD-HH-mm-ss')}` + } + window.createVideoPlayerTab = function(video){ + var newTabId = getVideoPlayerTabId(video) + var humanStartTime = formattedTime(video.time,true) + var humanEndTime = formattedTime(video.end,true) + var tabLabel = `${lang['Video']} : ${loadedMonitors[video.mid].name} : ${formattedTime(video.time,true)}` + var videoUrl = getLocation() + video.href + var hasRows = video.events && video.events.length > 0 + var loadedEvents = {} + var eventMatrixHtml = `` + if(hasRows){ + var objectsFound = {} + $.each(video.events,function(n,theEvent){ + loadedEvents[new Date(theEvent.time)] = theEvent + var objectsFound = {} + eventMatrixHtml += `
    ` + eventMatrixHtml += `
    +
    ${formattedTime(theEvent.time)}
    +
    ` + $.each(theEvent.details.matrices,function(n,matrix){ + if(!objectsFound[matrix.tag])objectsFound[matrix.tag] = 1 + ++objectsFound[matrix.tag] + }) + $.each(objectsFound,function(tag,count){ + eventMatrixHtml += `
    +
    ${tag} : ${count}
    +
    ` + }) + eventMatrixHtml += `
    ` + }) + eventMatrixHtml += `
    ` + } + var baseHtml = `
    +
    +
    +
    ${tabLabel}
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + ${lang.Started} +
    ${humanStartTime}
    +
    +
    + ${lang.Ended} +
    ${humanEndTime}
    +
    +
    +
    +
    + ${lang.Download} + ${permissionCheck('video_delete',video.mid) ? ` ${lang.Delete}` : ''} + ${permissionCheck('video_delete',video.mid) ? ` ${video.archive === 1 ? lang.Unarchive : lang.Archive}` : ''} +
    +
    +
    +
    + ${eventMatrixHtml} +
    +
    +
    +
    +
    ` + var tabCreateResponse = createNewTab(newTabId,tabLabel,baseHtml,{},null,'videoPlayer') + console.log(tabCreateResponse) + if(!tabCreateResponse.existAlready){ + var videoElement = tabCreateResponse.theTab.find('.tab-videoPlayer-video-element')[0] + var videoObjectContainer = tabCreateResponse.theTab.find('.tab-videoPlayer-event-objects') + var videoHeight = videoObjectContainer.height() + var videoWidth = videoObjectContainer.width() + videoElement.ontimeupdate = function(time){ + var eventTime = new Date(((new Date(video.time).getTime() / 1000) + videoElement.currentTime) * 1000) + var theEvent = loadedEvents[eventTime] + if(theEvent){ + drawMatrices(theEvent,{ + theContainer: videoObjectContainer, + height: videoHeight, + width: videoWidth, + }) + }else{ + videoObjectContainer.find('.stream-detected-object').remove() + } + } + } + } + window.closeVideoPlayer = function(tabId){ + console.log('closeVideoPlayer') + try{ + var videoElement = $(`#tab-${tabId}`).find('video')[0] + if(!videoElement.paused)videoElement.pause(); + videoElement.removeAttribute('src'); + videoElement.load(); + }catch(err){ + console.log(err) + } + } + window.pauseVideoPlayer = function(tabId){ + console.log('pauseVideoPlayer') + try{ + var videoElement = $(`#tab-${tabId}`).find('video')[0] + try{ + videoElement.pause(); + }catch(err){ + + } + }catch(err){ + console.log(err) + } + } + window.resumeVideoPlayer = function(tabId){ + console.log('resumeVideoPlayer') + try{ + var videoElement = $(`#tab-${tabId}`).find('video')[0] + try{ + videoElement.play(); + }catch(err){ + + } + }catch(err){ + console.log(err) + } + } + function drawVideoInfoCard(){ + + } + function drawVideoEventsList(){ + + } + $('body') + .on('tab-open-videoPlayer',function(){ + if(pageLoadingData && pageLoadingData.title){ + theBlock.find('.video-title').html(pageLoadingData.title) + theBlock.find('.video-time').html(formattedTime(pageLoadingData.time,true)) + theBlock.find('.video-end').html(formattedTime(pageLoadingData.end,true)) + } + }) + .on('click','.tab-videoPlayer-event-row',function(){ + var el = $(this) + var parent = el.parents('main') + var videoEl = parent.find('video') + var videoData = loadedVideosInMemory[parent.attr('video-id')] + var videoTime = new Date(videoData.time).getTime() / 1000 + var timeIndex = new Date(el.attr('time-index')).getTime() / 1000 + var newVideoTimeIndex = timeIndex - videoTime + console.log(newVideoTimeIndex) + videoEl[0].currentTime = newVideoTimeIndex + videoEl[0].play() + }) +}) diff --git a/web/assets/js/bs5.videos.js b/web/assets/js/bs5.videos.js new file mode 100644 index 00000000..97840508 --- /dev/null +++ b/web/assets/js/bs5.videos.js @@ -0,0 +1,761 @@ +var loadedVideosInMemory = {} +var loadedFramesMemory = {} +var loadedFramesMemoryTimeout = {} +var loadedFramesLock = {} +function getLocalTimelapseImageLink(imageUrl){ + if(loadedFramesLock[imageUrl]){ + return null; + }else if(loadedFramesMemory[imageUrl]){ + return loadedFramesMemory[imageUrl] + }else{ + loadedFramesLock[imageUrl] = true + return new Promise((resolve,reject) => { + fetch(imageUrl) + .then(res => res.blob()) // Gets the response and returns it as a blob + .then(blob => { + var objectURL = URL.createObjectURL(blob); + loadedFramesMemory[imageUrl] = objectURL + clearTimeout(loadedFramesMemoryTimeout[imageUrl]) + loadedFramesMemoryTimeout[imageUrl] = setTimeout(function(){ + URL.revokeObjectURL(objectURL) + delete(loadedFramesMemory[imageUrl]) + delete(loadedFramesMemoryTimeout[imageUrl]) + },1000 * 60 * 10); + loadedFramesLock[imageUrl] = false; + resolve(objectURL); + }).catch((err) => { + resolve() + }); + }) + } +} +async function preloadAllTimelapseFramesToMemoryFromVideoList(framesSortedByDays){ + async function syncWait(waitTime){ + return new Promise((resolve,reject) => { + setTimeout(function(){ + resolve() + },waitTime) + }) + } + for (let ii = 0; ii < framesSortedByDays.length; ii++) { + var frame = framesSortedByDays[ii] + console.log ('Loading... ',frame.href) + await syncWait(50) + await getLocalTimelapseImageLink(frame.href) + console.log ('Loaded! ',frame.href) + } +} +function createVideoLinks(video,options){ + var details = safeJsonParse(video.details) + var queryString = [] + // if(details.isUTC === true){ + // queryString.push('isUTC=true') + // }else{ + // video.time = s.utcToLocal(video.time) + // video.end = s.utcToLocal(video.end) + // } + if(queryString.length > 0){ + queryString = '?' + queryString.join('&') + }else{ + queryString = '' + } + video.ext = video.ext ? video.ext : 'mp4' + if(details.type === 'googd'){ + video.href = undefined + }else if(!video.ext && video.href){ + video.ext = video.href.split('.') + video.ext = video.ext[video.ext.length - 1] + } + video.filename = formattedTimeForFilename(video.time,null,`YYYY-MM-DDTHH-mm-ss`) + '.' + video.ext; + var href = getApiPrefix('videos') + '/'+video.mid+'/'+video.filename; + video.actionUrl = href + video.links = { + deleteVideo : href+'/delete' + queryString, + changeToUnread : href+'/status/1' + queryString, + changeToRead : href+'/status/2' + queryString + } + if(!video.href || options.hideRemote === true)video.href = href + queryString + video.details = details + return video +} +function applyDataListToVideos(videos,events,keyName,reverseList){ + var updatedVideos = videos.concat([]) + var currentEvents = events.concat([]) + updatedVideos.forEach(function(video){ + var videoEvents = [] + currentEvents.forEach(function(theEvent,index){ + var startTime = new Date(video.time) + var endTime = new Date(video.end) + var eventTime = new Date(theEvent.time) + if(theEvent.mid === video.mid && eventTime >= startTime && eventTime <= endTime){ + videoEvents.push(theEvent) + currentEvents.splice(index, 1) + } + }) + if(reverseList)videoEvents = videoEvents.reverse() + video[keyName || 'events'] = videoEvents + }) + return updatedVideos +} +function applyTimelapseFramesListToVideos(videos,events,keyName,reverseList){ + var thisApiPrefix = getApiPrefix() + '/timelapse/' + $user.ke + '/' + var newVideos = applyDataListToVideos(videos,events,keyName,reverseList) + newVideos.forEach(function(video){ + video.timelapseFrames.forEach(function(row){ + var apiURL = thisApiPrefix + row.mid + row.href = libURL + apiURL + '/' + row.filename.split('T')[0] + '/' + row.filename + }) + }) + return newVideos +} +function getFrameOnVideoRow(percentageInward,video){ + var startTime = video.time + var endTime = video.end + var timeDifference = endTime - startTime + var timeInward = timeDifference / (100 / percentageInward) + var timeAdded = new Date(startTime.getTime() + timeInward) // ms + var frames = video.timelapseFrames || [] + var foundFrame = frames.length === 1 ? frames[0] : frames.find(function(row){ + return new Date(row.time) >= timeAdded + }); + return { + timeInward: timeInward, + foundFrame: foundFrame, + timeAdded: timeAdded, + } +} +function getVideoFromDay(percentageInward,reversedVideos,startTime,endTime){ + var timeDifference = endTime - startTime + var timeInward = timeDifference / (100 / percentageInward) + var timeAdded = new Date(startTime.getTime() + timeInward) // ms + var foundVideoIndex = reversedVideos.findIndex(function(row){ + return new Date(timeAdded) >= new Date(row.time) + }); + var foundVideo = reversedVideos[foundVideoIndex - 1] || reversedVideos[foundVideoIndex] || reversedVideos[0] + return foundVideo +} +// function bindFrameFindingByMouseMove(createdCardCarrier,video){ +// var createdCardElement = createdCardCarrier.find('.video-time-card').first() +// var timeImg = createdCardElement.find('.video-time-img') +// var timeStrip = createdCardElement.find('.video-time-strip') +// var timeNeedleSeeker = createdCardElement.find('.video-time-needle-seeker') +// if(video.timelapseFrames.length > 0){ +// createdCardElement.on('mousemove',function(evt){ +// var offest = createdCardElement.offset() +// var elementWidth = createdCardElement.width() + 2 +// var amountMoved = evt.pageX - offest.left +// var percentMoved = amountMoved / elementWidth * 100 +// percentMoved = percentMoved > 100 ? 100 : percentMoved < 0 ? 0 : percentMoved +// var frameFound = getFrameOnVideoRow(percentMoved,video).frameFound +// if(frameFound){ +// timeImg.css('background-image',`url(${frameFound.href})`) +// } +// timeNeedleSeeker.css('left',`${amountMoved}px`) +// }) +// timeImg.css('background-image',`url(${getFrameOnVideoRow(1,video).frameFound.href})`) +// }else{ +// if(video.events.length === 0){ +// timeStrip.hide() +// }else{ +// var eventMatrixHtml = `` +// var objectsFound = {} +// eventMatrixHtml += ` +// +// +// +// +// ` +// $.each(([]).concat(video.events).splice(0,11),function(n,theEvent){ +// var imagePath = `${formattedTimeForFilename(theEvent.time,false,'YYYY-MM-DD')}/${formattedTimeForFilename(theEvent.time,false,'YYYY-MM-DDTHH-mm-ss')}.jpg` +// possibleEventFrames += `
    ` +// }) +// $.each(video.events,function(n,theEvent){ +// $.each(theEvent.details.matrices,function(n,matrix){ +// if(!objectsFound[matrix.tag])objectsFound[matrix.tag] = 1 +// ++objectsFound[matrix.tag] +// }) +// }) +// $.each(objectsFound,function(tag,count){ +// eventMatrixHtml += ` +// +// +// ` +// }) +// eventMatrixHtml += `
    ${lang.Events}${video.events.length}
    ${tag}${count}
    ` +// timeStrip.append(eventMatrixHtml) +// } +// timeImg.remove() +// } +// } +function bindFrameFindingByMouseMoveForDay(createdCardCarrier,dayKey,videos,allFrames){ + var stripTimes = getStripStartAndEnd(videos,allFrames) + var dayStart = stripTimes.start + var dayEnd = stripTimes.end + var createdCardElement = createdCardCarrier.find('.video-time-card') + var timeImg = createdCardElement.find('.video-time-img') + var rowHeader = createdCardElement.find('.video-time-header') + var timeStrip = createdCardElement.find('.video-time-strip') + var timeNeedleSeeker = createdCardElement.find('.video-time-needle-seeker') + var firstFrameOfDay = allFrames[0] || null + $.each(videos,function(day,video){ + $.each(video.timelapseFrames,function(day,frame){ + if(!firstFrameOfDay)firstFrameOfDay = frame; + }) + }) + if(!firstFrameOfDay){ + timeImg.remove() + rowHeader.css('position','initial') + }else{ + timeImg.attr('src',firstFrameOfDay.href) + } + var videoSlices = createdCardElement.find('.video-day-slice') + var videoTimeLabel = createdCardElement.find('.video-time-label') + var currentlySelected = videos[0] + var currentlySelectedFrame = null + var reversedVideos = ([]).concat(videos).reverse(); + createdCardElement.on('mousemove',function(evt){ + var offest = createdCardElement.offset() + var elementWidth = createdCardElement.width() + 2 + var amountMoved = evt.pageX - offest.left + var percentMoved = amountMoved / elementWidth * 100 + percentMoved = percentMoved > 100 ? 100 : percentMoved < 0 ? 0 : percentMoved + var videoFound = videos[0] ? getVideoFromDay(percentMoved,reversedVideos,dayStart,dayEnd) : null + createdCardElement.find(`[data-time]`).css('background-color','') + if(videoFound){ + // var videoSlice = createdCardElement.find(`[data-time="${videoFound.time}"]`).css('background-color','rgba(255,255,255,0.3)') + // var videoSliceOffest = videoSlice.offset() + // var videoSliceElementWidth = videoSlice.width() + // var videoSliceAmountMoved = evt.pageX - videoSliceOffest.left + // var videoSlicePercentMoved = videoSliceAmountMoved / videoSliceElementWidth * 100 + // videoSlicePercentMoved = videoSlicePercentMoved > 100 ? 100 : videoSlicePercentMoved < 0 ? 0 : videoSlicePercentMoved + + if(currentlySelected && currentlySelected.time !== videoFound.time){ + timeNeedleSeeker.attr('video-time-seeked-video-position',videoFound.time) + } + currentlySelected = Object.assign({},videoFound) + } + // draw frame + var result = getFrameOnVideoRow(percentMoved,{ + time: dayStart, + end: dayEnd, + timelapseFrames: allFrames, + }) + var frameFound = result.foundFrame + videoTimeLabel.text(formattedTime(result.timeAdded,'hh:mm:ss AA, DD-MM-YYYY')) + if(frameFound){ + currentlySelectedFrame = Object.assign({},frameFound) + setTimeout(async function(){ + var frameUrl = await getLocalTimelapseImageLink(frameFound.href) + if(frameUrl && currentlySelectedFrame.time === frameFound.time)timeImg.attr('src',frameUrl); + },1) + } + timeNeedleSeeker.attr('video-slice-seeked',result.timeInward).css('left',`${percentMoved}%`) + }) +} +function getPercentOfTimePositionFromVideo(video,theEvent){ + var startTime = new Date(video.time) + var endTime = new Date(video.end) + var eventTime = new Date(theEvent.time) + var rangeMax = endTime - startTime + var eventMs = eventTime - startTime + var percentChanged = eventMs / rangeMax * 100 + return percentChanged +} +function createVideoRow(row,classOverride){ + var eventMatrixHtml = `` + if(row.events && row.events.length > 0){ + $.each(row.events,function(n,theEvent){ + var leftPercent = getPercentOfTimePositionFromVideo(row,theEvent) + eventMatrixHtml += `
    ` + }) + } + var videoEndpoint = getApiPrefix(`videos`) + '/' + row.mid + '/' + row.filename + return ` +
    +
    +
    +
    + ${loadedMonitors[row.mid] ? loadedMonitors[row.mid].name : row.mid} +
    +
    + + + +
    +
    +
    +
    +
    +
    + ${moment(row.time).fromNow()} +
    +
    + ~${durationBetweenTimes(row.time,row.end)} ${lang.Minutes} +
    +
    +
    +
    +
    ${lang.Started} : ${formattedTime(row.time,true)}
    +
    ${lang.Ended} : ${formattedTime(row.end,true)}
    +
    +
    +
    +
    + +
    +
    ` +} +function sortVideosByDays(videos){ + var days = {} + videos.forEach(function(video){ + var videoTime = new Date(video.time) + var theDayKey = `${videoTime.getDate()}-${videoTime.getMonth()}-${videoTime.getFullYear()}` + if(!days[video.mid])days[video.mid] = {}; + if(!days[video.mid][theDayKey])days[video.mid][theDayKey] = []; + days[video.mid][theDayKey].push(video) + }) + return days +} +function sortFramesByDays(frames){ + var days = {} + var thisApiPrefix = getApiPrefix() + '/timelapse/' + $user.ke + '/' + frames.forEach(function(frame){ + var frameTime = new Date(frame.time) + var theDayKey = `${frameTime.getDate()}-${frameTime.getMonth()}-${frameTime.getFullYear()}` + if(!days[frame.mid])days[frame.mid] = {}; + if(!days[frame.mid][theDayKey])days[frame.mid][theDayKey] = []; + var apiURL = thisApiPrefix + frame.mid + frame.href = libURL + apiURL + '/' + frame.filename.split('T')[0] + '/' + frame.filename + days[frame.mid][theDayKey].push(frame) + }) + return days +} +function getAllDays(videos,frames){ + var listOfDays = {} + $.each(loadedMonitors,function(monitorId){ + if(!listOfDays[monitorId])listOfDays[monitorId] = {} + }) + videos.forEach(function(video){ + var videoTime = new Date(video.time) + var theDayKey = `${videoTime.getDate()}-${videoTime.getMonth()}-${videoTime.getFullYear()}` + listOfDays[video.mid][theDayKey] = [] + }) + frames.forEach(function(frame){ + var frameTime = new Date(frame.time) + var theDayKey = `${frameTime.getDate()}-${frameTime.getMonth()}-${frameTime.getFullYear()}` + listOfDays[frame.mid][theDayKey] = [] + }) + return listOfDays +} +function getStripStartAndEnd(videos,frames){ + var stripStartTimeByVideos = videos[0] ? new Date(videos[0].time) : null + var stripEndTimeByVideos = videos[0] ? new Date(videos[videos.length - 1].end) : null + var stripStartTimeByFrames = frames[0] ? new Date(frames[0].time) : stripStartTimeByVideos + var stripEndTimeByFrames = frames[0] ? new Date(frames[frames.length - 1].time) : stripEndTimeByVideos + var stripStartTime = stripStartTimeByVideos && stripStartTimeByVideos < stripStartTimeByFrames ? stripStartTimeByVideos : stripStartTimeByFrames + var stripEndTime = stripEndTimeByVideos && stripEndTimeByVideos > stripEndTimeByFrames ? stripEndTimeByVideos : stripEndTimeByFrames + return { + start: new Date(stripStartTime), + end: new Date(stripEndTime), + } +} +function getVideoPercentWidthForDay(row,videos,frames){ + var startTime = new Date(row.time) + var endTime = new Date(row.end) + var timeDifference = endTime - startTime + var stripTimes = getStripStartAndEnd(videos,frames) + var stripTimeDifference = stripTimes.end - stripTimes.start + var percent = (timeDifference / stripTimeDifference) * 100 + return percent +} +function createDayCard(videos,frames,dayKey,monitorId,classOverride){ + var html = '' + var eventMatrixHtml = `` + var stripTimes = getStripStartAndEnd(videos,frames) + var startTime = stripTimes.start + var endTime = stripTimes.end + var firstVideoTime = videos[0] ? videos[0].time : null + var dayParts = formattedTime(startTime).split(' ')[1].split('-') + var day = dayParts[2] + var month = dayParts[1] + var year = dayParts[0] + $.each(videos,function(n,row){ + var nextRow = videos[n + 1] + var marginRight = !!nextRow ? getVideoPercentWidthForDay({time: row.end, end: nextRow.time},videos,frames) : 0; + eventMatrixHtml += `
    ` + if(row.events && row.events.length > 0){ + $.each(row.events,function(n,theEvent){ + var leftPercent = getPercentOfTimePositionFromVideo(row,theEvent) + eventMatrixHtml += `
    ` + }) + } + eventMatrixHtml += `
    ` + eventMatrixHtml += `
    ` + }) + html += ` +
    +
    +
    +
    +
    + ${loadedMonitors[monitorId] ? loadedMonitors[monitorId].name : monitorId} +
    + ${formattedTime(startTime)} to ${formattedTime(endTime)} +
    +
    +
    +
    ${day}
    +
    ${month}, ${year}
    +
    +
    +
    +
    + +
    + +
    +
    ` + return html +} +function drawVideoRowsToList(targetElement,rows){ + var theVideoList = $(targetElement) + theVideoList.empty() + $.each(rows,function(n,row){ + theVideoList.append(createVideoRow(row)) + }) + liveStamp() +} +function loadVideoData(video){ + delete(video.f) + loadedVideosInMemory[`${video.mid}${video.time}`] = video +} +function getVideos(options,callback){ + return new Promise((resolve,reject) => { + options = options ? options : {} + var searchQuery = options.searchQuery + var requestQueries = [] + var monitorId = options.monitorId + var archived = options.archived + var customVideoSet = options.customVideoSet + var limit = options.limit || 300 + var eventStartTime + var eventEndTime + // var startDate = options.startDate + // var endDate = options.endDate + if(options.startDate){ + eventStartTime = formattedTimeForFilename(options.startDate,false) + requestQueries.push(`start=${eventStartTime}`) + } + if(options.endDate){ + eventEndTime = formattedTimeForFilename(options.endDate,false) + requestQueries.push(`end=${eventEndTime}`) + } + if(searchQuery){ + requestQueries.push(`search=${searchQuery}`) + } + if(archived){ + requestQueries.push(`archived=1`) + } + $.getJSON(`${getApiPrefix(customVideoSet ? customVideoSet : searchQuery ? `videosByEventTag` : `videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(data){ + var videos = data.videos + $.getJSON(`${getApiPrefix(`timelapse`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`noLimit=1`]).join('&')}`,function(timelapseFrames){ + $.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([`limit=${limit}`]).join('&')}`,function(eventData){ + var newVideos = applyDataListToVideos(videos,eventData) + newVideos = applyTimelapseFramesListToVideos(newVideos,timelapseFrames,'timelapseFrames',true) + $.each(newVideos,function(n,video){ + loadVideoData(video) + }) + if(callback)callback({videos: newVideos, frames: timelapseFrames}); + resolve({videos: newVideos, frames: timelapseFrames}) + }) + }) + }) + }) +} +function getEvents(options,callback){ + options = options ? options : {} + var requestQueries = [] + var monitorId = options.monitorId + var limit = options.limit || 5000 + var eventStartTime + var eventEndTime + // var startDate = options.startDate + // var endDate = options.endDate + if(options.startDate){ + eventStartTime = formattedTimeForFilename(options.startDate,false) + requestQueries.push(`start=${eventStartTime}`) + } + if(options.endDate){ + eventEndTime = formattedTimeForFilename(options.endDate,false) + requestQueries.push(`end=${eventEndTime}`) + } + if(options.onlyCount){ + requestQueries.push(`onlyCount=1`) + } + $.getJSON(`${getApiPrefix(`events`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.join('&')}`,function(eventData){ + callback(eventData) + }) +} +function deleteVideo(video,callback){ + return new Promise((resolve,reject) => { + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + $.getJSON(videoEndpoint + '/delete',function(data){ + if(callback)callback(data) + resolve(data) + }) + }) +} +async function deleteVideos(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await deleteVideo(video) + } +} +function downloadVideo(video){ + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + downloadFile(videoEndpoint,video.filename) +} +async function downloadVideos(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await downloadVideo(video) + } +} +function compressVideo(video,callback){ + if(video.filename.includes('.webm')){ + console.log('Already Compressed') + if(callback)callback('Already Compressed') + return + } + return new Promise((resolve) => { + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + $.getJSON(videoEndpoint + '/compress',function(data){ + if(data.ok){ + console.log('Video Compressing') + }else{ + console.log('Video Not Compressing',data,videoEndpoint) + } + if(callback)callback() + resolve() + }) + }) +} +async function compressVideos(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await compressVideo(video) + } +} +function getArchiveButtons(video,isFileBin){ + return $(`[data-mid="${video.mid}"][data-ke="${video.ke}"][data-time="${video.time}"] .archive-${isFileBin ? `file` : 'video'}`) +} +var currentlyArchiving = {} +function archiveVideo(video,unarchive,isFileBin){ + return new Promise((resolve) => { + var videoEndpoint = getApiPrefix(isFileBin ? `fileBin` : `videos`) + '/' + video.mid + '/' + (isFileBin ? video.name : video.filename) + // var currentlyArchived = video.archive === 1 + if(currentlyArchiving[videoEndpoint]){ + resolve({ok: false}) + return; + } + currentlyArchiving[videoEndpoint] = true + $.getJSON(videoEndpoint + '/archive' + `${unarchive ? `?unarchive=1` : ''}`,function(data){ + if(data.ok){ + var archiveButtons = getArchiveButtons(video,isFileBin) + var classToRemove = 'btn-default' + var classToAdd = 'btn-success status-archived' + var iconToRemove = 'fa-unlock-alt' + var iconToAdd = 'fa-lock' + var elTitle = `${lang.Unarchive}` + if(!data.archived){ + console.log('Video Unarchived',unarchive) + classToRemove = 'btn-success status-archived' + classToAdd = 'btn-default' + iconToRemove = 'fa-lock' + iconToAdd = 'fa-unlock-alt' + elTitle = `${lang.Archive}` + }else{ + console.log('Video Archived',unarchive) + } + archiveButtons.removeClass(classToRemove).addClass(classToAdd).attr('title',elTitle) + archiveButtons.find('i').removeClass(iconToRemove).addClass(iconToAdd) + archiveButtons.find('span').text(elTitle) + video.archive = data.archived ? 1 : 0 + }else{ + console.log('Video Archive status unchanged',data,videoEndpoint) + } + delete(currentlyArchiving[videoEndpoint]) + resolve(data) + }) + }) +} +async function archiveVideos(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await archiveVideo(video) + } +} +function unarchiveVideo(video){ + return archiveVideo(video,true) +} +async function unarchiveVideos(videos){ + for (let i = 0; i < videos.length; i++) { + var video = videos[i]; + await unarchiveVideo(video) + } +} +onWebSocketEvent(function(d){ + switch(d.f){ + case'video_delete': + $('[file="'+d.filename+'"][mid="'+d.mid+'"]:not(.modal)').remove(); + $('[data-file="'+d.filename+'"][data-mid="'+d.mid+'"]:not(.modal)').remove(); + $('[data-time-formed="'+(new Date(d.time))+'"][data-mid="'+d.mid+'"]:not(.modal)').remove(); + var videoPlayerId = getVideoPlayerTabId(d) + if(tabTree.name === videoPlayerId){ + goBackOneTab() + } + deleteTab(videoPlayerId) + // if($.powerVideoWindow.currentDataObject&&$.powerVideoWindow.currentDataObject[d.filename]){ + // delete($.timelapse.currentVideos[$.powerVideoWindow.currentDataObject[d.filename].position]) + // $.powerVideoWindow.drawTimeline(false) + // } + // if($.timelapse.currentVideos&&$.timelapse.currentVideos[d.filename]){ + // delete($.timelapse.currentVideosArray.videos[$.timelapse.currentVideos[d.filename].position]) + // $.timelapse.drawTimeline(false) + // } + // if($.vidview.loadedVideos && $.vidview.loadedVideos[d.filename])delete($.vidview.loadedVideos[d.filename]) + break; + } +}) +$(document).ready(function(){ + $('body') + .on('click','.open-video',function(e){ + e.preventDefault() + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + var videoTime = el.attr('data-time') + var video = loadedVideosInMemory[`${monitorId}${videoTime}`] + createVideoPlayerTab(video) + return false; + }) + .on('click','[video-time-seeked-video-position]',function(){ + var el = $(this) + var monitorId = el.attr('data-mid') + var videoTime = el.attr('video-time-seeked-video-position') + var timeInward = (parseInt(el.attr('video-slice-seeked')) / 1000) - 2 + var video = loadedVideosInMemory[`${monitorId}${videoTime}`] + timeInward = timeInward < 0 ? 0 : timeInward + createVideoPlayerTab(video,timeInward) + }) + .on('click','.delete-video',function(e){ + e.preventDefault() + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + var videoTime = el.attr('data-time') + var video = loadedVideosInMemory[`${monitorId}${videoTime}`] + var ext = video.filename.split('.') + ext = ext[ext.length - 1] + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + $.confirm.create({ + title: lang["Delete Video"] + ' : ' + video.filename, + body: `${lang.DeleteVideoMsg}

    `, + clickOptions: { + title: ' ' + lang.Delete, + class: 'btn-danger btn-sm' + }, + clickCallback: function(){ + $.getJSON(videoEndpoint + '/delete',function(data){ + if(data.ok){ + console.log('Video Deleted') + }else{ + console.log('Video Not Deleted',data,videoEndpoint) + } + }) + } + }); + return false; + }) + .on('click','.compress-video',function(e){ + e.preventDefault() + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + var videoTime = el.attr('data-time') + var video = loadedVideosInMemory[`${monitorId}${videoTime}`] + var ext = video.filename.split('.') + ext = ext[ext.length - 1] + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + $.confirm.create({ + title: lang["Compress"] + ' : ' + video.filename, + body: `${lang.CompressVideoMsg}

    `, + clickOptions: { + title: ' ' + lang.Compress, + class: 'btn-primary btn-sm' + }, + clickCallback: function(){ + compressVideo(video) + } + }); + return false; + }) + .on('click','.archive-video',function(e){ + e.preventDefault() + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + var videoTime = el.attr('data-time') + var unarchive = $(this).hasClass('status-archived') + var video = loadedVideosInMemory[`${monitorId}${videoTime}`] + var ext = video.filename.split('.') + ext = ext[ext.length - 1] + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + if(unarchive){ + unarchiveVideo(video) + }else{ + // $.confirm.create({ + // title: lang["Archive"] + ' : ' + video.filename, + // body: `${lang.ArchiveVideoMsg}

    `, + // clickOptions: { + // title: ' ' + lang.Archive, + // class: 'btn-primary btn-sm' + // }, + // clickCallback: function(){ + archiveVideo(video) + // } + // }); + } + return false; + }) + .on('click','.fix-video',function(e){ + e.preventDefault() + var el = $(this).parents('[data-mid]') + var monitorId = el.attr('data-mid') + var videoTime = el.attr('data-time') + var video = loadedVideosInMemory[`${monitorId}${videoTime}`] + var ext = video.filename.split('.') + ext = ext[ext.length - 1] + var videoEndpoint = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename + $.confirm.create({ + title: lang["Fix Video"] + ' : ' + video.filename, + body: `${lang.FixVideoMsg}

    `, + clickOptions: { + title: ' ' + lang.Fix, + class: 'btn-danger btn-sm' + }, + clickCallback: function(){ + $.getJSON(videoEndpoint + '/fix',function(data){ + if(data.ok){ + console.log('Video Fixed') + }else{ + console.log('Video Not Fixed',data,videoEndpoint) + } + }) + } + }); + return false; + }) +}) diff --git a/web/libs/css/main.dash2.css b/web/assets/js/bs5.videosAndEventsList.js similarity index 100% rename from web/libs/css/main.dash2.css rename to web/assets/js/bs5.videosAndEventsList.js diff --git a/web/assets/js/bs5.videosTable.js b/web/assets/js/bs5.videosTable.js new file mode 100644 index 00000000..85113278 --- /dev/null +++ b/web/assets/js/bs5.videosTable.js @@ -0,0 +1,440 @@ +$(document).ready(function(e){ + var theEnclosure = $('#tab-videosTableView') + var monitorsList = theEnclosure.find('.monitors_list') + var dateSelector = theEnclosure.find('.date_selector') + var videosTableDrawArea = $('#videosTable_draw_area') + var videosTablePreviewArea = $('#videosTable_preview_area') + var objectTagSearchField = $('#videosTable_tag_search') + var cloudVideoCheckSwitch = $('#videosTable_cloudVideos') + var sideLinkListBox = $('#side-menu-link-videosTableView ul') + var loadedVideosTable = []; + var redrawTimeout; + var frameUrlCache = {} + var frameUrlCacheTimeouts = {} + async function getSnapshotFromVideoTimeFrame(monitorId,startDate,endDate){ + const frameUrlCacheId = `${monitorId}${startDate}${endDate}` + if(frameUrlCache[frameUrlCacheId]){ + return frameUrlCache[frameUrlCacheId] + }else{ + const frame = (await getTimelapseFrames(monitorId,startDate,endDate,1))[0] + const href = frame && frame.href ? frame.href : '' + frameUrlCache[frameUrlCacheId] = `${href}` + frameUrlCacheTimeouts[frameUrlCacheId] = setTimeout(() => { + delete(frameUrlCache[frameUrlCacheId]) + delete(frameUrlCacheTimeouts[frameUrlCacheId]) + },1000 * 60 * 15) + return href + } + } + function loadFramesForVideosInView(){ + videosTableDrawArea.find('.video-thumbnail').each(async (n,imgEl) => { + const el = $(imgEl) + const monitorId = el.attr('data-mid') + const startDate = el.attr('data-time') + const endDate = el.attr('data-end') + const imgBlock = el.find('.video-thumbnail-img-block') + const href = await getSnapshotFromVideoTimeFrame(monitorId,startDate,endDate) + imgBlock.find('img').attr('src',href) + }) + } + function openVideosTableView(monitorId,startDate,endDate){ + drawVideosTableViewElements(monitorId,startDate,endDate) + } + function getSelectedTime(asUtc){ + var dateRange = dateSelector.data('daterangepicker') + var startDate = dateRange.startDate.clone() + var endDate = dateRange.endDate.clone() + if(asUtc){ + startDate = startDate.utc() + endDate = endDate.utc() + } + startDate = startDate.format('YYYY-MM-DDTHH:mm:ss') + endDate = endDate.format('YYYY-MM-DDTHH:mm:ss') + return { + startDate: startDate, + endDate: endDate + } + } + + dateSelector.daterangepicker({ + startDate: moment().utc().subtract(2, 'days'), + endDate: moment().utc(), + timePicker: true, + locale: { + format: 'YYYY/MM/DD hh:mm:ss A' + } + }, function(start, end, label) { + drawVideosTableViewElements() + }) + monitorsList.change(function(){ + drawVideosTableViewElements() + }) + objectTagSearchField.change(function(){ + drawVideosTableViewElements() + }) + cloudVideoCheckSwitch.change(function(){ + drawVideosTableViewElements() + }) + async function drawVideosTableViewElements(usePreloadedData){ + var dateRange = getSelectedTime(false) + var searchQuery = objectTagSearchField.val() || null + var startDate = dateRange.startDate + var endDate = dateRange.endDate + var monitorId = monitorsList.val() + var wantsArchivedVideo = getVideoSetSelected() === 'archive' + var frameIconsHtml = '' + if(!usePreloadedData){ + loadedVideosTable = (await getVideos({ + monitorId, + startDate, + endDate, + searchQuery, + archived: wantsArchivedVideo, + customVideoSet: wantCloudVideos() ? 'cloudVideos' : null, + })).videos; + $.each(loadedVideosTable,function(n,v){ + loadedVideosInMemory[`${monitorId}${v.time}`] + }) + } + // for (let i = 0; i < loadedVideosTable.length; i++) { + // const file = loadedVideosTable[i] + // const frameUrl = await getSnapshotFromVideoTimeFrame(file.mid,file.time,file.end); + // file.frameUrl = frameUrl + // } + videosTableDrawArea.bootstrapTable('destroy') + videosTableDrawArea.bootstrapTable({ + onPostBody: loadFramesForVideosInView, + onPageChange: () => { + setTimeout(() => { + loadFramesForVideosInView() + },500) + }, + pagination: true, + search: true, + columns: [ + { + field: 'mid', + title: '', + checkbox: true, + formatter: () => { + return { + checked: false + } + }, + }, + { + field: 'image', + title: '', + }, + { + field: 'Monitor', + title: '', + }, + { + field: 'time', + title: lang['Time Created'], + }, + { + field: 'end', + title: lang['Ended'] + }, + { + field: 'objects', + title: lang['Objects Found'] + }, + { + field: 'tags', + title: '' + }, + { + field: 'size', + title: '' + }, + { + field: 'buttons', + title: '' + } + ], + data: loadedVideosTable.map((file) => { + var href = getFullOrigin(true) + file.href + var loadedMonitor = loadedMonitors[file.mid] + return { + image: `
    +
    + +
    + +
    `, + Monitor: loadedMonitor && loadedMonitor.name ? loadedMonitor.name : file.mid, + mid: file.mid, + time: formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA'), + end: formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA'), + tags: ` + ${file.ext} + `, + objects: file.objects, + size: convertKbToHumanSize(file.size), + buttons: ` +
    + + + ${permissionCheck('video_delete',file.mid) ? `` : ''} + ${permissionCheck('video_delete',file.mid) ? `` : ''} + ${permissionCheck('video_delete',file.mid) ? `` : ''} +
    + `, + } + }) + }) + } + function drawPreviewVideo(href){ + videosTablePreviewArea.html(``) + } + function getSelectedRows(){ + var rowsSelected = [] + videosTableDrawArea.find('[name="btSelectItem"]:checked').each(function(n,checkbox){ + var rowInfo = $(checkbox).parents('tr').find('.row-info') + var monitorId = rowInfo.attr('data-mid') + var groupKey = rowInfo.attr('data-ke') + var filename = rowInfo.attr('data-filename') + rowsSelected.push({ + mid: monitorId, + ke: groupKey, + filename: filename, + }) + }) + return rowsSelected + } + function getVideoSetSelected(){ + return cloudVideoCheckSwitch.val() + } + function wantCloudVideos(){ + const isChecked = getVideoSetSelected() === 'cloud' + return isChecked + } + function drawCompressedVideoProgressBar(data){ + var videoUrl = buildNewFileLink(data) + var html = `
  • +
    + +
    + ${lang.Compressing}... +
    +
    +
    +
    ${data.percent}%
    +
    +
    + + + +
    +
  • ` + sideLinkListBox.append(html) + } + function buildNewFileLink(data){ + return apiBaseUrl + '/videos/' + data.ke + '/' + data.mid + '/' + data.name + } + function downloadCompressedVideo(data){ + var downloadUrl = buildNewFileLink(data) + downloadFile(downloadUrl,data.name) + } + $('body') + .on('click','.open-videosTable',function(e){ + e.preventDefault() + var monitorId = getRowsMonitorId(this) + openTab(`videosTableView`,{},null,null,null,() => { + drawMonitorListToSelector(monitorsList,null,null,true) + monitorsList.val(monitorId) + drawVideosTableViewElements() + }) + return false; + }); + sideLinkListBox + .on('click','.remove-row',function(){ + var el = $(this).parents('[data-mid]') + el.remove() + }); + theEnclosure + .on('click','.preview-video',function(e){ + e.preventDefault() + var href = $(this).attr('href') + drawPreviewVideo(href) + return false; + }) + .on('click','.refresh-data',function(e){ + e.preventDefault() + drawVideosTableViewElements() + return false; + }) + .on('click','.open-snapshot',function(e){ + e.preventDefault() + var href = $(this).parents('.video-thumbnail').find('img').click() + return false; + }) + .on('click','.delete-selected-videos',function(e){ + e.preventDefault() + var videos = getSelectedRows() + if(videos.length === 0)return; + $.confirm.create({ + title: lang["Delete Videos"], + body: `${lang.DeleteTheseMsg}`, + clickOptions: { + title: ' ' + lang.Delete, + class: 'btn-danger btn-sm' + }, + clickCallback: function(){ + deleteVideos(videos).then(() => { + console.log(`Done Deleting Rows!`) + }) + } + }); + return false; + }) + .on('click','.compress-selected-videos',function(e){ + e.preventDefault() + var videos = getSelectedRows() + if(videos.length === 0)return; + $.confirm.create({ + title: lang["Compress Videos"], + body: `${lang.CompressTheseMsg}`, + clickOptions: { + title: ' ' + lang.Compress, + class: 'btn-primary btn-sm' + }, + clickCallback: function(){ + compressVideos(videos).then(() => { + console.log(`Done Sending Compression Request!`) + }) + } + }); + return false; + }) + .on('click','.archive-selected-videos',function(e){ + e.preventDefault() + var videos = getSelectedRows() + if(videos.length === 0)return; + $.confirm.create({ + title: lang["Archive Videos"], + body: `${lang.ArchiveTheseMsg}`, + clickOptions: { + title: ' ' + lang.Archive, + class: 'btn-primary btn-sm' + }, + clickCallback: function(){ + archiveVideos(videos).then(() => { + console.log(`Done Archiving Rows!`) + }) + } + }); + return false; + }) + .on('click','.download-selected-videos',function(e){ + e.preventDefault() + var videos = getSelectedRows() + if(videos.length === 0)return; + $.confirm.create({ + title: lang["Batch Download"], + body: `${lang.batchDownloadText}`, + clickOptions: { + title: ' ' + lang.Yes, + class: 'btn-success btn-sm' + }, + clickCallback: function(){ + downloadVideos(videos) + } + }); + return false; + }) + .on('click','.pop-img',function(e){ + e.preventDefault() + var videos = getSelectedRows() + if(videos.length === 0)return; + $.confirm.create({ + title: lang["Batch Download"], + body: `${lang.batchDownloadText}`, + clickOptions: { + title: ' ' + lang.Yes, + class: 'btn-success btn-sm' + }, + clickCallback: function(){ + downloadVideos(videos) + } + }); + return false; + }) + onWebSocketEvent((data) => { + switch(data.f){ + case'video_delete': + if(tabTree.name === 'videosTableView'){ + var videoIndexToRemove = loadedVideosTable.findIndex(row => data.mid === row.mid && new Date(row.time).getTime() === new Date(data.time).getTime()) + if(videoIndexToRemove !== -1){ + loadedVideosTable.splice(videoIndexToRemove, 1); + delete(loadedVideosInMemory[`${data.mid}${data.time}`]) + clearTimeout(redrawTimeout) + redrawTimeout = setTimeout(function(){ + drawVideosTableViewElements(true) + },2000) + } + } + break; + case'video_compress_started': + console.log(`Compressing Video...`,data) + break; + case'video_compress_completed': + if(data.success){ + var progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`) + var saveBuiltVideo = dashboardOptions().switches.saveCompressedVideo + if(saveBuiltVideo === 1){ + downloadCompressedVideo(data) + progressItem.remove() + console.log(`Downloaded!`,data) + }else if(!data.automated){ + progressItem.find('.row-status').text(`${lang.Done}!`) + progressItem.find('.dot').removeClass('dot-orange').addClass('dot-green') + progressItem.find('.download-button').show() + progressItem.find('.progress-bar').css('width',`100%`).text(`100%`) + }else if(data.automated){ + progressItem.remove() + } + } + break; + case'video_compress_percent': + var progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`) + data.percent = data.percent > 100 ? 100 : data.percent + if(progressItem.length === 0){ + drawCompressedVideoProgressBar(data) + }else{ + progressItem = sideLinkListBox.find(`[data-mid="${data.mid}"][data-ke="${data.mid}"][data-name="${data.name}"]`) + progressItem.find('.progress-bar').css('width',`${data.percent}%`).text(`${data.percent}%`) + } + console.log(data) + break; + } + }) + addOnTabOpen('videosTableView', function () { + drawMonitorListToSelector(monitorsList,null,null,true) + drawVideosTableViewElements() + }) + addOnTabReopen('videosTableView', function () { + var theSelected = `${monitorsList.val()}` + drawMonitorListToSelector(monitorsList,null,null,true) + monitorsList.val(theSelected) + }) + addOnTabAway('videosTableView', function () { + videosTablePreviewArea.find('video')[0].pause() + }) +}) diff --git a/web/assets/js/bs5.websocket.js b/web/assets/js/bs5.websocket.js new file mode 100644 index 00000000..87ce02c7 --- /dev/null +++ b/web/assets/js/bs5.websocket.js @@ -0,0 +1,76 @@ +var mainSocket = {} +var websocketPath = checkCorrectPathEnding(location.pathname) + 'socket.io' +var websocketQuery = {} +if(location.search === '?p2p=1'){ + window.machineId = location.pathname.split('/')[2] + websocketPath = '/socket.io' + websocketQuery.machineId = machineId +} +var onInitWebsocketFunctions = [] +function onInitWebsocket(theAction){ + onInitWebsocketFunctions.push(theAction) +} +var onWebSocketEventFunctions = [] +function onWebSocketEvent(theAction){ + onWebSocketEventFunctions.push(theAction) +} +var queuedCallbacks = {} +$(document).ready(function(){ + mainSocket = io(location.origin,{ + path: websocketPath, + query: websocketQuery + }) + mainSocket.f = function(data,callback){ + if(!data.ke)data.ke = $user.ke; + if(!data.uid)data.uid = $user.uid; + if(callback){ + var callbackId = generateId(); + data.callbackId = callbackId + queuedCallbacks[callbackId] = callback + } + console.log('Sending Data',data) + return mainSocket.emit('f',data) + } + mainSocket.on('ping', function(){ + mainSocket.emit('pong',{beat:1}) + }) + mainSocket.on('connect',function (d){ + console.log('Connected to Websocket!') + if(location.search === '?p2p=1'){ + mainSocket.emit('p2pInitUser',{ + user: { + ke: $user.ke, + mail: $user.mail, + auth_token: $user.auth_token, + details: $user.details, + uid: $user.uid, + }, + machineId: machineId + }) + }else{ + mainSocket.f({ + f: 'init', + ke: $user.ke, + auth: $user.auth_token, + uid: $user.uid + }) + } + }) + mainSocket.on('f',function (d){ + switch(d.f){ + case'init_success': + console.log('Authenticated to Websocket!') + $.each(onInitWebsocketFunctions,function(n,theAction){ + theAction(d) + }) + break; + case'callback': + console.log('Callback from Websocket Request',d) + queuedCallbacks[d.callbackId](...d.args) + break; + } + $.each(onWebSocketEventFunctions,function(n,theAction){ + theAction(d) + }) + }) +}) diff --git a/web/libs/js/super.configEditor.js b/web/assets/js/super.configEditor.js similarity index 99% rename from web/libs/js/super.configEditor.js rename to web/assets/js/super.configEditor.js index e34d8b5d..f63bc7b1 100644 --- a/web/libs/js/super.configEditor.js +++ b/web/assets/js/super.configEditor.js @@ -1,6 +1,6 @@ $(document).ready(function(){ var schema = { - "title": "Shinobi Configuration", + "title": "Main Configuration", "type": "object", "properties": { "debugLog": { diff --git a/web/libs/js/super.customAutoLoad.js b/web/assets/js/super.customAutoLoad.js similarity index 90% rename from web/libs/js/super.customAutoLoad.js rename to web/assets/js/super.customAutoLoad.js index 5910d6b1..3d73889d 100644 --- a/web/libs/js/super.customAutoLoad.js +++ b/web/assets/js/super.customAutoLoad.js @@ -14,19 +14,19 @@ $(document).ready(function(){ }else{ listElement.append(`
    -
    +

    ${humanName}

    -
    ${lang['Time Created']} : ${module.created}
    -
    ${lang['Last Modified']} : ${module.lastModified}
    +
    ${lang['Time Created']} : ${module.created}
    +
    ${lang['Last Modified']} : ${module.lastModified}
    diff --git a/web/libs/js/super.easyRemoteAccess.js b/web/assets/js/super.easyRemoteAccess.js similarity index 72% rename from web/libs/js/super.easyRemoteAccess.js rename to web/assets/js/super.easyRemoteAccess.js index c2eef8a6..f48e8ff3 100644 --- a/web/libs/js/super.easyRemoteAccess.js +++ b/web/assets/js/super.easyRemoteAccess.js @@ -1,8 +1,12 @@ $(document).ready(function(){ var easyRemoteAccessTab = $('#easyRemoteAccess') + var toggleAffected = easyRemoteAccessTab.find('.p2p-toggle-affected') + var p2pEnabledSwitch = easyRemoteAccessTab.find('[name="p2pEnabled"]') var p2pHostSelectedContainer = $('#p2pHostSelected') var easyRemoteAccessForm = easyRemoteAccessTab.find('form') + var remoteDashboardLinkButton = easyRemoteAccessTab.find('.remote-dashboard-link') var loadingRegistration = false + var statusConnections = {} var currentlyRegisteredP2PServer = currentlySelectedP2PServerId ? currentlySelectedP2PServerId + '' : undefined function copyToClipboard(str) { const el = document.createElement('textarea'); @@ -35,11 +39,13 @@ $(document).ready(function(){ var chartViewerCount = cardEl.find('.chartViewerCount') var connectedUsers = cardEl.find('.connectedUsers') var registeredServers = cardEl.find('.registeredServers') - var socketConnection = io(`ws://${server.host}:${server.p2pPort}`,{ + var chartPort = server.v2 ? server.chartPort || parseInt(server.webPort) + 2 : server.p2pPort + var socketConnection = io(`ws://${server.host}:${chartPort}`,{ transports: ['websocket'], query: { charts: '1' - } + }, + reconnect: false, }) socketConnection.on('initUI',function(data){ cardEl.find('.ramTotal').text(bytesToSize(data.ram)) @@ -55,6 +61,7 @@ $(document).ready(function(){ connectedUsers.text(data.users) chartViewerCount.text(data.statViewers) }) + statusConnections[key] = socketConnection } function disableForm(){ loadingRegistration = true @@ -74,8 +81,49 @@ $(document).ready(function(){ var cardEl = easyRemoteAccessTab.find(`[drawn-id="${key}"]`) easyRemoteAccessTab.find(`[drawn-id].selected`).removeClass('selected') cardEl.addClass('selected') + setCurrentRemoteLink() } } + function setCurrentRemoteLink(){ + var apiKey = easyRemoteAccessForm.find('[name="p2pApiKey"]').val() + var selectedServer = p2pServerList[currentlyRegisteredP2PServer] + console.log(selectedServer,currentlySelectedP2PServerId,p2pServerList) + if(selectedServer && selectedServer.host){ + var href = `http://${selectedServer.host}:${selectedServer.webPort}/s/${apiKey}/${window.useBetterP2P ? '' : '?p2p=1'}` + remoteDashboardLinkButton.attr('href',href) + }else{ + new PNotify({ + type: 'warning', + title: lang['P2P Server Not Selected'], + text: lang.p2pServerNotSelectedText, + }) + } + } + function beginAllStatusConnections(){ + $.each(p2pServerList,function(key,server){ + server.key = key + if(window.useBetterP2P && !server.v2)return; + if(!window.useBetterP2P && server.v2)return; + beginStatusConnectionForServer(key,server) + }) + } + function closeAllStatusConnections(){ + $.each(statusConnections,function(key,server){ + server.disconnect() + delete(statusConnections[key]) + }) + } + function setVisibilityForList(){ + var isOn = p2pEnabledSwitch.val() === '1' + if(isOn){ + beginAllStatusConnections() + toggleAffected.show() + }else{ + closeAllStatusConnections() + toggleAffected.hide() + } + } + p2pEnabledSwitch.change(setVisibilityForList) easyRemoteAccessTab.find('.submit').click(function(){ easyRemoteAccessForm.submit() }) @@ -96,6 +144,7 @@ $(document).ready(function(){ title: lang['P2P Settings Applied'], text: lang.p2pSettingsText1, }) + setCurrentRemoteLink() setTimeout(enableForm,5000) } }) @@ -108,26 +157,6 @@ $(document).ready(function(){ el.addClass('active') currentlySelectedP2PServerId = p2pServerId }) - easyRemoteAccessTab.on('click','.remote-dashboard-link',function(e){ - e.preventDefault() - if(!loadingRegistration){ - var apiKey = easyRemoteAccessForm.find('[name="p2pApiKey"]').val() - var selectedServer = p2pServerList[currentlyRegisteredP2PServer] - console.log(selectedServer,currentlySelectedP2PServerId,p2pServerList) - if(selectedServer && selectedServer.host){ - var href = `http://${selectedServer.host}:${selectedServer.webPort}/s/${apiKey}?p2p=1` - var win = window.open(href, '_blank'); - win.focus(); - }else{ - new PNotify({ - type: 'warning', - title: lang['P2P Server Not Selected'], - text: lang.p2pServerNotSelectedText, - }) - } - } - return false; - }) easyRemoteAccessTab.on('click','.remote-dashboard-link-copy',function(e){ e.preventDefault() if(!loadingRegistration){ @@ -135,7 +164,7 @@ $(document).ready(function(){ var selectedServer = p2pServerList[currentlyRegisteredP2PServer] console.log(selectedServer,currentlySelectedP2PServerId,p2pServerList) if(selectedServer && selectedServer.host){ - var href = `http://${selectedServer.host}:${selectedServer.webPort}/s/${apiKey}?p2p=1` + var href = `http://${selectedServer.host}:${selectedServer.webPort}/s/${apiKey}/${window.useBetterP2P ? '' : '?p2p=1'}` copyToClipboard(href) new PNotify({ type: 'success', @@ -152,9 +181,6 @@ $(document).ready(function(){ } return false; }) - $.each(p2pServerList,function(key,server){ - server.key = key - beginStatusConnectionForServer(key,server) - }) + setVisibilityForList() displayCurrentlySelectedInternally() }) diff --git a/web/libs/js/super.pluginManager.js b/web/assets/js/super.pluginManager.js similarity index 90% rename from web/libs/js/super.pluginManager.js rename to web/assets/js/super.pluginManager.js index f74ce6bc..b2e55564 100644 --- a/web/libs/js/super.pluginManager.js +++ b/web/assets/js/super.pluginManager.js @@ -14,24 +14,24 @@ $(document).ready(function(){ }else{ listElement.append(`
    -
    -
    +
    +

    ${humanName}

    -
    ${lang['Time Created']} : ${module.created}
    -
    ${lang['Last Modified']} : ${module.lastModified}
    +
    ${lang['Time Created']} : ${module.created}
    +
    ${lang['Last Modified']} : ${module.lastModified}
    -
    -
    +
    +