Merge branch 'dev' into 'cron-as-worker-process'

# Conflicts:
#   libs/socketio.js
cron-as-worker-process
Moe 2022-08-09 23:22:54 +00:00
commit 5d9db1124b
119 changed files with 5650 additions and 7404 deletions

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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 "============="

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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,6 +16,9 @@
"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",
"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",
@ -23,7 +26,9 @@
"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",
@ -74,6 +79,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",
@ -165,6 +171,7 @@
"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",
@ -207,6 +214,7 @@
"Videos": "Videos",
"Events": "Events",
"Events Found": "Events Found",
"Objects Found": "Objects Found",
"Recent Events": "Recent Events",
"Streams": "Streams",
"Snapshot": "Snapshot",
@ -254,6 +262,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",
@ -384,6 +395,7 @@
"yourFileDownloadedShortly": "Please wait. Your file will be downloaded shortly.",
"Save Built Video on Completion": "Save Built 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",
@ -480,9 +492,10 @@
"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.",
"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.",
"DeleteThisMsg": "Do you want to delete this? You cannot recover it.",
"DeleteTheseMsg": "Do you want to delete these? You cannot recover them.",
"dropBoxSuccess": "Success! Files saved to your Dropbox.",
"API Key Deleted": "API Key Deleted",
"APIKeyDeletedText": "Key has been deleted. It will no longer work.",
@ -655,7 +668,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",
@ -728,7 +741,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",
@ -738,6 +751,7 @@
"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",
@ -855,9 +869,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",
@ -958,8 +971,12 @@
"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",
@ -986,6 +1003,7 @@
"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",
@ -1151,6 +1169,7 @@
"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",
@ -1170,6 +1189,7 @@
"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",
@ -1183,6 +1203,7 @@
"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",
@ -1397,6 +1418,9 @@
"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 <a target='_blank' href='https://hub.shinobi.video/articles/view/DmWIID78VtvEfnf'>optimization guide</a> 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.",
@ -1563,11 +1587,11 @@
"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 Motion Events in SQL. This will allow display of motion over video during the time motion events occured in the Power Viewer.",
"fieldTextDetectorRecordMethod": "There are multiple ways to begin recording when an event occurs, like motion. Traditional Recording is the most user-friendly.",
"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": "If there is an overlap in trigger record should it reset.",
"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.",
@ -1579,7 +1603,7 @@
"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.",
"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.",
@ -1633,7 +1657,7 @@
"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 Traditional Recording, Hotswap, or Delete Motionless with their currently set options in the Global Detection Settings section.",
"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.",

View File

@ -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){

View File

@ -186,6 +186,13 @@ module.exports = (processCwd,config) => {
else
return false;
}
function asyncSetTimeout(timeoutAmount) {
return new Promise((resolve,reject) => {
setTimeout(function(){
resolve()
},timeoutAmount)
})
}
return {
parseJSON: parseJSON,
stringJSON: stringJSON,
@ -203,5 +210,6 @@ module.exports = (processCwd,config) => {
fetchTimeout: fetchTimeout,
fetchDownloadAndWrite: fetchDownloadAndWrite,
fetchWithAuthentication: fetchWithAuthentication,
asyncSetTimeout: asyncSetTimeout,
}
}

View File

@ -14,7 +14,7 @@ module.exports = function(s,config,lang,app,io){
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='https://shinobi.video/libs/assets/backgrounds/7.jpg'}
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){

View File

@ -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)
}
}
}

View File

@ -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 = {};
@ -71,7 +74,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){
var sendDetectedData = function(detectorObject){
pamDiffResponder(detectorObject)
}
}else{
}else{
var sendDetectedData = function(detectorObject){
pamDiffResponder.write(Buffer.from(JSON.stringify(detectorObject)))
}
@ -241,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 = [],
@ -372,6 +397,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){
getPropertiesFromBlob,
createMatricesFromBlobs,
logData,
getTileMotionEvent,
// parameters
pamDetectorIsEnabled,
noiseFilterArray,

View File

@ -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,
}
}

View File

@ -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,
}

View File

@ -258,8 +258,9 @@ function startConnection(p2pServerAddress,subscriptionId){
})
onIncomingMessage('disconnect',function(data,requestId){
console.log(`FAILED LICENSE CHECK ON P2P`)
if(data.retryLater)console.log(`Retrying Later`)
stayDisconnected = data && !data.retryLater
const retryLater = data && data.retryLater;
stayDisconnected = !retryLater
if(retryLater)console.log(`Retrying P2P Later...`)
})
}
const responseTunnels = {}

View File

@ -26,13 +26,20 @@ const getBuffer = async (url) => {
try {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
const buffer = Buffer.from(arrayBuffer);
return buffer;
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) => {

View File

@ -1,7 +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;
}
})
}

View File

@ -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,
@ -128,9 +131,13 @@ module.exports = function(s,config,lang,app,io){
const groupKey = onvifOptions.ke
const monitorId = onvifOptions.mid
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
theUrl = (await theDevice.services.media.getSnapshotUri({
ProfileToken : theDevice.current_profile.token,
})).GetSnapshotUriResponse.MediaUri.Uri;
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,

View File

@ -1,286 +1,299 @@
var os = require('os');
var exec = require('child_process').exec;
module.exports = function(s,config,lang){
const { fetchWithAuthentication } = require('../basic/utils.js')(process.cwd(),config)
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)
const hasDigestAuthEnabled = monitorConfig.details.control_digest_auth === '1'
const requestUrl = controlBaseUrl + controlOptions.path
const response = {
ok: true,
type:'Control Trigger Ended'
}
const theRequest = fetchWithAuthentication(requestUrl,{
method: controlOptions.method,
digestAuth: hasDigestAuthEnabled,
body: controlOptions.postData || null
});
theRequest.then(res => res.text())
.then((data) => {
moveLock[options.ke + options.id] = false
s.userLog(monitorConfig,response);
});
theRequest.catch((err) => {
response.ok = false
response.type = 'Control Error'
response.msg = err
callback(response)
})
}
if(options.direction === 'stopMove'){
stopCamera()
response = await moveGeneric(options,false)
}else{
moveLock[options.ke + options.id] = true
let controlURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}`]
let controlOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig)
const hasDigestAuthEnabled = monitorConfig.details.control_digest_auth === '1'
const requestUrl = controlBaseUrl + controlOptions.path
const response = {
ok: true,
type: lang['Control Triggered']
// 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;
}
const theRequest = fetchWithAuthentication(requestUrl,{
method: controlOptions.method,
digestAuth: hasDigestAuthEnabled,
body: controlOptions.postData || null
});
theRequest.then(res => res.text())
.then((data) => {
if(monitorConfig.details.control_stop == '1' && options.direction !== 'center' ){
s.userLog(monitorConfig,{type: lang['Control Trigger Started']});
if(controlUrlStopTimeout > 0){
setTimeout(function(){
stopCamera()
},controlUrlStopTimeout)
}
}else{
moveLock[options.ke + options.id] = false
callback(response)
}
});
theRequest.catch((err) => {
response.ok = false
response.type = lang['Control Error']
response.msg = err
callback(response)
})
}
}
if(callback)callback(response);
return response;
}
const getPresetPositions = (options,callback) => {
const profileToken = options.ProfileToken || "__CURRENT_TOKEN"
@ -299,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,
@ -366,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'
@ -375,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
@ -382,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) => {
@ -399,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,
}
}

View File

@ -40,7 +40,6 @@ module.exports = async (s,config,lang,app,io) => {
const arguments = s.getPostData(req) || []
try{
const groupKey = req.params.ke
console.log(Object.keys(user))
endData.response = await s.group[groupKey].zwave[actionFunction](...arguments)
}catch(err){
endData.ok = false

View File

@ -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) => {
@ -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) {

View File

@ -87,12 +87,13 @@ module.exports = function(s,config,lang){
s.onMonitorStart((monitorConfig) => {
initializeOnvifEvents(monitorConfig)
})
const connectionInfoArray = s.definitions["Monitor Settings"].blocks["Connection"].info
connectionInfoArray.splice(connectionInfoArray.length - 2, 0, {
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": [
{

View File

@ -152,7 +152,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) => {
@ -420,7 +419,8 @@ module.exports = (s,config,lang,app,io) => {
monitorConfig.mode === 'start' &&
(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()
@ -527,14 +527,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,{

View File

@ -1,211 +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.onFfmpegBuildMainStreamExtensions = []
s.onFfmpegBuildMainStream = function(callback){
s.onFfmpegBuildMainStreamExtensions.push(callback)
}
//
s.onFfmpegBuildStreamChannelExtensions = []
s.onFfmpegBuildStreamChannel = function(callback){
s.onFfmpegBuildStreamChannelExtensions.push(callback)
}
//
s.onMonitorPingFailedExtensions = []
s.onMonitorPingFailed = function(callback){
s.onMonitorPingFailedExtensions.push(callback)
}
//
s.onMonitorDiedExtensions = []
s.onMonitorDied = function(callback){
s.onMonitorDiedExtensions.push(callback)
}
//
s.onMonitorCreateStreamPipeExtensions = []
s.onMonitorCreateStreamPipe = function(callback){
s.onMonitorCreateStreamPipeExtensions.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)
}
//
s.onDataPortMessageExtensions = []
s.onDataPortMessage = function(callback){
s.onDataPortMessageExtensions.push(callback)
}
//
s.onHttpRequestUpgradeExtensions = {}
s.onHttpRequestUpgrade = function(nameOfCallback,callback){
s.onHttpRequestUpgradeExtensions[nameOfCallback] = 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`)
}

View File

@ -679,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'){

View File

@ -130,7 +130,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)
}
})
}
})

View File

@ -215,12 +215,20 @@ module.exports = function(s,config,lang,app,io){
}
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']
})
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({

View File

@ -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
})
}
})
}

View File

@ -1773,6 +1773,7 @@ module.exports = function(s,config,lang){
monitorRestrictions.push(['or','mid','=',v])
}
})
console.log(monitorRestrictions)
}catch(er){
}
}else if(
@ -1790,20 +1791,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,
}
}
}

View File

@ -31,7 +31,9 @@ module.exports = (s,config,lang) => {
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){
@ -473,7 +475,102 @@ module.exports = (s,config,lang) => {
// },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,

View File

@ -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){

View File

@ -149,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
@ -188,13 +195,6 @@ module.exports = function(s,config,lang,getSnapshot){
})
}
}
await getSnapshot(d,monitorConfig)
sendMail([
{
filename: d.screenshotName + '.jpg',
content: d.screenshotBuffer
}
])
})
}
}

View File

@ -150,6 +150,13 @@ module.exports = function (s, config, lang, getSnapshot) {
}),
},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
@ -178,13 +185,6 @@ module.exports = function (s, config, lang, getSnapshot) {
})
}
}
await getSnapshot(d,monitorConfig)
sendMail([
{
filename: d.screenshotName + '.jpg',
content: d.screenshotBuffer
}
])
}
};

View File

@ -32,7 +32,7 @@ module.exports = function(s,config,lang,getSnapshot){
username: username,
password: password,
clientId: `shinobi_${Math.random().toString(16).substr(2, 8)}`,
reconnectPeriod: 1000, // 10 seconds
reconnectPeriod: 10000, // 10 seconds
});
client.on('reconnect', (e) => mqttUserLog(`MQTT Reconnected`))
client.on('disconnect', (e) => mqttUserLog(`MQTT Disconnected`))

View File

@ -68,6 +68,19 @@ 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
@ -96,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){

View File

@ -26,10 +26,26 @@ module.exports = function(s,config,lang,app,io){
*/
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 = await getOnvifDevice(groupKey,monitorId)
const cameraInfo = await getUIFieldValues(onvifDevice)
endData.onvifData = cameraInfo
@ -46,11 +62,29 @@ 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 = await getOnvifDevice(groupKey,monitorId)
const form = s.getPostData(req)
const videoToken = form.VideoConfiguration && form.VideoConfiguration.videoToken ? form.VideoConfiguration.videoToken : null
@ -100,10 +134,28 @@ 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 = await getOnvifDevice(groupKey,monitorId)
const cameraInfo = await rebootCamera(onvifDevice)
endData.onvifData = cameraInfo

View File

@ -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)
})

View File

@ -1,6 +1,7 @@
var os = require('os');
const onvif = require("shinobi-onvif");
const {
addCredentialsToUrl,
stringContains,
getBuffer,
} = require('../common.js')
@ -145,9 +146,13 @@ module.exports = (s,config,lang) => {
responseList.push(cameraResponse)
var imageSnap
try{
const snapUri = (await device.services.media.getSnapshotUri({
ProfileToken : device.current_profile.token,
})).data.GetSnapshotUriResponse.MediaUri.Uri
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)

View File

@ -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){

View File

@ -2,6 +2,9 @@ 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})

View File

@ -367,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){
@ -630,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){
@ -699,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)

View File

@ -162,6 +162,20 @@ module.exports = function(s,config){
}catch(err){
console.log(err)
}
try{
s.databaseEngine.schema.table('Videos', table => {
table.string('objects')
}).then(() => {
console.log(`objects added to Videos table`)
}).catch((err) => {
if(err && err.code !== 'ER_DUP_FIELDNAME'){
console.log('error')
console.log(err)
}
})
}catch(err){
console.log(err)
}
delete(s.preQueries)
}
}

View File

@ -17,6 +17,9 @@ module.exports = function(s,config,lang,app,io){
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){
@ -248,24 +251,6 @@ module.exports = function(s,config,lang,app,io){
})
})
}
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()
})
})
}
async function chunkFramesAndBuildMultipleVideosThenSticth(options){
// a single video with too many frames makes the video unplayable, this is the fix.
const frames = options.frames
@ -311,10 +296,10 @@ module.exports = function(s,config,lang,app,io){
listFile: concatListFile,
output: finalMp4OutputLocation,
})
await fs.promises.unlink(concatListFile)
for (let i = 0; i < filePathsList; i++) {
await fs.promises.rm(concatListFile)
for (let i = 0; i < filePathsList.length; i++) {
var segmentFileOutput = filePathsList[i]
await fs.promises.unlink(segmentFileOutput)
await fs.promises.rm(segmentFileOutput)
}
s.debugLog('videoBuildProcess Stitching Complete!',finalMp4OutputLocation)
}
@ -400,6 +385,40 @@ module.exports = function(s,config,lang,app,io){
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
// // // // //
/**
@ -409,25 +428,44 @@ 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,
@ -453,47 +491,33 @@ 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],['mid','=',req.params.id],['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){
s.closeJsonResponse(res,{
ok: false
})
return
}
const buildResponse = await createVideoFromTimelapse(r.reverse(),s.getPostData(req, 'fps'))
initiateTimelapseVideoBuild({
groupKey,
monitorId,
framesPosted,
framesPerSecond,
}).then((buildResponse) => {
s.closeJsonResponse(res,buildResponse)
})
},res,req);
@ -507,15 +531,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
@ -529,7 +569,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,
@ -597,35 +637,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(async function(monitorId){
var frameSet = groups[groupKey][monitorId]
await createVideoFromTimelapse(frameSet,30)
})
})
})
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){

View File

@ -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 {

View File

@ -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){

View File

@ -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,7 +179,7 @@ module.exports = function(s,config,lang){
mid: queryInfo.mid,
ke: queryInfo.ke,
time: queryInfo.time,
filename: queryInfo.filename,
filename: queryInfo.filename,
details: s.s({
type : 'whcs',
location : saveLocation
@ -232,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',
@ -243,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'",

View File

@ -94,7 +94,7 @@ module.exports = function(s,config,lang){
}),
size: k.filesize,
end: k.endTime,
href: webdavRemoteUrl
href: ''
}
})
s.setCloudDiskUsedForGroup(e.ke,{
@ -157,6 +157,61 @@ module.exports = function(s,config,lang){
}
}
}
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'){
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.aws_s3_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',
@ -167,6 +222,9 @@ module.exports = function(s,config,lang){
cloudDiskUseStartupExtensions: cloudDiskUseStartupForWebDav,
beforeAccountSave: beforeAccountSaveForWebDav,
onAccountSave: cloudDiskUseStartupForWebDav,
onInsertTimelapseFrame,
onDeleteTimelapseFrameFromCloud,
onGetVideoData
})
return {
"evaluation": "details.use_webdav !== '0'",

View File

@ -219,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

View File

@ -288,7 +288,10 @@ 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: "*",

View File

@ -1,6 +1,9 @@
const fs = require('fs')
const { spawn } = require('child_process')
module.exports = (s,config,lang) => {
const {
splitForFFPMEG,
} = require('../ffmpeg/utils.js')(s,config,lang)
// orphanedVideoCheck : new function
const checkIfVideoIsOrphaned = (monitor,videosDirectory,filename) => {
const response = {ok: true}
@ -64,10 +67,10 @@ module.exports = (s,config,lang) => {
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)
}
@ -210,9 +213,105 @@ 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 = {}
async 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()
}
})
}
})
}
return {
reEncodeVideoAndReplace,
stitchMp4Files,
orphanedVideoCheck: orphanedVideoCheck,
scanForOrphanedVideos: scanForOrphanedVideos,
cutVideoLength: cutVideoLength,
getVideosBasedOnTagFoundInMatrixOfAssociatedEvent: getVideosBasedOnTagFoundInMatrixOfAssociatedEvent,
}
}

View File

@ -80,6 +80,7 @@ module.exports = function(s,config,lang){
ext: e.ext,
status: 1,
details: s.s(k.details),
objects: k.objects || '',
size: k.filesize,
end: k.endTime,
}
@ -116,6 +117,9 @@ module.exports = function(s,config,lang){
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 = [...new Set(listOTags)].join(',');
k.stat = fs.statSync(k.dir+k.file)
k.filesize = k.stat.size
k.filesizeMB = parseFloat((k.filesize/1048576).toFixed(2))
@ -137,6 +141,7 @@ module.exports = function(s,config,lang){
ke: e.ke,
filename: k.filename,
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')
}
@ -156,6 +161,7 @@ module.exports = function(s,config,lang){
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

View File

@ -5,6 +5,9 @@ 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)
*/
@ -13,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"}
@ -100,9 +105,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
}
var form = s.getPostData(req) || {}
@ -171,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
@ -206,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)
@ -287,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){
@ -296,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)
})
@ -522,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({
@ -560,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]

View File

@ -30,6 +30,10 @@ module.exports = function(s,config,lang,app,io){
spawnSubstreamProcess,
destroySubstreamProcess,
} = require('./monitor/utils.js')(s,config,lang)
const {
reEncodeVideoAndReplace,
getVideosBasedOnTagFoundInMatrixOfAssociatedEvent,
} = require('./video/utils.js')(s,config,lang)
s.renderPage = function(req,res,paths,passables,callback){
passables.window = {}
passables.data = req.params
@ -461,6 +465,7 @@ module.exports = function(s,config,lang,app,io){
renderPage(config.renderPaths.super,{
config: config,
lang: lang,
define: s.getDefinitonFile(config.language),
$user: superLoginResponse.user,
customAutoLoad: s.customAutoLoadTree,
currentVersion: s.currentVersion,
@ -642,8 +647,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
}
@ -747,8 +766,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
}
@ -826,11 +859,19 @@ module.exports = function(s,config,lang,app,io){
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(
user.permissions.control_monitors === "0" ||
user.details.sub &&
user.details.allmonitors !== '1' &&
user.details.monitor_edit.indexOf(monitorId) === -1
isRestrictedApiKey && apiKeyPermissions.control_monitors_disallowed ||
isRestricted && !monitorPermissions[`${monitorId}_monitors`]
){
response.msg = user.lang['Not Permitted']
}else{
@ -855,64 +896,64 @@ module.exports = function(s,config,lang,app,io){
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 : 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
*/
@ -924,10 +965,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'
@ -952,11 +1010,7 @@ module.exports = function(s,config,lang,app,io){
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,{
@ -970,6 +1024,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([
@ -978,17 +1086,28 @@ 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 monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId)
const preliminaryValidationFailed = (
user.permissions.watch_videos === "0" ||
hasRestrictions &&
(!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1)
);
if(req.query.onlyCount === '1' && !preliminaryValidationFailed){
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'], events: []});
return
}
if(req.query.onlyCount === '1'){
const response = {ok: true}
s.knexQuery({
action: "count",
@ -1026,7 +1145,7 @@ module.exports = function(s,config,lang,app,io){
noFormat: true,
noCount: true,
rowName: 'events',
preliminaryValidationFailed: preliminaryValidationFailed
preliminaryValidationFailed: false
},(response) => {
res.end(s.prettyPrint(response))
})
@ -1042,10 +1161,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,
@ -1060,9 +1190,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)
@ -1072,51 +1200,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}
@ -1149,10 +1256,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",
@ -1239,12 +1342,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)
@ -1265,16 +1376,18 @@ 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(actualUrl).then(actual => {
fetch(r.href).then(actual => {
actual.headers.forEach((v, n) => res.setHeader(n, v));
actual.body.pipe(res);
})
@ -1294,12 +1407,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)
@ -1359,26 +1480,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 d = {
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{
Object.assign(d, {details: s.parseJSON(req.query.data)});
Object.assign(simulatedEvent, {details: s.parseJSON(req.query.data)});
}catch(err){
s.closeJsonResponse(res,{
ok: false,
@ -1388,13 +1518,19 @@ module.exports = function(s,config,lang,app,io){
}
}
// 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) {
Object.assign(d, {
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: req.query.plug,
reason: req.query.reason,
confidence: req.query.confidence,
name: req.query.name
plug,
reason,
confidence,
name,
}
});
}
@ -1405,51 +1541,21 @@ module.exports = function(s,config,lang,app,io){
})
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 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']
@ -1472,8 +1578,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
@ -1493,10 +1616,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,
@ -1512,11 +1652,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))
})
@ -1557,12 +1693,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]
@ -1615,11 +1763,25 @@ 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)
@ -1635,8 +1797,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: "*",
@ -1647,15 +1807,14 @@ 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;
var details = s.parseJSON(r.details) || {}
switch(req.params.mode){
case'fix':
response.ok = true;
s.video('fix',r)
response = await reEncodeVideoAndReplace(r)
break;
case'status':
r.f = 'video_edit'

View File

@ -10,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')
@ -21,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'])
@ -44,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');
@ -108,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){
@ -169,6 +202,11 @@ module.exports = function(s,config,lang,app){
*/
app.get([config.webPaths.apiPrefix+':auth/hls/:ke/:id/:file',config.webPaths.apiPrefix+':auth/hls/:ke/:id/:channel/:file'], 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.permissions.watch_stream==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){
@ -195,6 +233,11 @@ module.exports = function(s,config,lang,app){
*/
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){
@ -221,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, {
@ -245,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
@ -302,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'}

392
package-lock.json generated
View File

@ -16,10 +16,10 @@
"connection-tester": "^0.2.0",
"cws": "^2.0.0",
"digest-fetch": "^1.2.1",
"discord.js": "^13.6.0",
"ejs": "^2.5.5",
"discord.js": "^12.2.0",
"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": "^100.0.0",
@ -27,7 +27,7 @@
"jsonfile": "^3.0.1",
"knex": "^0.21.4",
"ldapauth-fork": "^5.0.2",
"moment": "^2.27.0",
"moment": "^2.29.4",
"mp4frag": "^0.2.0",
"mqtt": "^4.3.7",
"mysql": "^2.18.1",
@ -35,7 +35,7 @@
"node-fetch": "^2.6.7",
"node-onvif-events": "^2.0.5",
"node-ssh": "^12.0.4",
"node-telegram-bot-api": "^0.52.0",
"node-telegram-bot-api": "^0.58.0",
"nodemailer": "^6.4.11",
"pam-diff": "^1.1.0",
"path": "^0.12.7",
@ -44,7 +44,7 @@
"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": "^4.4.1",
@ -57,49 +57,24 @@
"shinobi": "camera.js"
}
},
"node_modules/@discordjs/builders": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.11.0.tgz",
"integrity": "sha512-ZTB8yJdJKrKlq44dpWkNUrAtEJEq0gqpb7ASdv4vmq6/mZal5kOv312hQ56I/vxwMre+VIkoHquNUAfnTbiYtg==",
"dependencies": {
"@sindresorhus/is": "^4.2.0",
"discord-api-types": "^0.26.0",
"ts-mixer": "^6.0.0",
"tslib": "^2.3.1",
"zod": "^3.11.6"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@discordjs/collection": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.4.0.tgz",
"integrity": "sha512-zmjq+l/rV35kE6zRrwe8BHqV78JvIh2ybJeZavBi5NySjWXqN3hmmAKg7kYMMXSeiWtSsMoZ/+MQi0DiQWy2lw==",
"engines": {
"node": ">=16.0.0",
"npm": ">=7.0.0"
}
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz",
"integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==",
"deprecated": "no longer supported"
},
"node_modules/@sapphire/async-queue": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.0.tgz",
"integrity": "sha512-z+CDw5X4UgIEpZL8KM+ThVx1i8V60HBg0l/oFewTNbQQeRDJHdVxHyJykv+SF1H+Rc8EkMS81VTWo95jVYgO/g==",
"engines": {
"node": ">=v14.0.0",
"npm": ">=7.0.0"
}
},
"node_modules/@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==",
"engines": {
"node": ">=10"
"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",
"mime-types": "^2.1.12"
},
"funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1"
"engines": {
"node": ">= 6"
}
},
"node_modules/@socket.io/base64-arraybuffer": {
@ -143,36 +118,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz",
"integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw=="
},
"node_modules/@types/node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==",
"dependencies": {
"@types/node": "*",
"form-data": "^3.0.0"
}
},
"node_modules/@types/node-fetch/node_modules/form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -1364,45 +1309,71 @@
"resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz",
"integrity": "sha1-eAqZyE59YAJgNhURxId2E78k9rs="
},
"node_modules/discord-api-types": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz",
"integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ==",
"engines": {
"node": ">=12"
}
},
"node_modules/discord.js": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.6.0.tgz",
"integrity": "sha512-tXNR8zgsEPxPBvGk3AQjJ9ljIIC6/LOPjzKwpwz8Y1Q2X66Vi3ZqFgRHYwnHKC0jC0F+l4LzxlhmOJsBZDNg9g==",
"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/builders": "^0.11.0",
"@discordjs/collection": "^0.4.0",
"@sapphire/async-queue": "^1.1.9",
"@types/node-fetch": "^2.5.12",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.26.0",
"form-data": "^4.0.0",
"node-fetch": "^2.6.1",
"ws": "^8.4.0"
"@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"
},
"engines": {
"node": ">=16.6.0",
"npm": ">=7.0.0"
"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/form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
},
"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": ">= 6"
"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": {
@ -3971,9 +3942,9 @@
}
},
"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",
@ -4536,6 +4507,31 @@
"node": ">= 0.6"
}
},
"node_modules/prism-media": {
"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.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"
},
"peerDependenciesMeta": {
"@discordjs/opus": {
"optional": true
},
"ffmpeg-static": {
"optional": true
},
"node-opus": {
"optional": true
},
"opusscript": {
"optional": true
}
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -5041,9 +5037,9 @@
}
},
"node_modules/shinobi-sound-detection": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/shinobi-sound-detection/-/shinobi-sound-detection-0.1.12.tgz",
"integrity": "sha512-HbHqHfJJmut4UWRlw+G1f+LIItycyLNdPW5oUd7g8mej+2PiSh0vUvt1ja/cryV0Hj+El9eZZp4xh9V/lFnA9A==",
"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",
@ -5798,16 +5794,6 @@
"tree-kill": "cli.js"
}
},
"node_modules/ts-mixer": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.1.tgz",
"integrity": "sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg=="
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -5819,6 +5805,11 @@
"node": "*"
}
},
"node_modules/tweetnacl": {
"version": "1.0.3",
"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",
@ -6499,43 +6490,23 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"node_modules/zod": {
"version": "3.14.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.14.3.tgz",
"integrity": "sha512-OzwRCSXB1+/8F6w6HkYHdbuWysYWnAF4fkRgKDcSFc54CE+Sv0rHXKfeNUReGCrHukm1LNpi6AYeXotznhYJbQ==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
},
"dependencies": {
"@discordjs/builders": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-0.11.0.tgz",
"integrity": "sha512-ZTB8yJdJKrKlq44dpWkNUrAtEJEq0gqpb7ASdv4vmq6/mZal5kOv312hQ56I/vxwMre+VIkoHquNUAfnTbiYtg==",
"requires": {
"@sindresorhus/is": "^4.2.0",
"discord-api-types": "^0.26.0",
"ts-mixer": "^6.0.0",
"tslib": "^2.3.1",
"zod": "^3.11.6"
}
},
"@discordjs/collection": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.4.0.tgz",
"integrity": "sha512-zmjq+l/rV35kE6zRrwe8BHqV78JvIh2ybJeZavBi5NySjWXqN3hmmAKg7kYMMXSeiWtSsMoZ/+MQi0DiQWy2lw=="
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz",
"integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ=="
},
"@sapphire/async-queue": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.3.0.tgz",
"integrity": "sha512-z+CDw5X4UgIEpZL8KM+ThVx1i8V60HBg0l/oFewTNbQQeRDJHdVxHyJykv+SF1H+Rc8EkMS81VTWo95jVYgO/g=="
},
"@sindresorhus/is": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz",
"integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="
"@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==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
},
"@socket.io/base64-arraybuffer": {
"version": "1.0.2",
@ -6575,35 +6546,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.7.tgz",
"integrity": "sha512-QB5D2sqfSjCmTuWcBWyJ+/44bcjO7VbjSbOE0ucoVbAsSNQc4Lt6QkgkVXkTDwkL4z/beecZNDvVX15D4P8Jbw=="
},
"@types/node-fetch": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.1.tgz",
"integrity": "sha512-oMqjURCaxoSIsHSr1E47QHzbmzNR5rK8McHuNb11BOM9cHcIK3Avy0s/b2JlXHoQGTYS3NsvWzV1M0iK7l0wbA==",
"requires": {
"@types/node": "*",
"form-data": "^3.0.0"
},
"dependencies": {
"form-data": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"@types/ws": {
"version": "8.5.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
"integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==",
"requires": {
"@types/node": "*"
}
},
"abort-controller": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
@ -7523,36 +7465,26 @@
}
}
},
"discord-api-types": {
"version": "0.26.1",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.26.1.tgz",
"integrity": "sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ=="
},
"discord.js": {
"version": "13.6.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-13.6.0.tgz",
"integrity": "sha512-tXNR8zgsEPxPBvGk3AQjJ9ljIIC6/LOPjzKwpwz8Y1Q2X66Vi3ZqFgRHYwnHKC0jC0F+l4LzxlhmOJsBZDNg9g==",
"version": "12.2.0",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.2.0.tgz",
"integrity": "sha512-Ueb/0SOsxXyqwvwFYFe0msMrGqH1OMqpp2Dpbplnlr4MzcRrFWwsBM9gKNZXPVBHWUKiQkwU8AihXBXIvTTSvg==",
"requires": {
"@discordjs/builders": "^0.11.0",
"@discordjs/collection": "^0.4.0",
"@sapphire/async-queue": "^1.1.9",
"@types/node-fetch": "^2.5.12",
"@types/ws": "^8.2.2",
"discord-api-types": "^0.26.0",
"form-data": "^4.0.0",
"node-fetch": "^2.6.1",
"ws": "^8.4.0"
"@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": {
"form-data": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
"ws": {
"version": "7.5.9",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
"integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
"requires": {}
}
}
},
@ -9492,9 +9424,9 @@
}
},
"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",
@ -9934,6 +9866,12 @@
"resolved": "https://registry.npmjs.org/precond/-/precond-0.2.3.tgz",
"integrity": "sha1-qpWRvKokkj8eD0hJ0kD0fvwQdaw="
},
"prism-media": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.3.4.tgz",
"integrity": "sha512-eW7LXORkTCQznZs+eqe9VjGOrLBxcBPXgNyHXMTSRVhphvd/RrxgIR7WaWt4fkLuhshcdT5KHL88LAfcvS3f5g==",
"requires": {}
},
"process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
@ -10337,9 +10275,9 @@
}
},
"shinobi-sound-detection": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/shinobi-sound-detection/-/shinobi-sound-detection-0.1.12.tgz",
"integrity": "sha512-HbHqHfJJmut4UWRlw+G1f+LIItycyLNdPW5oUd7g8mej+2PiSh0vUvt1ja/cryV0Hj+El9eZZp4xh9V/lFnA9A==",
"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",
@ -10917,16 +10855,6 @@
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="
},
"ts-mixer": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.1.tgz",
"integrity": "sha512-hvE+ZYXuINrx6Ei6D6hz+PTim0Uf++dYbK9FFifLNwQj+RwKquhQpn868yZsCtJYiclZF1u8l6WZxxKi+vv7Rg=="
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
},
"tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -10935,6 +10863,11 @@
"safe-buffer": "^5.0.1"
}
},
"tweetnacl": {
"version": "1.0.3",
"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",
@ -11523,11 +11456,6 @@
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz",
"integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk="
},
"zod": {
"version": "3.14.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.14.3.tgz",
"integrity": "sha512-OzwRCSXB1+/8F6w6HkYHdbuWysYWnAF4fkRgKDcSFc54CE+Sv0rHXKfeNUReGCrHukm1LNpi6AYeXotznhYJbQ=="
}
}
}

View File

@ -22,10 +22,10 @@
"connection-tester": "^0.2.0",
"cws": "^2.0.0",
"digest-fetch": "^1.2.1",
"discord.js": "^13.6.0",
"ejs": "^2.5.5",
"discord.js": "^12.2.0",
"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": "^100.0.0",
@ -33,7 +33,7 @@
"jsonfile": "^3.0.1",
"knex": "^0.21.4",
"ldapauth-fork": "^5.0.2",
"moment": "^2.27.0",
"moment": "^2.29.4",
"mp4frag": "^0.2.0",
"mqtt": "^4.3.7",
"mysql": "^2.18.1",
@ -41,7 +41,7 @@
"node-fetch": "^2.6.7",
"node-onvif-events": "^2.0.5",
"node-ssh": "^12.0.4",
"node-telegram-bot-api": "^0.52.0",
"node-telegram-bot-api": "^0.58.0",
"nodemailer": "^6.4.11",
"pam-diff": "^1.1.0",
"path": "^0.12.7",
@ -50,7 +50,7 @@
"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": "^4.4.1",

View File

@ -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"

View File

@ -20,8 +20,8 @@
"random-seedable": "^1.0.8",
"seedrandom": "2.4.3",
"rimraf": "^3.0.2",
"socket.io": "2.0.4",
"socket.io-client": "1.7.4",
"socket.io": "^4.4.1",
"socket.io-client": "^4.4.1",
"xor128": "^0.1.0"
},
"bin": "shinobi-tensorflow.js",

View File

@ -5,14 +5,14 @@
"description": "Object Detection plugin based on @tensorflow/tfjs-node",
"main": "shinobi-tensorflow.js",
"dependencies": {
"@tensorflow-models/coco-ssd": "^2.2.2",
"@tensorflow/tfjs-converter": "^3.13.0",
"@tensorflow/tfjs-core": "^3.13.0",
"@tensorflow/tfjs-layers": "^3.13.0",
"@tensorflow/tfjs-node": "^3.13.0",
"@tensorflow/tfjs-node-gpu": "^3.13.0",
"@tensorflow/tfjs-backend-cpu": "^3.13.0",
"@tensorflow/tfjs-backend-webgl": "^3.13.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",

View File

@ -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)

View File

@ -7,4 +7,5 @@
width: 100%;
}
#region_editor_live iframe,.canvas_holder canvas{border:0;position:absolute;left:0;top:0}
#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%;}

View File

@ -26,7 +26,7 @@
border-radius: 5px
}
.tab-videoPlayer:hover .tab-videoPlayer-event-objects {
.tab-videoPlayer-view-container:hover .tab-videoPlayer-event-objects {
display: none;
}
@ -68,11 +68,16 @@
top: 0;
}
.video-time-needle-event {
border-left: 3px solid rgb(249 242 31 / 20%);
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);

View File

@ -0,0 +1,4 @@
.video-thumbnail img {
height: 75px;
border-radius: 10px;
}

View File

@ -430,3 +430,8 @@ ul.squeeze {
.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;
}

BIN
web/assets/img/splash.avif Normal file

Binary file not shown.

View File

@ -1,4 +1,5 @@
PNotify.prototype.options.styling = "fontawesome";
var isSubAccount = !!$user.details.sub
var loadedMonitors = {}
var tabTree = null
var pageLoadingData = {}
@ -679,7 +680,7 @@ function permissionCheck(toCheck,monitorId){
return false
}
function drawMonitorListToSelector(jqTarget,selectFirst,showId){
function drawMonitorListToSelector(jqTarget,selectFirst,showId,addAllMonitorsOption){
var html = ''
$.each(loadedMonitors,function(n,v){
html += createOptionHtml({
@ -687,7 +688,10 @@ function drawMonitorListToSelector(jqTarget,selectFirst,showId){
label: v.name + (showId ? ` (${v.mid})` : ''),
})
})
jqTarget.html(html)
addAllMonitorsOption ? jqTarget.html(`
<option value="">${lang['All Monitors']}</option>
<optgroup label="${lang.Monitors}">${html}</optgroup>
`) : jqTarget.html(html);
if(selectFirst){
jqTarget
.find('option')
@ -812,6 +816,12 @@ function downloadJSON(jsonData,filename){
.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'
@ -819,21 +829,23 @@ function convertKbToHumanSize(theNumber){
return `${number} ${unit}`
}
function drawIndicatorBar(item){
var html = `<div id="indicator-${item.name}" class="mb-2">
<div class="d-flex flex-row text-white mb-1">
<div class="pr-2">
<i class="fa fa-${item.icon}"></i>
</div>
<div class="flex-grow-1">
<small>${item.label}</small>
var html = `<div class="box-wrapper " style>
<div id="indicator-${item.name}" class="mb-2">
<div class="d-flex flex-row text-white mb-1">
<div class="pr-2">
<i class="fa fa-${item.icon}"></i>
</div>
<div class="flex-grow-1">
<small>${item.label}</small>
</div>
<div>
<small class="indicator-percent">0%</small>
</div>
</div>
<div>
<small class="indicator-percent">0%</small>
</div>
</div>
<div>
<div class="progress">
<div class="progress-bar progress-bar-warning" role="progressbar" style="width: 0%;"></div>
<div class="progress">
<div class="progress-bar progress-bar-warning" role="progressbar" style="width: 0%;"></div>
</div>
</div>
</div>
</div>`

View File

@ -75,6 +75,7 @@ $(document).ready(function(e){
}
],
data: data.files.map((file) => {
var href = getApiPrefix('fileBin') + '/' + selectedMonitor + '/' + file.name
return {
monitorName: `<b>${loadedMonitors[file.mid]?.name || file.mid}</b>`,
name: file.name,
@ -85,8 +86,8 @@ $(document).ready(function(e){
`,
size: convertKbToHumanSize(file.size),
buttons: `
<a class="btn btn-sm btn-primary" href="${file.href}" download title="${lang.Download}"><i class="fa fa-download"></i></a>
${file.details.video ? `<a class="btn btn-sm btn-primary preview-video" href="${file.href}" title="${lang.Play}"><i class="fa fa-play"></i></a>` : ``}
<a class="btn btn-sm btn-primary" href="${href}" download title="${lang.Download}"><i class="fa fa-download"></i></a>
${file.details.video ? `<a class="btn btn-sm btn-primary preview-video" href="${href}" title="${lang.Play}"><i class="fa fa-play"></i></a>` : ``}
`,
}
})

View File

@ -75,6 +75,6 @@ $(document).ready(function(){
dontShowForOneWeek()
return false;
})
console.log('Please support the Shinobi developement.')
console.log('Please support the Shinobi development.')
console.log('https://licenses.shinobi.video/subscribe')
})

View File

@ -128,6 +128,7 @@ function buildLiveGridBlock(monitor){
})
return
}
var monitorId = monitor.mid
var monitorDetails = safeJsonParse(monitor.details)
var monitorLiveId = `monitor_live_${monitor.mid}`
var subStreamChannel = monitor.subStreamChannel
@ -135,6 +136,11 @@ function buildLiveGridBlock(monitor){
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 += `<a title="${button.label}" class="btn btn-sm mr-1 badge btn-${button.class}"><i class="fa fa-${button.icon}"></i></a>`
})
var baseHtml = `<div
id="${monitorLiveId}"
data-ke="${monitor.ke}"
@ -155,24 +161,13 @@ function buildLiveGridBlock(monitor){
${streamElement}
</div>
</div>
${streamBlockInfo.gridBlockAfterContentHtml || ''}
${(streamBlockInfo.gridBlockAfterContentHtml || '').replace(`$QUICKLINKS`,quickLinkHtml)}
<div class="mdl-overlay-menu-backdrop hidden">
<ul class="mdl-overlay-menu list-group">`
var buttons = streamBlockInfo.links
if(!monitor.details.control === '1'){
delete(buttons["Control"])
}
if(!permissionCheck('video_view',monitor.mid)){
delete(buttons["Videos List"])
delete(buttons["Time-lapse"])
delete(buttons["Power Viewer"])
delete(buttons["Calendar"])
}
if(!permissionCheck('monitor_edit',monitor.mid)){
delete(buttons["Monitor Settings"])
}
$.each(buttons,function(n,v){
baseHtml += `<li class="list-item cursor-pointer ${v.class}" title="${v.label}" ${v.attr}><i class="fa fa-${v.icon}"></i> ${v.label}</li>`
$.each(buttons,function(n,button){
if(button.eval && !eval(button.eval))return;
baseHtml += `<li class="list-item cursor-pointer ${button.class}" title="${button.label}" ${button.attr}><i class="fa fa-${button.icon}"></i> ${button.label}</li>`
})
baseHtml += `</ul>
</div>
@ -182,15 +177,17 @@ function buildLiveGridBlock(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 = `<div class="PTZ_controls">
<div class="pad">
<div class="control top run-live-grid-monitor-ptz" data-ptz-control="up"></div>
<div class="control left run-live-grid-monitor-ptz" data-ptz-control="left"></div>
<div class="control right run-live-grid-monitor-ptz" data-ptz-control="right"></div>
<div class="control bottom run-live-grid-monitor-ptz" data-ptz-control="down"></div>
<div class="control top run-live-grid-monitor-ptz${stopCommandOnRelease ? `-move` : '' }" data-ptz-control="up"></div>
<div class="control left run-live-grid-monitor-ptz${stopCommandOnRelease ? `-move` : '' }" data-ptz-control="left"></div>
<div class="control right run-live-grid-monitor-ptz${stopCommandOnRelease ? `-move` : '' }" data-ptz-control="right"></div>
<div class="control bottom run-live-grid-monitor-ptz${stopCommandOnRelease ? `-move` : '' }" data-ptz-control="down"></div>
<div class="control middle run-live-grid-monitor-ptz" data-ptz-control="center"></div>
</div>
<div class="btn-group btn-group-sm btn-group-justified">
@ -808,8 +805,9 @@ function signalCheckLiveStream(options){
}
$(document).ready(function(e){
liveGrid
.on('dblclick','.stream-hud',function(){
$(this).parents('[data-mid]').find('[monitor="fullscreen"]').click();
.on('dblclick','.stream-block',function(){
var monitorItem = $(this).parents('[data-mid]');
fullScreenLiveGridStream(monitorItem)
})
$('body')
.resize(function(){
@ -898,6 +896,18 @@ $(document).ready(function(e){
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')
@ -936,7 +946,7 @@ $(document).ready(function(e){
},700)
})
.on('gsresizestop', function(){
resetAllLiveGridDimensionsInMemory()
// resetAllLiveGridDimensionsInMemory()
saveLiveGridBlockPositions()
});
addOnTabReopen('liveGrid', function () {
@ -956,7 +966,10 @@ $(document).ready(function(e){
case'video_build_success':
d.status = 1
d.mid = d.id || d.mid
if(liveGridElements[d.mid] && liveGridElements[d.mid].streamElement)drawVideoCardToMiniList(d.mid,createVideoLinks(d),false)
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
@ -1091,9 +1104,9 @@ $(document).ready(function(e){
}
dashboardSwitchCallbacks.dontShowDetection = function(toggleState){
if(toggleState !== 1){
window.dontShowDetection = true
}else{
window.dontShowDetection = false
}else{
window.dontShowDetection = true
}
}
dashboardSwitchCallbacks.monitorMuteAudio = function(toggleState){

View File

@ -7,14 +7,17 @@ function generateId(x){
t += p.charAt(Math.floor(Math.random() * p.length));
return t;
}
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 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)
@ -22,7 +25,10 @@ if(!cachedMachineId){
}
$(document).ready(function(){
$('#machineID').val(cachedMachineId)
$('[selector]').change(onSelectorChange).change();
$('[selector]').change(function(){
var el = $(this)
onSelectorChange(el)
}).change();
})
loginForm.submit(function(e){
$('#login-message').remove()

View File

@ -12,6 +12,7 @@ var copySettingsSelector = $('#copy_settings')
var monitorPresetsSelection = $('#monitorPresetsSelection')
var monitorPresetsNameField = $('#monitorPresetsName')
var monitorGroupSelectors = $('#monitor_groups')
var monitorGroupMutliTriggerSelectContainer = $('#monitor_group_detector_multi')
var editorForm = monitorEditorWindow.find('form')
var fieldsLoaded = {}
var sections = {}
@ -134,7 +135,6 @@ function generateDefaultMonitorSettings(){
"detector_scale_y": "480",
"detector_record_method": "sip",
"detector_trigger": "1",
"detector_trigger_record_fps": "",
"detector_timeout": "0.5",
"detector_send_video_length": "",
"watchdog_reset": "1",
@ -287,6 +287,14 @@ function getMonitorGroupsSelected(){
})
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 = {}
@ -408,6 +416,7 @@ window.getMonitorEditFormFields = function(){
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)
@ -437,12 +446,12 @@ function getAdditionalStreamChannelFields(tempID,channelId){
}
addOnTabOpen('monitorSettings', function () {
triggerSecondaryHideCheckOnAll()
setFieldVisibility()
drawMonitorSettingsSubMenu()
})
addOnTabReopen('monitorSettings', function () {
triggerSecondaryHideCheckOnAll()
setFieldVisibility()
drawMonitorSettingsSubMenu()
})
function drawInputMapHtml(options){
@ -649,7 +658,7 @@ function importIntoMonitorEditor(options){
${v.name} <span class="text-muted">(${v.id})</span>
</div>
<div class="pr-3">
<span><input class="form-check-input no-abs mdl-switch__input form-check-input" type="checkbox" value="${v.id}" ${isSelected ? 'checked' : ''}/></span>
<span><input class="form-check-input no-abs" ${b} type="checkbox" value="${v.id}" ${isSelected ? 'checked' : ''}/></span>
</div>
</div>
</div>`
@ -675,7 +684,7 @@ function importIntoMonitorEditor(options){
}
})
monitorsForCopy.find('optgroup').html(tmp)
triggerSecondaryHideCheckOnAll()
setFieldVisibility()
drawMonitorSettingsSubMenu()
}
//parse "Automatic" field in "Input" Section
@ -897,27 +906,65 @@ var showInputMappingFields = function(showMaps){
}else{
el.hide()
}
triggerSecondaryHideCheckOnAll()
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()
@ -927,49 +974,6 @@ 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=safeJsonParse(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-monitor-settings').click(function(){
$.pB.submit(buildMonitorURL(),true)
})
@ -1035,7 +1039,8 @@ editorForm.find('[detail]').change(function(){
})
editorForm.on('change','[selector]',function(){
var el = $(this);
triggerSecondaryHideCheck(el)
onSelectorChange(el,editorForm)
setFieldVisibilityNewWay()
drawMonitorSettingsSubMenu()
});
editorForm.find('[name="type"]').change(function(e){
@ -1073,7 +1078,7 @@ editorForm.find('[name="type"]').change(function(e){
$('.shinobi-detector_name').empty()
$('.shinobi-detector_plug').hide()
$('.shinobi-detector-invert').show()
triggerSecondaryHideCheckOnAll()
setFieldVisibility()
drawMonitorSettingsSubMenu()
}else{
var pluginTitle = []
@ -1089,7 +1094,7 @@ 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('<br>'))
triggerSecondaryHideCheckOnAll()
setFieldVisibility()
drawMonitorSettingsSubMenu()
}
}
@ -1128,7 +1133,7 @@ editorForm.find('[name="type"]').change(function(e){
${hasSelectedMonitor ? `<ul class="json-to-block striped import-monitor-preset cursor-pointer">${jsonToHtmlBlock(humanizedMonitorKeys)}</ul>` : ''}
</div>
<div class="pr-3">
<span><input class="form-check-input no-abs mdl-switch__input" type="checkbox" value="${preset.name}" ${hasSelectedMonitor ? 'checked' : ''}/></span>
<span><input class="form-check-input no-abs" type="checkbox" value="${preset.name}" ${hasSelectedMonitor ? 'checked' : ''}/></span>
</div>
<div>
<a class="badge btn btn-sm btn-danger delete-preset"><i class="fa fa-trash-o"></i></a>
@ -1440,7 +1445,7 @@ editorForm.find('[name="type"]').change(function(e){
var monitorConfig = validation.monitorConfig
if(loadedMonitors[monitorConfig.mid]){
deleteMonitors([monitorConfig],function(){
resetMonitorEditor()
openMonitorEditorPage()
goBackOneTab()
})
}else{

View File

@ -155,8 +155,7 @@ function runPtzCommand(monitorId,switchChosen){
break;
default:
mainSocket.f({
f: 'monitor',
ff: 'control',
f: 'control',
direction: switchChosen,
id: monitorId,
ke: $user.ke
@ -164,6 +163,14 @@ function runPtzCommand(monitorId,switchChosen){
break;
}
}
function runPtzMove(monitorId,switchChosen,doMove){
mainSocket.f({
f: doMove ? 'startMove' : 'stopMove',
direction: switchChosen,
id: monitorId,
ke: $user.ke
})
}
function runTestDetectionTrigger(monitorId,callback){
$.getJSON(getApiPrefix() + '/motion/'+$user.ke+'/'+monitorId+'/?data={"plug":"manual_trigger","name":"Manual Trigger","reason":"Manual","confidence":100}',function(d){
debugLog(d)

View File

@ -32,7 +32,7 @@ $(document).ready(function(){
},
{
value: 'onDetectorNoTriggerTimeout',
label: lang['Detection Event'],
label: lang['No Trigger'],
},
{
value: 'onAccountSave',

View File

@ -300,11 +300,14 @@ $(document).ready(function(){
submitTheForm()
return false;
})
onWebSocketEvent(function(d){
switch(d.f){
case'init_success':
drawMonitorListToSelector(monitorsList)
break;
}
addOnTabOpen('onvifDeviceManager', function () {
drawMonitorListToSelector(monitorsList)
var monitorId = monitorsList.val()
openOnvifDeviceManager(monitorId)
})
addOnTabReopen('onvifDeviceManager', function () {
drawMonitorListToSelector(monitorsList)
var monitorId = monitorsList.val()
openOnvifDeviceManager(monitorId)
})
})

View File

@ -53,6 +53,7 @@ $(document).ready(function(e){
var theLocation = getLocationFromUri(options.uri)
var pathLocation = theLocation.location
var monitorConfigPartial = {
name: pathLocation.hostname,
mid: tempID + `${options.port}`,
host: pathLocation.hostname,
port: pathLocation.port,

View File

@ -16,6 +16,7 @@ $(document).ready(function(e){
var monitorSlotPlaySpeeds = {}
var currentlyPlayingVideos = {}
var powerVideoMute = true
var powerVideoCanAutoPlay = true
var lastPowerVideoSelectedMonitors = []
var extenders = {
onVideoPlayerTimeUpdateExtensions: [],
@ -48,6 +49,11 @@ $(document).ready(function(e){
});
// fix utc/localtime translation (use timelapseJpeg as guide, it works as expected) />
function loadVideosToTimeLineMemory(monitorId,videos,events){
videos.forEach((video) => {
createVideoLinks(video,{
hideRemote: true
})
})
powerVideoLoadedVideos[monitorId] = videos
powerVideoLoadedEvents[monitorId] = events
}
@ -357,6 +363,15 @@ $(document).ready(function(e){
// .off("play").on("play",function(){
// console.log(monitorId,'play')
// })
.off("loadedmetadata").on("loadedmetadata",function(){
resetWidthForActiveVideoPlayers()
})
.off("pause").on("pause",function(){
resetWidthForActiveVideoPlayers()
})
.off("play").on("play",function(){
resetWidthForActiveVideoPlayers()
})
.off("timeupdate").on("timeupdate",function(){
try{
var event = eventsLabeledByTime[monitorId][video.time][parseInt(this.currentTime)]
@ -439,6 +454,14 @@ $(document).ready(function(e){
motionMeterProgressBar.css('width','0')
motionMeterProgressBarTextBox.text('0')
}
function resetWidthForActiveVideoPlayers(){
var numberOfMonitors = 0
powerVideoMonitorViewsElement.find(`.videoPlayer .videoNow`).each(function(n,videoEl){
if(videoEl.currentTime > 0)numberOfMonitors += 1
})
var widthOfBlock = 100 / numberOfMonitors
powerVideoMonitorViewsElement.find('.videoPlayer').css('width',`${widthOfBlock}%`)
}
function loadVideoIntoMonitorSlot(video,selectedTime){
if(!video)return
resetVisualDetectionDataForMonitorSlot(video.mid)
@ -529,8 +552,15 @@ $(document).ready(function(e){
videoNow.setAttribute('preload',true)
videoNow.muted = true
videoNow.playbackRate = monitorSlotPlaySpeeds[video.mid] || 1
videoNow.currentTime = timeToStartAt / 1000
try{
videoNow.currentTime = timeToStartAt / 1000
}catch(err){
console.log(err)
}
videoNow.play()
setTimeout(function(){
resetWidthForActiveVideoPlayers()
},1400)
extenders.onVideoPlayerCreateExtensions.forEach(function(extender){
extender(videoElement,watchPoint)
})
@ -678,9 +708,11 @@ $(document).ready(function(e){
toggleZoomAllSlots()
break;
case'playAll':
powerVideoCanAutoPlay = true
playAllSlots()
break;
case'pauseAll':
powerVideoCanAutoPlay = false
pauseAllSlots()
break;
case'playSpeedAll':
@ -701,6 +733,27 @@ $(document).ready(function(e){
addOnTabOpen('powerVideo', function () {
drawMonitorsList()
})
addOnTabReopen('powerVideo', function () {
if(powerVideoCanAutoPlay){
powerVideoWindow.find('video').each(function(n,video){
try{
video.play()
}catch(err){
console.log(err)
}
})
}
})
addOnTabAway('powerVideo', function () {
powerVideoWindow.find('video').each(function(n,video){
console.log(video)
try{
video.pause()
}catch(err){
console.log(err)
}
})
})
// addOnTabReopen('powerVideo', function () {
// drawMonitorsList()
// })

View File

@ -11,25 +11,28 @@ $(document).ready(function(){
}
}
function drawDaysToList(videos,toBegin,frames){
var videosSortedByDays = sortVideosByDays(videos)
var framesSortedByDays = sortFramesByDays(frames)
$.each(videosSortedByDays,function(monitorId,days){
if(!framesSortedByDays[monitorId])framesSortedByDays[monitorId] = {};
$.each(days,function(dayKey,videos){
var copyOfVideos = ([]).concat(videos).reverse()
var listOfDays = getAllDays(videos,frames)
var videosSortedByDays = Object.assign({},listOfDays,sortVideosByDays(videos))
var framesSortedByDays = Object.assign({},listOfDays,sortFramesByDays(frames))
$.each(listOfDays,function(monitorId,days){
$.each(days,function(dayKey){
var copyOfVideos = ([]).concat(videosSortedByDays[monitorId][dayKey] || []).reverse()
var copyOfFrames = ([]).concat(framesSortedByDays[monitorId][dayKey] || []).reverse()
theList.append(createDayCard(copyOfVideos,dayKey,monitorId))
theList.append(createDayCard(copyOfVideos,copyOfFrames,dayKey,monitorId))
var theChildren = theList.children()
var createdCardCarrier = toBegin ? theChildren.first() : theChildren.last()
bindFrameFindingByMouseMoveForDay(createdCardCarrier,dayKey,copyOfVideos,copyOfFrames)
// preloadAllTimelapseFramesToMemoryFromVideoList(framesSortedByDays)
// preloadAllTimelapseFramesToMemoryFromVideoList(copyOfFrames)
})
})
}
function drawListFiller(filler){
theList.html(`<div class="text-center ${definitions.Theme.isDark ? 'text-white' : ''} pt-4"><h3>${filler}</h3></div>`);
}
function loadVideos(options,callback){
theList.empty();
drawListFiller(`<i class="fa fa-spinner fa-pulse"></i>`)
var currentDate = new Date()
var videoRange = parseInt(videoRangeEl.val()) || 72
var videoRange = parseInt(videoRangeEl.val()) || 1
options.videoRange = videoRange
options.startDate = moment(currentDate).subtract(videoRange, 'hours')._d;
options.endDate = moment(currentDate)._d;
@ -46,11 +49,9 @@ $(document).ready(function(){
callback(data)
}
getVideos(options,function(data){
if(data.videos.length === 0){
options.limit = 20
delete(options.startDate)
delete(options.endDate)
getVideos(options,drawVideoData)
theList.empty()
if(data.videos.length === 0 && data.frames.length === 0){
drawListFiller(lang['No Data'])
}else{
drawVideoData(data)
}

View File

@ -8,6 +8,7 @@ $(document).ready(function(e){
var regionEditorCanvasHolder = regionEditorWindow.find('.canvas_holder')
var regionEditorMonitorsList = $('#region_editor_monitors')
var regionEditorLiveView = $('#region_editor_live')
var regionEditorGridOverlay = regionEditorWindow.find('.grid')
var useRegionStillImage = false
var getRegionEditorCanvas = function(){
return regionEditorWindow.find('canvas')
@ -113,25 +114,44 @@ $(document).ready(function(e){
}
function initLiveStream(monitorId){
var monitorId = monitorId || getCurrentlySelectedMonitorId()
var apiPoint = 'embed'
var liveElement = regionEditorLiveView.find('iframe,img')
regionEditorLiveView.find('iframe,img').attr('src','').hide()
if(useRegionStillImage){
apiPoint = 'jpeg'
}else{
apiPoint = 'embed'
}
var apiUrl = `${getApiPrefix(apiPoint)}/${monitorId}`
if(apiPoint === 'embed'){
apiUrl += '/fullscreen|jquery|relative'
}else{
apiUrl += '/s.jpg'
}
if(liveElement.attr('src') !== apiUrl){
var loadedMonitor = loadedMonitors[monitorId]
var monitorDetails = loadedMonitor.details
if(loadedMonitor.mode === 'stop'){
var apiUrl = placeholder.getData(placeholder.plcimg({
bgcolor: '#000',
text: lang[`Cannot watch a monitor that isn't running.`],
size: regionViewerDetails.detector_scale_x+'x'+regionViewerDetails.detector_scale_y
}))
liveElement.attr('src',apiUrl).show()
}else{
var apiPoint = 'embed'
regionEditorLiveView.find('iframe,img').attr('src','').hide()
regionEditorGridOverlay.css('background-size',`71px 71px`).show()
if(monitorDetails.detector_motion_tile_mode === '1'){
var tileSize = monitorDetails.detector_tile_size || 20
regionEditorGridOverlay.css('background-size',`${tileSize}px ${tileSize}px`).show()
}else{
regionEditorGridOverlay.hide()
}
if(useRegionStillImage){
apiPoint = 'jpeg'
}else{
apiPoint = 'embed'
}
var apiUrl = `${getApiPrefix(apiPoint)}/${monitorId}`
if(apiPoint === 'embed'){
apiUrl += '/fullscreen|jquery|relative'
}else{
apiUrl += '/s.jpg'
}
if(liveElement.attr('src') !== apiUrl){
liveElement.attr('src',apiUrl).show()
}
liveElement.attr('width',regionViewerDetails.detector_scale_x)
liveElement.attr('height',regionViewerDetails.detector_scale_y)
}
liveElement.attr('width',regionViewerDetails.detector_scale_x)
liveElement.attr('height',regionViewerDetails.detector_scale_y)
}
var initCanvas = function(dontReloadStream){
var newArray = [];
@ -302,5 +322,8 @@ $(document).ready(function(e){
drawMonitorListToSelector(regionEditorMonitorsList)
regionEditorMonitorsList.val(theSelected)
})
addOnTabAway('regionEditor', function () {
regionEditorLiveView.find('iframe,img').remove()
})
drawSubMenuItems('regionEditor',definitions['Region Editor'])
})

View File

@ -12,7 +12,6 @@ function buildTabHtml(tabName,tabLabel,tabIcon){
<i class="fa fa-${tabIcon ? tabIcon : 'file-o'}"></i> &nbsp; ${tabLabel} <span class="delete-tab align-text-bottom"><i class="fa fa-times"></i></span>
</a>
</li>`
return `<a class="nav-link" page-open="${tabName}">${tabLabel} &nbsp; <span class="delete-tab badge btn bg-light text-dark rounded-pill align-text-bottom"><i class="fa fa-times"></i></span></a>`
}
function drawMonitorIconToMenu(item){
var html = `<li class="nav-item monitor-icon monitor_block glM${item.mid}" data-ke="${item.ke}" data-mid="${item.mid}" data-status-code="${item.code}">

View File

@ -95,7 +95,7 @@ function drawAddStorageIndicators(){
var el = $(`#indicator-${storage.name}`)
loadedIndicators[storage.name] = {
value: el.find('.value'),
percent: el.find('.percent'),
percent: el.find('.indicator-percent'),
progressBar: el.find('.progress-bar'),
}
})
@ -143,6 +143,9 @@ $('body')
}
_this.attr('type',type)
})
function parseDiskUsePercent(diskUsed,diskLimit){
return parseFloat((diskUsed/diskLimit)*100).toFixed(1)+'%'
}
onWebSocketEvent(function (d){
switch(d.f){
case'init_success':
@ -173,7 +176,10 @@ onWebSocketEvent(function (d){
case'diskUsed':
var diskLimit = d.limit || 10000
var diskUsed = d.size
var percent = parseInt((diskUsed/diskLimit)*100)+'%';
var percent = parseDiskUsePercent(diskUsed,diskLimit);
var videosPercent = parseDiskUsePercent(d.usedSpaceVideos,diskLimit);
var timelapsePercent = parseDiskUsePercent(d.usedSpaceTimelapseFrames,diskLimit);
var fileBinPercent = parseDiskUsePercent(d.usedSpaceFilebin,diskLimit);
var humanText = parseFloat(diskUsed)
if(humanText > 1000){
humanText = (humanText / 1000).toFixed(2) + ' GB'
@ -182,7 +188,12 @@ onWebSocketEvent(function (d){
}
diskIndicatorBarUsed.html(humanText)
diskIndicatorPercentText.html(percent)
diskIndicatorBar.css('width',percent)
diskIndicatorBar[0].style.width = videosPercent
diskIndicatorBar[0].title = `${lang['Video Share']} : ${videosPercent}`
diskIndicatorBar[1].style.width = timelapsePercent
diskIndicatorBar[1].title = `${lang['Timelapse Frames Share']} : ${timelapsePercent}`
diskIndicatorBar[2].style.width = fileBinPercent
diskIndicatorBar[2].title = `${lang['FileBin Share']} : ${fileBinPercent}`
if(d.addStorage){
$.each(d.addStorage,function(n,storage){
var percent = parseInt((storage.usedSpace/storage.sizeLimit)*100)+'%'

View File

@ -209,13 +209,18 @@ $(document).ready(function(){
permissionsSection.find('.permission-view select').each(function(n,v){
var el = $(v)
var monitorId = el.attr('monitor')
var value = el.val()
var value = el.val() // permissions selected
$.each(value,function(n,permissionNameSelected){
if(!foundSelected[permissionNameSelected])foundSelected[permissionNameSelected] = []
foundSelected[permissionNameSelected].push(monitorId)
})
})
details = Object.assign(details,foundSelected)
details = Object.assign(details,{
'monitors': [],
'monitor_edit': [],
'video_view': [],
'video_delete': [],
},foundSelected)
detailsElement.val(JSON.stringify(details))
}
var getCompleteForm = function(){

View File

@ -1,3 +1,25 @@
var apiBaseUrl = getApiPrefix()
function getTimelapseFrames(monitorId,startDate,endDate,limit){
return new Promise((resolve,reject) => {
if(!monitorId || !startDate || !endDate){
console.log(new Error(`getTimelapseFrames error : Failed to get proper params`))
resolve([])
return
}
var queryString = [
'start=' + startDate,
'end=' + endDate,
limit === 'noLimit' ? `noLimit=1` : limit ? `limit=${limit}` : `limit=50`
]
var apiURL = apiBaseUrl + '/timelapse/' + $user.ke + '/' + monitorId
$.getJSON(apiURL + '?' + queryString.join('&'),function(data){
$.each(data,function(n,fileInfo){
fileInfo.href = apiURL + '/' + fileInfo.filename.split('T')[0] + '/' + fileInfo.filename
})
resolve(data)
})
})
}
$(document).ready(function(e){
//Timelapse JPEG Window
var timelapseWindow = $('#tab-timelapseViewer')
@ -14,7 +36,7 @@ $(document).ready(function(e){
var liveStreamView = timelapseWindow.find('.liveStreamView')
var monitorsList = timelapseWindow.find('.monitors_list')
var downloadButton = timelapseWindow.find('.download_mp4')
var apiBaseUrl = getApiPrefix()
var selectAllBox = timelapseWindow.find('.select-all')
var downloadRecheckTimers = {}
var currentPlaylist = {}
var frameSelected = null
@ -64,29 +86,22 @@ $(document).ready(function(e){
liveStreamView.find('iframe').width(playBackViewImage.width())
}
var drawTimelapseWindowElements = function(selectedMonitor,startDate,endDate){
function drawTimelapseWindowElements(selectedMonitor,startDate,endDate){
setDownloadButtonLabel(lang['Build Video'], 'database')
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,
'noLimit=1'
]
var frameIconsHtml = ''
var apiURL = apiBaseUrl + '/timelapse/' + $user.ke + '/' + selectedMonitor
$.getJSON(apiURL + '?' + queryString.join('&'),function(data){
getTimelapseFrames(selectedMonitor,startDate,endDate,'noLimit').then((data) => {
if(data && data[0]){
var firstFilename = data[0].filename
frameSelected = firstFilename
currentPlaylist = {}
currentPlaylistArray = []
$.each(data.reverse(),function(n,fileInfo){
fileInfo.href = apiURL + '/' + fileInfo.filename.split('T')[0] + '/' + fileInfo.filename
fileInfo.number = n
frameIconsHtml += '<div class="col-md-4 frame-container"><div class="frame" data-filename="' + fileInfo.filename + '" frame-container-unloaded="' + fileInfo.href + '"><div class="button-strip"><button type="button" class="btn btn-sm btn-danger delete"><i class="fa fa-trash-o"></i></button></div><div class="shade">' + moment(fileInfo.time).format('YYYY-MM-DD HH:mm:ss') + '</div></div></div>'
frameIconsHtml += '<div class="col-md-4 frame-container" frame-container="' + fileInfo.filename + '"><div class="frame" data-filename="' + fileInfo.filename + '" frame-container-unloaded="' + fileInfo.href + '"><div class="button-strip"><input name="' + fileInfo.href + '" value="' + fileInfo.filename + '" type="checkbox" class="form-check-input"><button type="button" class="btn btn-sm btn-danger delete"><i class="fa fa-trash-o"></i></button></div><div class="shade">' + moment(fileInfo.time).format('YYYY-MM-DD HH:mm:ss') + '</div></div></div>'
currentPlaylist[fileInfo.filename] = fileInfo
})
currentPlaylistArray = data
@ -96,7 +111,7 @@ $(document).ready(function(e){
resetFilmStripPositions()
loadVisibleTimelapseFrames()
}else{
frameIconsHtml = lang['No Data']
frameIconsHtml = `<div class="text-center">${lang['No Data']}</div>`
frameIcons.html(frameIconsHtml)
}
})
@ -137,7 +152,7 @@ $(document).ready(function(e){
var playTimelapse = function(){
var playPauseText = timelapseWindow.find('.playPauseText')
canPlay = true
playPauseText.text(lang.Pause)
playPauseText.html(`<i class="fa fa-pause"></i> ${lang.Pause}`)
startPlayLoop()
}
var destroyTimelapse = function(){
@ -151,7 +166,7 @@ $(document).ready(function(e){
var pauseTimelapse = function(){
var playPauseText = timelapseWindow.find('.playPauseText')
canPlay = false
playPauseText.text(lang.Play)
playPauseText.html(`<i class="fa fa-play"></i> ${lang.Play}`)
clearTimeout(playIntervalTimer)
playIntervalTimer = null
}
@ -169,6 +184,47 @@ $(document).ready(function(e){
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 + `<br><br><img style="max-width:100%" src="${frameHrefs[0]}">`,
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')
@ -196,39 +252,46 @@ $(document).ready(function(e){
class: 'btn-danger',
title: lang.Delete,
},
clickCallback: function(){
$.getJSON(frame.href + '/delete',function(response){
if(response.ok){
el.parent().remove()
}
})
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()
var parsedFrames = JSON.stringify(currentPlaylistArray.map(function(frame){
window.askedForTimelapseVideoBuild = true
var parsedFrames = currentPlaylistArray.map(function(frame){
return {
mid: frame.mid,
ke: frame.ke,
filename: frame.filename,
}
}));
$.post(apiBaseUrl + '/timelapseBuildVideo/' + $user.ke + '/' + selectedMonitor,{
fps: fps,
});
mainSocket.f({
f: 'timelapseVideoBuild',
mid: selectedMonitor,
frames: parsedFrames,
},function(response){
setDownloadButtonLabel(response.msg, '')
new PNotify({
title: lang['Timelapse Frames Video'],
text: response.msg,
type: response.fileExists ? 'success' : 'info'
})
if(response.fileExists)downloadTimelapseVideo(response);
fps: fps,
})
})
function isElementVisible (el) {
@ -254,10 +317,7 @@ $(document).ready(function(e){
}
function downloadTimelapseVideo(data){
var downloadUrl = buildFileBinUrl(data)
var a = document.createElement('a')
a.href = downloadUrl
a.download = data.name
a.click()
downloadFile(downloadUrl,data.name)
}
function onTimelapseVideoBuildComplete(data){
var saveBuiltVideo = dashboardOptions().switches.timelapseSaveBuiltVideo
@ -303,6 +363,18 @@ $(document).ready(function(e){
}
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!']}`

View File

@ -1,20 +1,19 @@
function getVideoPlayerTabId(video){
return `videoPlayer-${video.mid}-${moment(video.time).format('YYYY-MM-DD-HH-mm-ss')}`
}
$(document).ready(function(){
var theBlock = $('#tab-videoPlayer')
window.createVideoPlayerTab = function(video,timeStart){
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 = `<b>${lang['Video']}</b> : ${loadedMonitors[video.mid].name} : ${formattedTime(video.time,true)}`
var videoUrl = getApiPrefix(`videos`) + '/' + video.mid + '/' + video.filename
var videoUrl = getLocation() + video.href
var hasRows = video.events && video.events.length > 0
var loadedEvents = {}
var eventMatrixHtml = ``
if(hasRows){
var objectsFound = {}
eventMatrixHtml += `<div class="border-top-dotted border-top-dark pt-2 mt-2"><h3>${lang.Events}</h3></div>`
$.each(video.events,function(n,theEvent){
loadedEvents[new Date(theEvent.time)] = theEvent
var objectsFound = {}
@ -35,46 +34,46 @@ $(document).ready(function(){
})
eventMatrixHtml += `</div>`
}
var baseHtml = `<main class="container page-tab tab-videoPlayer" id="tab-${newTabId}" video-id="${video.mid}${video.time}" data-mid="${video.mid}" data-time="${video.time}">
<div class="mt-3 ${definitions.Theme.isDark ? 'bg-dark text-white' : 'bg-light text-dark'} rounded shadow-sm" style="overflow:hidden">
<div class="d-flex flex-row">
<div class="flex-grow-1">
<h6 class="video-title border-bottom-dotted border-bottom-dark p-3 mb-0">${tabLabel}</h6>
</div>
<div>
<a class="btn btn-default btn-sm delete-tab-dynamic"><i class="fa fa-times"></i></a>
</div>
var baseHtml = `<main class="container page-tab tab-videoPlayer" id="tab-${newTabId}" video-id="${video.mid}${video.time}" data-time="${video.time}" data-mid="${video.mid}">
<div class="my-3 ${definitions.Theme.isDark ? 'bg-dark text-white' : 'bg-light text-dark'} rounded shadow-sm">
<div class="p-3">
<h6 class="video-title border-bottom-dotted border-bottom-dark pb-2 mb-0">${tabLabel}</h6>
</div>
<div style="position: relative">
<div class="tab-videoPlayer-view-container" style="position: relative">
<div class="tab-videoPlayer-event-objects" style="position: absolute;width: 100%; height: 100%; z-index: 10"></div>
<video class="tab-videoPlayer-video-element" controls autoplay src="${videoUrl}"></video>
</div>
<div class="d-flex flex-row">
<div class="d-block p-3">
<b class="flex-grow-1">${lang.Started}</b>
<div class="video-time">${humanStartTime}</div>
</div>
<div class="d-block p-3">
<b class="flex-grow-1">${lang.Ended}</b>
<div class="video-end">${humanEndTime}</div>
</div>
</div>
<div class="mb-3">
${eventMatrixHtml}
</div>
<div class="d-flex flex-row">
<div class="flex-grow-1 bg-gradient-blue">
<a class="btn btn-block px-1 py-3 ${definitions.Theme.isDark ? 'text-white' : 'text-dark'}" download href="${videoUrl}"><i class="fa fa-download"></i> ${lang.Download}</a>
</div>
<div class="flex-grow-1 bg-gradient-orange">
<a class="btn btn-block px-1 py-3 delete-video ${definitions.Theme.isDark ? 'text-white' : 'text-dark'}"><i class="fa fa-trash-o"></i> ${lang.Delete}</a>
<div class="p-3">
<div class="row">
<div class="col-md-6">
<div class="row">
<div class="col-md-6">
<b class="flex-grow-1">${lang.Started}</b>
<div class="video-time">${humanStartTime}</div>
</div>
<div class="col-md-6">
<b class="flex-grow-1">${lang.Ended}</b>
<div class="video-end">${humanEndTime}</div>
</div>
</div>
<div class="d-block pt-4">
<div class="btn-group btn-group-justified">
<a class="btn btn-sm btn-success" download href="${videoUrl}"><i class="fa fa-download"></i> ${lang.Download}</a>
${permissionCheck('video_delete',video.mid) ? `<a class="btn btn-sm btn-danger delete-video"><i class="fa fa-trash-o"></i> ${lang.Delete}</a>` : ''}
</div>
</div>
</div>
<div class="col-md-6">
${eventMatrixHtml}
</div>
</div>
</div>
</div>
</main>`
var tabCreateResponse = createNewTab(newTabId,tabLabel,baseHtml,{},null,'videoPlayer')
var videoElement = tabCreateResponse.theTab.find('.tab-videoPlayer-video-element')[0]
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()
@ -92,7 +91,6 @@ $(document).ready(function(){
}
}
}
if(timeStart)videoElement.currentTime = timeStart;
}
window.closeVideoPlayer = function(tabId){
console.log('closeVideoPlayer')
@ -155,5 +153,6 @@ $(document).ready(function(){
var newVideoTimeIndex = timeIndex - videoTime
console.log(newVideoTimeIndex)
videoEl[0].currentTime = newVideoTimeIndex
videoEl[0].play()
})
})

View File

@ -1,10 +1,14 @@
var loadedVideosInMemory = {}
var loadedFramesMemory = {}
var loadedFramesMemoryTimeout = {}
var loadedFramesLock = {}
function getLocalTimelapseImageLink(imageUrl){
if(loadedFramesMemory[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
@ -16,8 +20,11 @@ function getLocalTimelapseImageLink(imageUrl){
URL.revokeObjectURL(objectURL)
delete(loadedFramesMemory[imageUrl])
delete(loadedFramesMemoryTimeout[imageUrl])
},1000 * 60 * 10)
resolve(objectURL)
},1000 * 60 * 10);
loadedFramesLock[imageUrl] = false;
resolve(objectURL);
}).catch((err) => {
resolve()
});
})
}
@ -38,7 +45,7 @@ async function preloadAllTimelapseFramesToMemoryFromVideoList(framesSortedByDays
console.log ('Loaded! ',frame.href)
}
}
function createVideoLinks(video){
function createVideoLinks(video,options){
var details = safeJsonParse(video.details)
var queryString = []
// if(details.isUTC === true){
@ -102,8 +109,8 @@ function applyTimelapseFramesListToVideos(videos,events,keyName,reverseList){
return newVideos
}
function getFrameOnVideoRow(percentageInward,video){
var startTime = new Date(video.time)
var endTime = new Date(video.end)
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
@ -117,79 +124,79 @@ function getFrameOnVideoRow(percentageInward,video){
timeAdded: timeAdded,
}
}
function getVideoFromDay(percentageInward,videos){
var startTime = new Date(videos[0].time)
var endTime = new Date(videos[videos.length - 1].end)
function getVideoFromDay(percentageInward,reversedVideos,startTime,endTime){
var timeDifference = endTime - startTime
var timeInward = timeDifference / (100 / percentageInward)
var timeAdded = new Date(startTime.getTime() + timeInward) // ms
var foundVideo = ([]).concat(videos).reverse().find(function(row){
return new Date(timeAdded - 1000) >= new Date(row.time)
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 += `
<table class="table table-striped mb-0">
<tr>
<th scope="col" class="${definitions.Theme.isDark ? 'text-white' : ''} text-epic">${lang.Events}</th>
<th scope="col" class="text-end"><span class="badge bg-light text-dark rounded-pill">${video.events.length}</span></th>
</tr>`
$.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 += `<div class="col-4 mb-2"><img class="rounded pop-image cursor-pointer" style="max-width:100%;" src="${getApiPrefix('timelapse')}/${theEvent.mid}/${imagePath}" onerror="$(this).parent().remove()"></div>`
})
$.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 += `<tr>
<td class="${definitions.Theme.isDark ? 'text-white' : ''}" style="text-transform:capitalize">${tag}</td>
<td class="text-end"><span class="badge badge-dark text-white rounded-pill">${count}</span></td>
</tr>`
})
eventMatrixHtml += `</table>`
timeStrip.append(eventMatrixHtml)
}
timeImg.remove()
}
}
// 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 += `
// <table class="table table-striped mb-0">
// <tr>
// <th scope="col" class="${definitions.Theme.isDark ? 'text-white' : ''} text-epic">${lang.Events}</th>
// <th scope="col" class="text-end"><span class="badge bg-light text-dark rounded-pill">${video.events.length}</span></th>
// </tr>`
// $.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 += `<div class="col-4 mb-2"><img class="rounded pop-image cursor-pointer" style="max-width:100%;" src="${getApiPrefix('timelapse')}/${theEvent.mid}/${imagePath}" onerror="$(this).parent().remove()"></div>`
// })
// $.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 += `<tr>
// <td class="${definitions.Theme.isDark ? 'text-white' : ''}" style="text-transform:capitalize">${tag}</td>
// <td class="text-end"><span class="badge badge-dark text-white rounded-pill">${count}</span></td>
// </tr>`
// })
// eventMatrixHtml += `</table>`
// 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 dayStart = videos[0].time
var dayEnd = videos[videos.length - 1].end
var firstFrameOfDay = null
var firstFrameOfDay = allFrames[0] || null
$.each(videos,function(day,video){
$.each(video.timelapseFrames,function(day,frame){
if(!firstFrameOfDay)firstFrameOfDay = frame;
@ -203,15 +210,16 @@ function bindFrameFindingByMouseMoveForDay(createdCardCarrier,dayKey,videos,allF
}
var videoSlices = createdCardElement.find('.video-day-slice')
var videoTimeLabel = createdCardElement.find('.video-time-label')
var currentlySelected = null
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 = getVideoFromDay(percentMoved,videos)
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)')
@ -228,8 +236,8 @@ function bindFrameFindingByMouseMoveForDay(createdCardCarrier,dayKey,videos,allF
}
// draw frame
var result = getFrameOnVideoRow(percentMoved,{
time: videos[0].time,
end: videos[videos.length - 1].end,
time: dayStart,
end: dayEnd,
timelapseFrames: allFrames,
})
var frameFound = result.foundFrame
@ -238,7 +246,7 @@ function bindFrameFindingByMouseMoveForDay(createdCardCarrier,dayKey,videos,allF
currentlySelectedFrame = Object.assign({},frameFound)
setTimeout(async function(){
var frameUrl = await getLocalTimelapseImageLink(frameFound.href)
if(currentlySelectedFrame.time === frameFound.time)timeImg.attr('src',frameUrl);
if(frameUrl && currentlySelectedFrame.time === frameFound.time)timeImg.attr('src',frameUrl);
},1)
}
timeNeedleSeeker.attr('video-slice-seeked',result.timeInward).css('left',`${percentMoved}%`)
@ -323,35 +331,65 @@ function sortFramesByDays(frames){
frame.href = libURL + apiURL + '/' + frame.filename.split('T')[0] + '/' + frame.filename
days[frame.mid][theDayKey].push(frame)
})
console.log(days)
return days
}
function getVideoPercentWidthForDay(row,videos){
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 stripStartTime = new Date(videos[0].time)
var stripEndTime = new Date(videos[videos.length - 1].end)
var stripTimeDifference = stripEndTime - stripStartTime
var stripTimes = getStripStartAndEnd(videos,frames)
var stripTimeDifference = stripTimes.end - stripTimes.start
var percent = (timeDifference / stripTimeDifference) * 100
return percent
}
function createDayCard(videos,dayKey,monitorId,classOverride){
function createDayCard(videos,frames,dayKey,monitorId,classOverride){
var html = ''
var eventMatrixHtml = ``
var dayParts = formattedTime(videos[0].time).split(' ')[1].split('-')
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]
if(nextRow)console.log({time: row.end, end: nextRow.time},n + 1)
var marginRight = !!nextRow ? getVideoPercentWidthForDay({time: row.end, end: nextRow.time},videos) : 0;
eventMatrixHtml += `<div class="video-day-slice" data-mid="${row.mid}" data-time="${row.time}" style="width:${getVideoPercentWidthForDay(row,videos)}%;position:relative">`
var marginRight = !!nextRow ? getVideoPercentWidthForDay({time: row.end, end: nextRow.time},videos,frames) : 0;
eventMatrixHtml += `<div class="video-day-slice" data-mid="${row.mid}" data-time="${row.time}" style="width:${getVideoPercentWidthForDay(row,videos,frames)}%;position:relative">`
if(row.events && row.events.length > 0){
$.each(row.events,function(n,theEvent){
var leftPercent = getPercentOfTimePositionFromVideo(row,theEvent)
eventMatrixHtml += `<div class="video-time-needle video-time-needle-event" style="margin-right:${leftPercent}%"></div>`
eventMatrixHtml += `<div class="video-time-needle video-time-needle-event" style="margin-left:${leftPercent}%"></div>`
})
}
eventMatrixHtml += `</div>`
@ -365,7 +403,7 @@ function createDayCard(videos,dayKey,monitorId,classOverride){
<div class="flex-grow-1 p-3">
<b>${loadedMonitors[monitorId] ? loadedMonitors[monitorId].name : monitorId}</b>
<div class="${definitions.Theme.isDark ? 'text-white' : ''}">
<span class="video-time-label">${formattedTime(videos[0].time)} to ${formattedTime(videos[videos.length - 1].end)}</span>
<span class="video-time-label">${formattedTime(startTime)} to ${formattedTime(endTime)}</span>
</div>
</div>
<div class="text-right p-3" style="background:rgba(0,0,0,0.5)">
@ -379,7 +417,7 @@ function createDayCard(videos,dayKey,monitorId,classOverride){
</div>
<div class="video-time-strip card-footer p-0">
<div class="flex-row d-flex" style="height:30px">${eventMatrixHtml}</div>
<div class="video-time-needle video-time-needle-seeker" data-mid="${videos[0].mid}" style="z-index: 2"></div>
<div class="video-time-needle video-time-needle-seeker" ${firstVideoTime ? `video-time-seeked-video-position="${firstVideoTime}"` : ''} data-mid="${monitorId}" style="z-index: 2"></div>
</div>
</div>
</div>`
@ -398,32 +436,40 @@ function loadVideoData(video){
loadedVideosInMemory[`${video.mid}${video.time}`] = video
}
function getVideos(options,callback){
options = options ? options : {}
var requestQueries = []
var monitorId = options.monitorId
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}`)
}
$.getJSON(`${getApiPrefix(`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)
return new Promise((resolve,reject) => {
options = options ? options : {}
var searchQuery = options.searchQuery
var requestQueries = []
var monitorId = options.monitorId
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}`)
}
$.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})
})
callback({videos: newVideos, frames: timelapseFrames})
})
})
})
@ -452,6 +498,31 @@ function getEvents(options,callback){
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)
}
}
onWebSocketEvent(function(d){
switch(d.f){
case'video_delete':
@ -459,6 +530,9 @@ onWebSocketEvent(function(d){
$('[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])
@ -474,12 +548,14 @@ onWebSocketEvent(function(d){
})
$(document).ready(function(){
$('body')
.on('click','.open-video',function(){
.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)
@ -490,7 +566,8 @@ $(document).ready(function(){
timeInward = timeInward < 0 ? 0 : timeInward
createVideoPlayerTab(video,timeInward)
})
.on('click','.delete-video',function(){
.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')
@ -510,10 +587,39 @@ $(document).ready(function(){
if(data.ok){
console.log('Video Deleted')
}else{
console.log('Video Not Deleted',data,deleteEndpoint)
console.log('Video Not Deleted',data,videoEndpoint)
}
})
}
})
});
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}<br><br><div class="row"><video class="video_video" autoplay loop controls><source src="${videoEndpoint}" type="video/${ext}"></video></div>`,
clickOptions: {
title: '<i class="fa fa-wrench"></i> ' + 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;
})
})

View File

@ -4,7 +4,37 @@ $(document).ready(function(e){
var dateSelector = theEnclosure.find('.date_selector')
var videosTableDrawArea = $('#videosTable_draw_area')
var videosTablePreviewArea = $('#videosTable_preview_area')
var loadedVideosInMemory = {};
var objectTagSearchField = $('#videosTable_tag_search')
var cloudVideoCheckSwitch = $('#videosTable_cloudVideos')
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 href = await getSnapshotFromVideoTimeFrame(monitorId,startDate,endDate)
imgEl.innerHTML = href ? `<img class="pop-image cursor-pointer" src="${href}">` : ''
})
}
function openVideosTableView(monitorId,startDate,endDate){
drawVideosTableViewElements(monitorId,startDate,endDate)
}
@ -37,65 +67,137 @@ $(document).ready(function(e){
monitorsList.change(function(){
drawVideosTableViewElements()
})
function drawVideosTableViewElements(selectedMonitor,startDate,endDate){
objectTagSearchField.change(function(){
drawVideosTableViewElements()
})
cloudVideoCheckSwitch.change(function(){
drawVideosTableViewElements()
})
async function drawVideosTableViewElements(usePreloadedData){
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 searchQuery = objectTagSearchField.val() || null
var startDate = dateRange.startDate
var endDate = dateRange.endDate
var monitorId = monitorsList.val()
var frameIconsHtml = ''
var apiURL = getApiPrefix('videos') + '/' + selectedMonitor;
var videosTableData = []
loadedVideosInMemory = {}
$.getJSON(apiURL + '?' + queryString.join('&'),function(data){
videosTableDrawArea.bootstrapTable('destroy')
videosTableDrawArea.bootstrapTable({
pagination: true,
search: true,
columns: [
{
field: 'monitorName',
title: lang['Monitor']
},
{
field: 'time',
title: lang['Time Created']
},
{
field: 'size',
title: ''
},
{
field: 'buttons',
title: ''
}
],
data: data.videos.map((file) => {
return {
monitorName: `<b>${loadedMonitors[file.mid]?.name || file.mid}</b>`,
time: `
<div><b>${lang.Started}</b> ${formattedTime(file.start, 'DD-MM-YYYY hh:mm:ss AA')}</div>
<div><b>${lang.Ended}</b> ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}</div>
`,
size: convertKbToHumanSize(file.size),
buttons: `
<a class="btn btn-sm btn-primary" href="${file.href}" download title="${lang.Download}"><i class="fa fa-download"></i></a>
<a class="btn btn-sm btn-primary preview-video" href="${file.href}" title="${lang.Play}"><i class="fa fa-play"></i></a>
`,
}
})
if(!usePreloadedData){
loadedVideosTable = (await getVideos({
monitorId,
startDate,
endDate,
searchQuery,
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: 'size',
title: ''
},
{
field: 'buttons',
title: ''
}
],
data: loadedVideosTable.map((file) => {
var href = getFullOrigin(true) + file.href
var loadedMonitor = loadedMonitors[file.mid]
return {
image: `<div class="video-thumbnail" data-mid="${file.mid}" data-ke="${file.ke}" data-time="${file.time}" data-end="${file.end}"></div>`,
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'),
objects: file.objects,
size: convertKbToHumanSize(file.size),
buttons: `
<div class="row-info" data-mid="${file.mid}" data-ke="${file.ke}" data-time="${file.time}" data-filename="${file.filename}">
<a class="btn btn-sm btn-primary" href="${href}" download title="${lang.Download}"><i class="fa fa-download"></i></a>
<a class="btn btn-sm btn-primary preview-video" href="${href}" title="${lang.Play}"><i class="fa fa-play"></i></a>
<a class="btn btn-sm btn-default open-video" href="${href}" title="${lang.Play}"><i class="fa fa-play"></i></a>
${permissionCheck('video_delete',file.mid) ? `<a class="btn btn-sm btn-danger delete-video" href="${href}" title="${lang.Delete}"><i class="fa fa-trash-o"></i></a>` : ''}
</div>
`,
}
})
})
}
function drawPreviewVideo(href){
videosTablePreviewArea.html(`<video class="video_video" style="width:100%" autoplay controls preload loop src="${href}"></video>`)
}
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 wantCloudVideos(){
const isChecked = cloudVideoCheckSwitch.val() === 'cloud'
return isChecked
}
$('body')
.on('click','.open-videosTable',function(e){
e.preventDefault()
var monitorId = getRowsMonitorId(this)
openTab(`videosTableView`,{},null,null,null,() => {
drawMonitorListToSelector(monitorsList)
drawMonitorListToSelector(monitorsList,null,null,true)
monitorsList.val(monitorId)
drawVideosTableViewElements()
})
@ -108,13 +210,83 @@ $(document).ready(function(e){
drawPreviewVideo(href)
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: '<i class="fa fa-trash-o"></i> ' + lang.Delete,
class: 'btn-danger btn-sm'
},
clickCallback: function(){
deleteVideos(videos).then(() => {
console.log(`Done Deleting 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: '<i class="fa fa-check"></i> ' + 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: '<i class="fa fa-check"></i> ' + 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' && monitorsList.val() === data.mid){
var videoIndexToRemove = loadedVideosTable.findIndex(row => 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;
}
})
addOnTabOpen('videosTableView', function () {
drawMonitorListToSelector(monitorsList)
drawMonitorListToSelector(monitorsList,null,null,true)
drawVideosTableViewElements()
})
addOnTabReopen('videosTableView', function () {
var theSelected = `${monitorsList.val()}`
drawMonitorListToSelector(monitorsList)
drawMonitorListToSelector(monitorsList,null,null,true)
monitorsList.val(theSelected)
})
addOnTabAway('videosTableView', function () {

View File

@ -14,14 +14,20 @@ 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){
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)
}
@ -58,6 +64,10 @@ $(document).ready(function(){
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)

View File

@ -15,3 +15,297 @@
.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;
}

View File

@ -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";
}

View File

@ -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)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -14,11 +14,11 @@ $(document).ready(function(){
}else{
listElement.append(`
<div class="col-md-12">
<div class="card" package-name="${module.name}">
<div class="card bg-dark" package-name="${module.name}">
<div class="card-body">
<div><h4 class="title mt-0">${humanName}</h4></div>
<div><pre><b>${lang['Time Created']} :</b> ${module.created}</pre></div>
<div><pre><b>${lang['Last Modified']} :</b> ${module.lastModified}</pre></div>
<div class="pb-2"><b>${lang['Time Created']} :</b> ${module.created}</div>
<div class="pb-2"><b>${lang['Last Modified']} :</b> ${module.lastModified}</div>
<div class="mb-2">
${!module.isIgnitor ? `
${module.hasInstaller ? `

View File

@ -1,9 +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');
@ -58,6 +61,7 @@ $(document).ready(function(){
connectedUsers.text(data.users)
chartViewerCount.text(data.statViewers)
})
statusConnections[key] = socketConnection
}
function disableForm(){
loadingRegistration = true
@ -95,6 +99,31 @@ $(document).ready(function(){
})
}
}
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()
})
@ -152,11 +181,6 @@ $(document).ready(function(){
}
return false;
})
$.each(p2pServerList,function(key,server){
server.key = key
if(window.useBetterP2P && !server.v2)return;
if(!window.useBetterP2P && server.v2)return;
beginStatusConnectionForServer(key,server)
})
setVisibilityForList()
displayCurrentlySelectedInternally()
})

View File

@ -14,11 +14,11 @@ $(document).ready(function(){
}else{
listElement.append(`
<div class="col-md-12">
<div class="card" package-name="${module.name}">
<div class="card-body">
<div class="card bg-dark text-white mb-3" package-name="${module.name}">
<div class="card-body pb-3">
<div><h4 class="title mt-0">${humanName}</h4></div>
<div><pre><b>${lang['Time Created']} :</b> ${module.created}</pre></div>
<div><pre><b>${lang['Last Modified']} :</b> ${module.lastModified}</pre></div>
<div class="pb-2"><b>${lang['Time Created']} :</b> ${module.created}</div>
<div class="pb-2"><b>${lang['Last Modified']} :</b> ${module.lastModified}</div>
<div class="mb-2">
${module.hasInstaller ? `
<a class="btn btn-sm btn-info" plugin-manager-action="install">${lang['Run Installer']}</a>
@ -30,8 +30,8 @@ $(document).ready(function(){
</div>
<div class="pl-2 pr-2">
<div class="install-output row">
<div class="col-md-6 pr-2"><pre class="install-output-stdout"></pre></div>
<div class="col-md-6 pl-2"><pre class="install-output-stderr"></pre></div>
<div class="col-md-6 pr-2"><pre class="install-output-stdout text-white mb-0"></pre></div>
<div class="col-md-6 pl-2"><pre class="install-output-stderr text-white mb-0"></pre></div>
</div>
<div class="command-installer row" style="display:none">
<div class="col-md-6">

View File

@ -1,12 +1,55 @@
$(document).ready(function(){
var changeSuperPreferencesTab = $('#changeSuperPreferences')
var tokenContainer = $('#super-tokens')
var changeSuperPreferencesForm = changeSuperPreferencesTab.find('form')
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 drawTokenRow(tokenValue){
var html = `<div class="d-flex flex-row token-row mt-3">
<div class="flex-grow-1">
<input class="form-control form-control-sm token-row-input" value="${tokenValue || generateId(30)}">
</div>
<div>
<a class="delete-token btn btn-danger btn-sm my-0 ml-3"><i class="fa fa-trash"></i></a>
</div>
</div>`
tokenContainer.append(html)
}
function getTokenRows(){
var rowsFound = []
changeSuperPreferencesTab.find('.token-row-input').each(function(n,v){
var el = $(v)
var tokenValue = el.val().trim()
if(tokenValue)rowsFound.push(tokenValue);
})
return rowsFound
}
function loadPreferences(){
var tokens = $user.tokens
changeSuperPreferencesTab.find('[name=mail]').val($user.mail)
if(tokens instanceof Array){
tokens.forEach(function(token){
drawTokenRow(token)
})
}
}
changeSuperPreferencesTab.find('.new-token').click(function(){
drawTokenRow()
})
changeSuperPreferencesTab.on('click','.delete-token',function(){
$(this).parents('.token-row').remove()
})
changeSuperPreferencesTab.find('.submit').click(function(){
changeSuperPreferencesForm.submit()
})
changeSuperPreferencesForm.submit(function(e){
e.preventDefault()
var formValues = $(this).serializeObject()
formValues.tokens = getTokenRows()
// $.ccio.cx({f:'accounts',ff:'saveSuper',form:formValues})
$.post(superApiPrefix + $user.sessionKey + '/accounts/saveSettings',{
data: JSON.stringify(formValues)
@ -15,4 +58,5 @@ $(document).ready(function(){
})
return false
})
loadPreferences()
})

Some files were not shown because too many files have changed in this diff Show More