diff --git a/Docker/README.md b/Docker/README.md
new file mode 100644
index 00000000..29284273
--- /dev/null
+++ b/Docker/README.md
@@ -0,0 +1,137 @@
+# Install Shinobi with Docker
+
+### There are three ways!
+
+## Docker Ninja Way
+
+> This method uses `docker-compose` and has the ability to quick install the TensorFlow Object Detection plugin.
+
+```
+bash <(curl -s https://gitlab.com/Shinobi-Systems/Shinobi-Installer/raw/master/shinobi-docker.sh)
+```
+
+## Docker Ninja Way - Version 2
+
+#### Installing Shinobi
+
+> 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
+```
+
+#### Installing Object Detection (TensorFlow.js)
+
+> 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.
+
+- `-p '8082:8082/tcp'` is an optional flag if you decide to run the plugin in host mode.
+- `-e PLUGIN_HOST='10.1.103.113'` Set this as your Shinobi IP Address.
+- `-e PLUGIN_PORT='8080'` Set this as your Shinobi Web Port number.
+
+```
+docker run -d --name='shinobi-tensorflow' -e PLUGIN_HOST='10.1.103.113' -e PLUGIN_PORT='8080' -v "$HOME/Shinobi/docker-plugins/tensorflow":'/config':'rw' shinobisystems/shinobi-tensorflow:latest
+```
+
+- CPU : https://gitlab.com/Shinobi-Systems/docker-plugin-tensorflow.js
+- GPU (NVIDIA CUDA) : https://gitlab.com/Shinobi-Systems/docker-plugin-tensorflow.js/-/tree/gpu
+
+
+## From Source
+> Image is based on Ubuntu Bionic (20.04). Node.js 12 is used. MariaDB and FFmpeg are included.
+
+1. Download Repo
+
+```
+git clone -b dev https://gitlab.com/Shinobi-Systems/Shinobi.git ShinobiSource
+```
+
+2. Enter Repo and Build Image.
+
+```
+cd ShinobiSource
+docker build --tag shinobi-image:1.0 .
+```
+
+3. Create a container with the image.
+
+> This command only works on Linux because of the temporary directory used. This location must exist in RAM. `-v "/dev/shm/shinobiStreams":'/dev/shm/streams':'rw'`. The timezone is also acquired from the host by the volume declaration of `-v '/etc/localtime':'/etc/localtime':'ro'`.
+
+```
+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' shinobi-image:1.0
+```
+
+ > Host mount paths have been updated in this document.
+
+ ### Volumes
+
+ | Volumes | Description |
+ |-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|
+ | /dev/shm/Shinobi/streams | **IMPORTANT!** This must be mapped to somewhere in the host's RAM. When running this image on Windows you will need to select a different location. |
+ | $HOME/Shinobi/config | Put `conf.json` or `super.json` files in here to override the default. values. |
+ | $HOME/Shinobi/customAutoLoad | Maps to the `/home/Shinobi/libs/customAutoLoad` folder for loading your own modules into Shinobi. |
+ | $HOME/Shinobi/database | A map to `/var/lib/mysql` in the container. This is the database's core files. |
+ | $HOME/Shinobi/videos | A map to `/home/Shinobi/videos`. The storage location of your recorded videos. |
+ | $HOME/Shinobi/plugins | A map to `/home/Shinobi/plugins`. Mapped so that plugins can easily be modified or swapped. |
+
+### Environment Variables
+
+ | Environment Variable | Description | Default |
+ |----------------------|----------------------------------------------------------------------|--------------------|
+ | SUBSCRIPTION_ID | **THIS IS NOT REQUIRED**. If you are a subscriber to any of the Shinobi services you may use that key as the value for this parameter. If you have donated by PayPal you may use your Transaction ID to activate the license as well. | *None* |
+ | DB_USER | Username that the Shinobi process will connect to the database with. | majesticflame |
+ | DB_PASSWORD | Password that the Shinobi process will connect to the database with. | *None* |
+ | DB_HOST | Address that the Shinobi process will connect to the database with. | localhost |
+ | DB_DATABASE | Database that the Shinobi process will interact with. | ccio |
+ | DB_DISABLE_INCLUDED | Disable included database to use your own. Set to `true` to disable.| false |
+ | PLUGIN_KEYS | The object containing connection keys for plugins running in client mode (non-host, default). | {} |
+ | SSL_ENABLED | Enable or Disable SSL. | false |
+ | SSL_COUNTRY | Country Code for SSL. | CA |
+ | SSL_STATE | Province/State Code for SSL. | BC |
+ | SSL_LOCATION | Location of where SSL key is being used. | Vancouver |
+ | SSL_ORGANIZATION | Company Name associated to key. | Shinobi Systems |
+ | SSL_ORGANIZATION_UNIT | Department associated to key. | IT Department |
+ | SSL_COMMON_NAME | Common Name associated to key. | nvr.ninja |
+
+ > You must add (to the docker container) `/config/ssl/server.key` and `/config/ssl/server.cert`. The `/config` folder is mapped to `$HOME/Shinobi/config` on the host by default with the quick run methods. Place `key` and `cert` in `$HOME/Shinobi/config/ssl`. If `SSL_ENABLED=true` and these files don't exist they will be generated with `openssl`.
+
+> For those using `DB_DISABLE_INCLUDED=true` please remember to create a user in your databse first. The Docker image will create the `DB_DATABASE` under the specified connection information.
+
+### Tips
+
+Modifying `conf.json` or Superuser credentials.
+> Please read **Volumes** table in this README. conf.json is for general configuration. super.json is for Superuser credential management.
+
+Get Docker Containers
+```
+docker ps -a
+```
+
+Get Images
+```
+docker images
+```
+
+Container Logs
+```
+docker logs /Shinobi
+```
+
+Enter the Command Line of the Container
+```
+docker exec -it /Shinobi /bin/bash
+```
+
+Stop and Remove
+```
+docker stop /Shinobi
+docker rm /Shinobi
+```
+
+**WARNING - DEVELOPMENT ONLY!!!** Kill all Containers and Images
+> These commands will completely erase all of your docker containers and images. **You have been warned!**
+
+```
+docker stop /Shinobi
+docker rm $(docker ps -a -f status=exited -q)
+docker rmi $(docker images -a -q)
+```
diff --git a/Docker/init.sh b/Docker/init.sh
new file mode 100644
index 00000000..2e235f6d
--- /dev/null
+++ b/Docker/init.sh
@@ -0,0 +1,105 @@
+#!/bin/sh
+set -e
+
+cp sql/framework.sql sql/framework1.sql
+OLD_SQL_USER_TAG="ccio"
+NEW_SQL_USER_TAG="$DB_DATABASE"
+sed -i "s/$OLD_SQL_USER_TAG/$NEW_SQL_USER_TAG/g" sql/framework1.sql
+if [ "$SSL_ENABLED" = "true" ]; then
+ if [ -d /config/ssl ]; then
+ echo "Using provided SSL Key"
+ cp -R /config/ssl ssl
+ SSL_CONFIG='{"key":"./ssl/server.key","cert":"./ssl/server.cert"}'
+ else
+ echo "Making new SSL Key"
+ mkdir -p ssl
+ openssl req -nodes -new -x509 -keyout ssl/server.key -out ssl/server.cert -subj "/C=$SSL_COUNTRY/ST=$SSL_STATE/L=$SSL_LOCATION/O=$SSL_ORGANIZATION/OU=$SSL_ORGANIZATION_UNIT/CN=$SSL_COMMON_NAME"
+ cp -R ssl /config/ssl
+ SSL_CONFIG='{"key":"./ssl/server.key","cert":"./ssl/server.cert"}'
+ fi
+else
+ SSL_CONFIG='{}'
+fi
+if [ "$DB_DISABLE_INCLUDED" = "false" ]; then
+ echo "MariaDB Directory ..."
+ ls /var/lib/mysql
+
+ if [ ! -f /var/lib/mysql/ibdata1 ]; then
+ echo "Installing MariaDB ..."
+ mysql_install_db --user=mysql --datadir=/var/lib/mysql --silent
+ fi
+ echo "Starting MariaDB ..."
+ /usr/bin/mysqld_safe --user=mysql &
+ sleep 5s
+
+ chown -R mysql /var/lib/mysql
+
+ if [ ! -f /var/lib/mysql/ibdata1 ]; then
+ mysql -u root --password="" -e "SET @@SESSION.SQL_LOG_BIN=0;
+ USE mysql;
+ DELETE FROM mysql.user ;
+ DROP USER IF EXISTS 'root'@'%','root'@'localhost','${DB_USER}'@'localhost','${DB_USER}'@'%';
+ CREATE USER 'root'@'%' IDENTIFIED BY '${DB_PASS}' ;
+ CREATE USER 'root'@'localhost' IDENTIFIED BY '${DB_PASS}' ;
+ CREATE USER '${DB_USER}'@'%' IDENTIFIED BY '${DB_PASS}' ;
+ CREATE USER '${DB_USER}'@'localhost' IDENTIFIED BY '${DB_PASS}' ;
+ GRANT ALL PRIVILEGES ON *.* TO 'root'@'%' WITH GRANT OPTION ;
+ GRANT ALL PRIVILEGES ON *.* TO 'root'@'localhost' WITH GRANT OPTION ;
+ GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'%' WITH GRANT OPTION ;
+ GRANT ALL PRIVILEGES ON *.* TO '${DB_USER}'@'localhost' WITH GRANT OPTION ;
+ DROP DATABASE IF EXISTS test ;
+ FLUSH PRIVILEGES ;"
+ fi
+
+ # Create MySQL database if it does not exists
+ if [ -n "${DB_HOST}" ]; then
+ echo "Wait for MySQL server" ...
+ while ! mysqladmin ping -h"$DB_HOST"; do
+ sleep 1
+ done
+ fi
+
+ echo "Setting up MySQL database if it does not exists ..."
+
+ echo "Create database schema if it does not exists ..."
+ mysql -e "source /home/Shinobi/sql/framework.sql" || true
+
+ echo "Create database user if it does not exists ..."
+ mysql -e "source /home/Shinobi/sql/user.sql" || true
+
+else
+ echo "Create database schema if it does not exists ..."
+ mysql -u "$DB_USER" -h "$DB_HOST" -p"$DB_PASSWORD" --port="$DB_PORT" -e "source /home/Shinobi/sql/framework.sql" || true
+fi
+
+DATABASE_CONFIG='{"host": "'$DB_HOST'","user": "'$DB_USER'","password": "'$DB_PASSWORD'","database": "'$DB_DATABASE'","port":'$DB_PORT'}'
+
+cronKey="$(head -c 1024 < /dev/urandom | sha256sum | awk '{print substr($1,1,29)}')"
+
+cd /home/Shinobi
+mkdir -p libs/customAutoLoad
+if [ -e "/config/conf.json" ]; then
+ cp /config/conf.json conf.json
+fi
+if [ ! -e "./conf.json" ]; then
+ sudo cp conf.sample.json conf.json
+fi
+sudo sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json
+node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG"
+sudo cp conf.json /config/conf.json
+
+
+echo "============="
+echo "Default Superuser : admin@shinobi.video"
+echo "Default Password : admin"
+echo "Log in at http://HOST_IP:SHINOBI_PORT/super"
+if [ -e "/config/super.json" ]; then
+ cp /config/super.json super.json
+fi
+if [ ! -e "./super.json" ]; then
+ sudo cp super.sample.json super.json
+ sudo cp super.sample.json /config/super.json
+fi
+# Execute Command
+echo "Starting Shinobi ..."
+exec "$@"
diff --git a/Docker/pm2.yml b/Docker/pm2.yml
new file mode 100644
index 00000000..1b6613d8
--- /dev/null
+++ b/Docker/pm2.yml
@@ -0,0 +1,7 @@
+apps:
+ - script : '/home/Shinobi/camera.js'
+ name : 'camera'
+ kill_timeout : 5000
+ - script : '/home/Shinobi/cron.js'
+ name : 'cron'
+ kill_timeout : 5000
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 00000000..1f9e379e
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,108 @@
+FROM node:12.18.3-buster-slim
+
+ENV DB_USER=majesticflame \
+ DB_PASSWORD='' \
+ DB_HOST='localhost' \
+ DB_DATABASE=ccio \
+ DB_PORT=3306 \
+ SUBSCRIPTION_ID=sub_XXXXXXXXXXXX \
+ PLUGIN_KEYS='{}' \
+ SSL_ENABLED='false' \
+ SSL_COUNTRY='CA' \
+ SSL_STATE='BC' \
+ SSL_LOCATION='Vancouver' \
+ SSL_ORGANIZATION='Shinobi Systems' \
+ SSL_ORGANIZATION_UNIT='IT Department' \
+ SSL_COMMON_NAME='nvr.ninja' \
+ DB_DISABLE_INCLUDED=false
+ARG DEBIAN_FRONTEND=noninteractive
+
+RUN mkdir -p /home/Shinobi /config /var/lib/mysql
+
+RUN apt update -y
+RUN apt install wget curl net-tools -y
+
+# Install MariaDB server... the debian way
+RUN if [ "$DB_DISABLE_INCLUDED" = "false" ] ; then set -ex; \
+ { \
+ echo "mariadb-server" mysql-server/root_password password '${DB_ROOT_PASSWORD}'; \
+ echo "mariadb-server" mysql-server/root_password_again password '${DB_ROOT_PASSWORD}'; \
+ } | debconf-set-selections; \
+ apt-get update; \
+ apt-get install -y \
+ "mariadb-server" \
+ socat \
+ ; \
+ find /etc/mysql/ -name '*.cnf' -print0 \
+ | xargs -0 grep -lZE '^(bind-address|log)' \
+ | xargs -rt -0 sed -Ei 's/^(bind-address|log)/#&/'; fi
+
+RUN if [ "$DB_DISABLE_INCLUDED" = "false" ] ; then sed -ie "s/^bind-address\s*=\s*127\.0\.0\.1$/#bind-address = 0.0.0.0/" /etc/mysql/my.cnf; fi
+
+# Install FFmpeg
+
+RUN apt install -y software-properties-common \
+ libfreetype6-dev \
+ libgnutls28-dev \
+ libmp3lame-dev \
+ libass-dev \
+ libogg-dev \
+ libtheora-dev \
+ libvorbis-dev \
+ libvpx-dev \
+ libwebp-dev \
+ libssh2-1-dev \
+ libopus-dev \
+ librtmp-dev \
+ libx264-dev \
+ libx265-dev \
+ yasm && \
+ apt install -y \
+ build-essential \
+ bzip2 \
+ coreutils \
+ gnutls-bin \
+ nasm \
+ tar \
+ x264
+
+RUN apt install -y \
+ ffmpeg \
+ git \
+ make \
+ g++ \
+ gcc \
+ pkg-config \
+ python3 \
+ wget \
+ tar \
+ sudo \
+ xz-utils
+
+
+WORKDIR /home/Shinobi
+COPY . .
+RUN rm -rf /home/Shinobiplugins
+COPY ./plugins /home/Shinobi/plugins
+RUN chmod -R 777 /home/Shinobi/plugins
+RUN npm i npm@latest -g && \
+ npm install pm2 -g && \
+ npm install --unsafe-perm && \
+ npm audit fix --force
+COPY ./Docker/pm2.yml ./
+
+# Copy default configuration files
+# COPY ./config/conf.json ./config/super.json /home/Shinobi/
+RUN chmod -f +x /home/Shinobi/Docker/init.sh
+
+VOLUME ["/home/Shinobi/videos"]
+VOLUME ["/home/Shinobi/plugins"]
+VOLUME ["/config"]
+VOLUME ["/customAutoLoad"]
+VOLUME ["/var/lib/mysql"]
+
+EXPOSE 8080
+
+ENTRYPOINT ["/home/Shinobi/Docker/init.sh"]
+
+CMD [ "pm2-docker", "pm2.yml" ]
diff --git a/INSTALL/cuda-10-2.sh b/INSTALL/cuda-10-2.sh
index 2f4f6d09..4f9e7c34 100644
--- a/INSTALL/cuda-10-2.sh
+++ b/INSTALL/cuda-10-2.sh
@@ -12,7 +12,7 @@ if [ -x "$(command -v apt)" ]; then
sudo apt-get update -y
- sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda -y --no-install-recommends
+ sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-10-2 -y --no-install-recommends
sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y
# Install CUDA DNN
diff --git a/INSTALL/cuda.sh b/INSTALL/cuda.sh
index 2f4f6d09..4f9e7c34 100644
--- a/INSTALL/cuda.sh
+++ b/INSTALL/cuda.sh
@@ -12,7 +12,7 @@ if [ -x "$(command -v apt)" ]; then
sudo apt-get update -y
- sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda -y --no-install-recommends
+ sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-10-2 -y --no-install-recommends
sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y
# Install CUDA DNN
diff --git a/README.md b/README.md
index 583ba94b..dc6b388c 100644
--- a/README.md
+++ b/README.md
@@ -1,146 +1,77 @@
-# Shinobi Pro
+# Shinobi Pro
### (Shinobi Open Source Software)
-Shinobi is the Open Source CCTV Solution written in Node.JS. Designed with multiple account system, Streams by WebSocket, and Save to WebM. Shinobi can record IP Cameras and Local Cameras.
+Shinobi is the Open Source CCTV Solution written in Node.JS. Designed with multiple account system, Streams by WebSocket, and Direct saving to MP4. Shinobi can record IP Cameras and Local Cameras.
-
+## Install and Use
-# Key Aspects
+- Installation : http://shinobi.video/docs/start
+- Post-Installation Tutorials : http://shinobi.video/docs/configure
+- Troubleshooting Guide : https://hub.shinobi.video/articles/view/v0AFPFchfVcFGUS
-For an updated list of features visit the official website. http://shinobi.video/features
+#### Docker
+- Install with **Docker** : https://gitlab.com/Shinobi-Systems/Shinobi/-/tree/dev/Docker
-- Time-lapse Viewer (Watch a hours worth of footage in a few minutes)
-- 2-Factor Authentication
-- Defeats stream limit imposed by browsers
- - With Base64 (Stream Type) and JPEG Mode (Option)
-- Records IP Cameras and Local Cameras
-- Streams by WebSocket, HLS (includes audio), and MJPEG
-- Save to WebM and MP4
- - Can save Audio
-- Push Events - When a video is finished it will appear in the dashboard without a refresh
-- Region Motion Detection (Similar to ZoneMinder Zone Detection)
- - Represented by a Motion Guage on each monitor
-- "No Motion" Notifications
-- 1 Process for Each Camera to do both, Recording and Streaming
-- Timeline for viewing Motion Events and Videos
-- Sub-Accounts with permissions
- - Monitor Viewing
- - Monitor Editing
- - Video Deleting
- - Separate API keys for sub account
-- Cron Filters can be set based on master account
-- Stream Analyzer built-in (FFprobe GUI)
-- Monitor Groups
-- Can snapshot images from stream directly
-- Lower Bandwith Mode (JPEG Mode)
- - Snapshot (cgi-bin) must be enabled in Monitor Settings
-- Control Cameras from Interface
-- API
- - Get videos
- - Get monitors
- - Change monitor modes : Disabled, Watch, Record
- - Embedding streams
-- Dashboard Framework made with Google Material Design Lite, jQuery, and Bootstrap
+## "is my camera supported?"
+
+Ask yourself these questions to get a general sense.
+
+- Does it have ONVIF?
+ - If yes, then it may have H.264 or H.265 streaming capability.
+- Does it have RTSP Protocol for Streaming?
+ - If yes, then it may have H.264 or H.265 streaming capability.
+- Can you stream it in VLC Player?
+ - If yes, use that same URL in Shinobi. You may need to specify the port number when using `rtsp://` protocol.
+- Does it have MJPEG Streaming?
+ - While this would work in Shinobi, it is far from ideal. Please see if any of the prior questions are applicable.
+- Does it have a web interface that you can connect to directly?
+ - If yes, then you may be able to find model information that can be used to search online for a streaming URL.
+
+Configuration Guides : http://shinobi.video/docs/configure
## Asking for help
-Before asking questions it would nice if you read the docs :) http://shinobi.video
+- General Support : https://shinobi.community
+ - Please be sure to read the `#guidelines` channel after joining.
+- Business Inquiries : business@shinobi.video or the Live Chat on https://shinobi.video
-After doing so please head on over to the Discord community chat for support. https://discordapp.com/invite/mdhmvuH
+## Support the Development
-The Issues section is only for bugs with the software. Comments and feature requests may be closed without comment. http://shinobi.video/docs/contribute
+It's a proven fact that generosity makes you a happier person :) https://www.nature.com/articles/ncomms15964
-Please be considerate of developer efforts. If you have simple questions, like "what does this button do?", please be sure to have read the docs entirely before asking. If you would like to skip reading the docs and ask away you can order a support package :) http://shinobi.video/support
+Get a Mobile License to unlock extended features on the Mobile App as well as support the development!
+- Shinobi Mobile App : https://cdn.shinobi.video/installers/ShinobiMobile/
+- Get a Mobile License : https://licenses.shinobi.video/subscribe?planSubscribe=plan_G31AZ9mknNCa6z
-## Making Suggestions or Feature Requests
-
-You can post suggestions on the Forum in the Suggestions category. Please do not treat this channel like a "demands" window. Developer efforts are limited. Much more than many alternatives.
-
-when you have a suggestion please try and make the changes yourself then post a pull request to the `dev` branch. Then we can decide if it's a good change for Shinobi. If you don't know how to go about it and want to have me put it higher on my priority list you can order a support package :) Pretty Ferengi of me... but until we live in a world without money please support Shinobi :) Cheers!
-
-http://shinobi.video/support
-
-## Help make Shinobi the best Open Source CCTV Solution.
-Donate - http://shinobi.video/docs/donate
-
-Ordering a License, Paid Support, or anything from here will allow a lot more time to be spent on Shinobi.
-
-Order Support - http://shinobi.video/support
-
-# Why make this?
+## Why make this?
http://shinobi.video/why
-# What others say
+## Author
-> "After trying zoneminder without success (heavy unstable and slow) I passed to Shinobi that despite being young spins a thousand times better (I have a setup with 16 cameras recording in FHD to ~ 10fps on a pentium of ~ 2009 and I turn with load below 1.5)."
+Moe Alam, Shinobi Systems
-> *A Reddit user, /r/ItalyInformatica*
+Shinobi is developed by many contributors. Please have a look at the commits to see some of who they are :)
+https://gitlab.com/Shinobi-Systems/Shinobi/-/commits/dev
-
-
-> "I would suggest Shinobi as a NVR. It's still in the early days but works a lot better than ZoneMinder for me. I'm able to record 16 cams at 1080p 15fps continously whith no load on server (Pentium E5500 3GB RAM) where zm crashed with 6 cams at 720p. Not to mention the better interface."
-
-> *A Reddit user, /r/HomeNetworking*
-
-# How to Install and Run
-
-> FOR DOCKER USERS : Docker is not officially supported and is not recommended. The kitematic method is provided for those who wish to quickly test Shinobi. The Docker files included in the master and dev branches are maintained by the community. If you would like support with Docker please find a community member who maintains the Docker files or please refer to Docker's forum.
-
-#### Fast Install (The Ninja Way)
-
-1. Become `root` to use the installer and run Shinobi. Use one of the following to do so.
-
- - Ubuntu 17.04, 17.10
- - `sudo su`
- - CentOS 7
- - `su`
- - MacOS 10.7(+)
- - `su`
-2. Download and run the installer.
-
-```
-bash <(curl -s https://gitlab.com/Shinobi-Systems/Shinobi-Installer/raw/master/shinobi-install.sh)
-```
-
-#### Elaborate Installs
-
-Installation Tutorials - http://shinobi.video/docs/start
-
-Troubleshooting Guide - http://shinobi.video/docs/start#trouble-section
-
-# Author
-
-Moe Alam
-
-Follow Shinobi on Twitter https://twitter.com/ShinobiCCTV
-
-Join the Community Chat
-
-
-
-# Support the Development
+## Support the Development
Ordering a certificate or support package greatly boosts development. Please consider contributing :)
http://shinobi.video/support
-# Links
+## Links
-Documentation - http://shinobi.video/docs
-
-Donate - https://shinobi.video/docs/donate
-
-Tested Cameras and Systems - http://shinobi.video/docs/supported
-
-Features - http://shinobi.video/features
-
-Reddit (Forum) - https://www.reddit.com/r/ShinobiCCTV/
-
-YouTube (Tutorials) - https://www.youtube.com/channel/UCbgbBLTK-koTyjOmOxA9msQ
-
-Discord (Community Chat) - https://discordapp.com/invite/mdhmvuH
-
-Twitter (News) - https://twitter.com/ShinobiCCTV
-
-Facebook (News) - https://www.facebook.com/Shinobi-1223193167773738/?ref=bookmarks
\ No newline at end of file
+- Articles : http://hub.shinobi.video/articles
+- Documentation : http://shinobi.video/docs
+- Features List : http://shinobi.video/features
+ - Some features may not be listed.
+- Donation : http://shinobi.video/docs/donate
+- Buy Shinobi Stuff : https://licenses.shinobi.video
+- User Submitted Configurations : http://shinobi.video/docs/cameras
+- Features : http://shinobi.video/features
+- Reddit (Forum) : https://www.reddit.com/r/ShinobiCCTV/
+- YouTube (Tutorials) : https://www.youtube.com/channel/UCbgbBLTK-koTyjOmOxA9msQ
+- Discord (Community Chat) : https://discordapp.com/invite/mdhmvuH
+- Twitter (News) : https://twitter.com/ShinobiCCTV
+- Facebook (News) : https://www.facebook.com/Shinobi-1223193167773738/?ref=bookmarks
diff --git a/camera.js b/camera.js
index b4b0e17b..de6657fd 100644
--- a/camera.js
+++ b/camera.js
@@ -1,6 +1,6 @@
//
// Shinobi
-// Copyright (C) 2016 Moe Alam, moeiscool
+// Copyright (C) 2020 Moe Alam, moeiscool
//
//
// # Donate
@@ -57,8 +57,6 @@ require('./libs/ffmpeg.js')(s,config,lang,function(ffmpeg){
require('./libs/events.js')(s,config,lang)
//recording functions
require('./libs/videos.js')(s,config,lang)
- //branding functions and config defaults
- require('./libs/videoDropInServer.js')(s,config,lang,app,io)
//plugins : websocket connected services..
require('./libs/plugins.js')(s,config,lang,io)
//health : cpu and ram trackers..
diff --git a/cron.js b/cron.js
index 0867892f..536dc116 100644
--- a/cron.js
+++ b/cron.js
@@ -135,6 +135,132 @@ s.sqlQuery = function(query,values,onMoveOn){
}
})
}
+const processSimpleWhereCondition = (dbQuery,where,didOne) => {
+ var whereIsArray = where instanceof Array;
+ if(where[0] === 'or' || where.__separator === 'or'){
+ if(whereIsArray){
+ where.shift()
+ dbQuery.orWhere(...where)
+ }else{
+ where = cleanSqlWhereObject(where)
+ dbQuery.orWhere(where)
+ }
+ }else if(!didOne){
+ didOne = true
+ whereIsArray ? dbQuery.where(...where) : dbQuery.where(where)
+ }else{
+ whereIsArray ? dbQuery.andWhere(...where) : dbQuery.andWhere(where)
+ }
+}
+const processWhereCondition = (dbQuery,where,didOne) => {
+ var whereIsArray = where instanceof Array;
+ if(!where[0])return;
+ if(where[0] && where[0] instanceof Array){
+ dbQuery.where(function() {
+ var _this = this
+ var didOneInsideGroup = false
+ where.forEach((whereInsideGroup) => {
+ processWhereCondition(_this,whereInsideGroup,didOneInsideGroup)
+ })
+ })
+ }else if(where[0] && where[0] instanceof Object){
+ dbQuery.where(function() {
+ var _this = this
+ var didOneInsideGroup = false
+ where.forEach((whereInsideGroup) => {
+ processSimpleWhereCondition(_this,whereInsideGroup,didOneInsideGroup)
+ })
+ })
+ }else{
+ processSimpleWhereCondition(dbQuery,where,didOne)
+ }
+}
+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())
+ console.error('knexError----------------------------------- END')
+}
+const knexQuery = (options,callback) => {
+ try{
+ if(!s.databaseEngine)return// console.log('Database Not Set');
+ // options = {
+ // action: "",
+ // columns: "",
+ // table: ""
+ // }
+ var dbQuery
+ switch(options.action){
+ case'select':
+ options.columns = options.columns.indexOf(',') === -1 ? [options.columns] : options.columns.split(',');
+ dbQuery = s.databaseEngine.select(...options.columns).from(options.table)
+ break;
+ case'count':
+ options.columns = options.columns.indexOf(',') === -1 ? [options.columns] : options.columns.split(',');
+ dbQuery = s.databaseEngine(options.table)
+ dbQuery.count(options.columns)
+ break;
+ case'update':
+ dbQuery = s.databaseEngine(options.table).update(options.update)
+ break;
+ case'delete':
+ dbQuery = s.databaseEngine(options.table)
+ break;
+ case'insert':
+ dbQuery = s.databaseEngine(options.table).insert(options.insert)
+ break;
+ }
+ if(options.where instanceof Array){
+ var didOne = false;
+ options.where.forEach((where) => {
+ processWhereCondition(dbQuery,where,didOne)
+ })
+ }else if(options.where instanceof Object){
+ dbQuery.where(options.where)
+ }
+ if(options.action === 'delete'){
+ dbQuery.del()
+ }
+ if(options.orderBy){
+ dbQuery.orderBy(...options.orderBy)
+ }
+ if(options.groupBy){
+ dbQuery.groupBy(options.groupBy)
+ }
+ if(options.limit){
+ if(`${options.limit}`.indexOf(',') === -1){
+ dbQuery.limit(options.limit)
+ }else{
+ const limitParts = `${options.limit}`.split(',')
+ dbQuery.limit(limitParts[0]).offset(limitParts[1])
+ }
+ }
+ if(config.debugLog === true){
+ console.log(dbQuery.toString())
+ }
+ if(callback || options.update || options.insert || options.action === 'delete'){
+ dbQuery.asCallback(function(err,r) {
+ if(err){
+ knexError(dbQuery,options,err)
+ }
+ if(callback)callback(err,r)
+ if(config.debugLogVerbose && config.debugLog === true){
+ s.debugLog('s.knexQuery QUERY',JSON.stringify(options,null,3))
+ s.debugLog('s.knexQuery RESPONSE',JSON.stringify(r,null,3))
+ s.debugLog('STACK TRACE, NOT AN ',new Error())
+ }
+ })
+ }
+ return dbQuery
+ }catch(err){
+ if(callback)callback(err,[])
+ knexError(dbQuery,options,err)
+ }
+}
s.debugLog = function(arg1,arg2){
if(config.debugLog === true){
@@ -236,29 +362,24 @@ const checkFilterRules = function(v,callback){
var b = v.d.filters[m];
s.debugLog(b)
if(b.enabled==="1"){
- b.ar=[v.ke];
- b.sql=[];
- b.where.forEach(function(j,k){
- if(j.p1==='ke'){j.p3=v.ke}
- switch(j.p3_type){
- case'function':
- b.sql.push(j.p1+' '+j.p2+' '+j.p3)
- break;
- default:
- b.sql.push(j.p1+' '+j.p2+' ?')
- b.ar.push(j.p3)
- break;
- }
+ const whereQuery = [
+ ['ke','=',v.ke],
+ ['status','!=',"0"],
+ ['details','NOT LIKE','%"archived":"1"%'],
+ ]
+ b.where.forEach(function(condition){
+ if(condition.p1 === 'ke'){condition.p3 = v.ke}
+ whereQuery.push([condition.p1,condition.p2 || '=',condition.p3])
})
- b.sql='WHERE ke=? AND status != 0 AND details NOT LIKE \'%"archived":"1"%\' AND ('+b.sql.join(' AND ')+')';
- if(b.sort_by&&b.sort_by!==''){
- b.sql+=' ORDER BY `'+b.sort_by+'` '+b.sort_by_direction
- }
- if(b.limit&&b.limit!==''){
- b.sql+=' LIMIT '+b.limit
- }
- s.sqlQuery('SELECT * FROM Videos '+b.sql,b.ar,function(err,r){
- if(r&&r[0]){
+ knexQuery({
+ action: "select",
+ columns: "*",
+ table: "Videos",
+ where: whereQuery,
+ orderBy: [b.sort_by,b.sort_by_direction.toLowerCase()],
+ limit: b.limit
+ },(err,r) => {
+ if(r && r[0]){
if(r.length > 0 || config.debugLog === true){
s.cx({f:'filterMatch',msg:r.length+' SQL rows match "'+m+'"',ke:v.ke,time:moment()})
}
@@ -309,10 +430,19 @@ const deleteRowsWithNoVideo = function(v,callback){
)
){
s.alreadyDeletedRowsWithNoVideosOnStart[v.ke]=true;
- es={};
- s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND status!=0 AND details NOT LIKE \'%"archived":"1"%\' AND time < ?',[v.ke,s.sqlDate('10 MINUTE')],function(err,evs){
- if(evs&&evs[0]){
- es.del=[];es.ar=[v.ke];
+ knexQuery({
+ action: "select",
+ columns: "*",
+ table: "Videos",
+ where: [
+ ['ke','=',v.ke],
+ ['status','!=','0'],
+ ['details','NOT LIKE','%"archived":"1"%'],
+ ['time','<',s.sqlDate('10 MINUTE')],
+ ]
+ },(err,evs) => {
+ if(evs && evs[0]){
+ const videosToDelete = [];
evs.forEach(function(ev){
var filename
var details
@@ -337,8 +467,8 @@ const deleteRowsWithNoVideo = function(v,callback){
s.tx({f:'video_delete',filename:filename+'.'+ev.ext,mid:ev.mid,ke:ev.ke,time:ev.time,end:s.moment(new Date,'YYYY-MM-DD HH:mm:ss')},'GRP_'+ev.ke);
}
});
- if(es.del.length>0 || config.debugLog === true){
- s.cx({f:'deleteNoVideo',msg:es.del.length+' SQL rows with no file deleted',ke:v.ke,time:moment()})
+ if(videosToDelete.length>0 || config.debugLog === true){
+ s.cx({f:'deleteNoVideo',msg:videosToDelete.length+' SQL rows with no file deleted',ke:v.ke,time:moment()})
}
}
setTimeout(function(){
@@ -353,7 +483,14 @@ const deleteRowsWithNoVideo = function(v,callback){
const deleteOldLogs = function(v,callback){
if(!v.d.log_days||v.d.log_days==''){v.d.log_days=10}else{v.d.log_days=parseFloat(v.d.log_days)};
if(config.cron.deleteLogs===true&&v.d.log_days!==0){
- s.sqlQuery("DELETE FROM Logs WHERE ke=? AND `time` < ?",[v.ke,s.sqlDate(v.d.log_days+' DAY')],function(err,rrr){
+ knexQuery({
+ action: "delete",
+ table: "Logs",
+ where: [
+ ['ke','=',v.ke],
+ ['time','<',s.sqlDate(v.d.log_days+' DAY')],
+ ]
+ },(err,rrr) => {
callback()
if(err)return console.error(err);
if(rrr.affectedRows && rrr.affectedRows.length>0 || config.debugLog === true){
@@ -368,7 +505,14 @@ const deleteOldLogs = function(v,callback){
const deleteOldEvents = function(v,callback){
if(!v.d.event_days||v.d.event_days==''){v.d.event_days=10}else{v.d.event_days=parseFloat(v.d.event_days)};
if(config.cron.deleteEvents===true&&v.d.event_days!==0){
- s.sqlQuery("DELETE FROM Events WHERE ke=? AND `time` < ?",[v.ke,s.sqlDate(v.d.event_days+' DAY')],function(err,rrr){
+ knexQuery({
+ action: "delete",
+ table: "Events",
+ where: [
+ ['ke','=',v.ke],
+ ['time','<',s.sqlDate(v.d.event_days+' DAY')],
+ ]
+ },(err,rrr) => {
callback()
if(err)return console.error(err);
if(rrr.affectedRows && rrr.affectedRows.length > 0 || config.debugLog === true){
@@ -383,7 +527,14 @@ const deleteOldEvents = function(v,callback){
const deleteOldEventCounts = function(v,callback){
if(!v.d.event_days||v.d.event_days==''){v.d.event_days=10}else{v.d.event_days=parseFloat(v.d.event_days)};
if(config.cron.deleteEvents===true&&v.d.event_days!==0){
- s.sqlQuery("DELETE FROM `Events Counts` WHERE ke=? AND `time` < ?",[v.ke,s.sqlDate(v.d.event_days+' DAY')],function(err,rrr){
+ knexQuery({
+ action: "delete",
+ table: "Events Counts",
+ where: [
+ ['ke','=',v.ke],
+ ['time','<',s.sqlDate(v.d.event_days+' DAY')],
+ ]
+ },(err,rrr) => {
callback()
if(err && err.code !== 'ER_NO_SUCH_TABLE')return console.error(err);
if(rrr.affectedRows && rrr.affectedRows.length > 0 || config.debugLog === true){
@@ -399,7 +550,15 @@ const deleteOldFileBins = function(v,callback){
if(!v.d.fileBin_days||v.d.fileBin_days==''){v.d.fileBin_days=10}else{v.d.fileBin_days=parseFloat(v.d.fileBin_days)};
if(config.cron.deleteFileBins===true&&v.d.fileBin_days!==0){
var fileBinQuery = " FROM Files WHERE ke=? AND `time` < ?";
- s.sqlQuery("SELECT *"+fileBinQuery,[v.ke,s.sqlDate(v.d.fileBin_days+' DAY')],function(err,files){
+ knexQuery({
+ action: "select",
+ columns: "*",
+ table: "Files",
+ where: [
+ ['ke','=',v.ke],
+ ['time','<',s.sqlDate(v.d.fileBin_days+' DAY')],
+ ]
+ },(err,files) => {
if(files&&files[0]){
//delete the files
files.forEach(function(file){
@@ -408,7 +567,14 @@ const deleteOldFileBins = function(v,callback){
})
})
//delete the database rows
- s.sqlQuery("DELETE"+fileBinQuery,[v.ke,v.d.fileBin_days],function(err,rrr){
+ knexQuery({
+ action: "delete",
+ table: "Files",
+ where: [
+ ['ke','=',v.ke],
+ ['time','<',s.sqlDate(v.d.fileBin_days+' DAY')],
+ ]
+ },(err,rrr) => {
callback()
if(err)return console.error(err);
if(rrr.affectedRows && rrr.affectedRows.length>0 || config.debugLog === true){
@@ -451,7 +617,14 @@ const processUser = function(number,rows){
if(!v.d.size||v.d.size==''){v.d.size=10000}else{v.d.size=parseFloat(v.d.size)};
//days to keep videos
if(!v.d.days||v.d.days==''){v.d.days=5}else{v.d.days=parseFloat(v.d.days)};
- s.sqlQuery('SELECT * FROM Monitors WHERE ke=?', [v.ke], function(err,rr) {
+ knexQuery({
+ action: "select",
+ columns: "*",
+ table: "Monitors",
+ where: [
+ ['ke','=',v.ke],
+ ]
+ },(err,rr) => {
if(!v.d.filters||v.d.filters==''){
v.d.filters={};
}
@@ -522,7 +695,14 @@ const clearCronInterval = function(){
}
const doCronJobs = function(){
s.cx({f:'start',time:moment()})
- s.sqlQuery('SELECT ke,uid,details,mail FROM Users WHERE details NOT LIKE \'%"sub"%\'', function(err,rows) {
+ knexQuery({
+ action: "select",
+ columns: "ke,uid,details,mail",
+ table: "Users",
+ where: [
+ ['details','NOT LIKE','%"sub"%'],
+ ]
+ },(err,rows) => {
if(err){
console.error(err)
}
diff --git a/languages/en_CA.json b/languages/en_CA.json
index 4ec5f06a..e346a099 100644
--- a/languages/en_CA.json
+++ b/languages/en_CA.json
@@ -199,6 +199,7 @@
"File Type": "File Type",
"Filesize": "Filesize",
"Video Status": "Video Status",
+ "Custom Auto Load": "Custom Auto Load",
"Preferences": "Preferences",
"Equal to": "Equal to",
"Not Equal to": "Not Equal to",
@@ -481,7 +482,15 @@
"StreamText": "
This section will designate the primary method of streaming out and its settings. This stream will be displayed in the dashboard. If you choose to use HLS, JPEG, or MJPEG then you can consume the stream through other programs.
Using JPEG stream essentially turns off the primary stream and uses the snapshot bin to get frames.
", "DetectorText": "When the Width and Height boxes are shown you should set them to 640x480 or below. This will optimize the read speed of frames.
", "RecordingText": "It is recommended that you set Record File Type to WebMMP4 and Video Codec to libvpxcopy or libx264 because your Input Type is set to .", + "'Already Installing...'": "'Already Installing...'", + "Time Created": "Time Created", + "Last Modified": "Last Modified", "Mode": "Mode", + "Run Installer": "Run Installer", + "Install": "Install", + "Enable": "Enable", + "Disable": "Disable", + "Delete": "Delete", "Name": "Name", "Skip Ping": "Skip Ping", "Retry Connection": "Retry Connection Number of times allowed to fail", @@ -578,6 +587,7 @@ "Attach Video Clip": "Attach Video Clip", "Error While Decoding": "Error While Decoding", "ErrorWhileDecodingText": "Your hardware may have an unstable connection to the network. Check your network connections.", + "ErrorWhileDecodingTextAudio": "Your camera is providing broken data. Try disabling the Audio in the camera's internal settings.", "Discord": "Discord", "Discord Alert on Trigger": "Discord Alert on Trigger", "Allow Next Email": "Allow Next Email in Minutes", @@ -929,6 +939,8 @@ "vda": "vda (Apple VDA Hardware Acceleration)", "videotoolbox": "videotoolbox", "cuvid": "cuvid (NVIDIA NVENC)", + "cuda": "cuda (NVIDIA NVENC)", + "opencl": "OpenCL", "Main": "Main", "Storage Location": "Storage Location", "Recommended": "Recommended", diff --git a/libs/auth.js b/libs/auth.js index a4ce1b32..a3757a5e 100644 --- a/libs/auth.js +++ b/libs/auth.js @@ -8,14 +8,30 @@ module.exports = function(s,config,lang){ // var getUserByUid = function(params,columns,callback){ if(!columns)columns = '*' - s.sqlQuery(`SELECT ${columns} FROM Users WHERE uid=? AND ke=?`,[params.uid,params.ke],function(err,r){ + s.knexQuery({ + action: "select", + columns: columns, + table: "Users", + where: [ + ['uid','=',params.uid], + ['ke','=',params.ke], + ] + },(err,r) => { if(!r)r = [] var user = r[0] callback(err,user) }) } var getUserBySessionKey = function(params,callback){ - s.sqlQuery('SELECT * FROM Users WHERE auth=? AND ke=?',[params.auth,params.ke],function(err,r){ + s.knexQuery({ + action: "select", + columns: '*', + table: "Users", + where: [ + ['auth','=',params.auth], + ['ke','=',params.ke], + ] + },(err,r) => { if(!r)r = [] var user = r[0] callback(err,user) @@ -23,7 +39,18 @@ module.exports = function(s,config,lang){ } var loginWithUsernameAndPassword = function(params,columns,callback){ if(!columns)columns = '*' - s.sqlQuery(`SELECT ${columns} FROM Users WHERE mail=? AND (pass=? OR pass=?) LIMIT 1`,[params.username,params.password,s.createHash(params.password)],function(err,r){ + s.knexQuery({ + action: "select", + columns: columns, + table: "Users", + where: [ + ['mail','=',params.username], + ['pass','=',params.password], + ['or','mail','=',params.username], + ['pass','=',s.createHash(params.password)], + ], + limit: 1 + },(err,r) => { if(!r)r = [] var user = r[0] callback(err,user) @@ -31,7 +58,15 @@ module.exports = function(s,config,lang){ } var getApiKey = function(params,columns,callback){ if(!columns)columns = '*' - s.sqlQuery(`SELECT ${columns} FROM API WHERE code=? AND ke=?`,[params.auth,params.ke],function(err,r){ + s.knexQuery({ + action: "select", + columns: columns, + table: "API", + where: [ + ['code','=',params.auth], + ['ke','=',params.ke], + ] + },(err,r) => { if(!r)r = [] var apiKey = r[0] callback(err,apiKey) @@ -226,7 +261,14 @@ module.exports = function(s,config,lang){ } var foundUser = function(){ if(params.users === true){ - s.sqlQuery('SELECT * FROM Users WHERE details NOT LIKE ?',['%"sub"%'],function(err,r) { + s.knexQuery({ + action: "select", + columns: "*", + table: "Users", + where: [ + ['details','NOT LIKE','%"sub"%'], + ] + },(err,r) => { adminUsersSelected = r success() }) diff --git a/libs/basic.js b/libs/basic.js index c5903aa1..b3c0d1ba 100644 --- a/libs/basic.js +++ b/libs/basic.js @@ -214,20 +214,27 @@ module.exports = function(s,config){ if(!e){e=''} if(config.systemLog===true){ if(typeof q==='string'&&s.databaseEngine){ - s.sqlQuery('INSERT INTO Logs (ke,mid,info) VALUES (?,?,?)',['$','$SYSTEM',s.s({type:q,msg:w})]); + s.knexQuery({ + action: "insert", + table: "Logs", + insert: { + ke: '$', + mid: '$SYSTEM', + info: s.s({type:q,msg:w}), + } + }) s.tx({f:'log',log:{time:s.timeObject(),ke:'$',mid:'$SYSTEM',time:s.timeObject(),info:s.s({type:q,msg:w})}},'$'); } return console.log(s.timeObject().format(),q,w,e) } } //system log - s.debugLog = function(q,w,e){ + s.debugLog = function(...args){ if(config.debugLog === true){ - if(!w){w = ''} - if(!e){e = ''} - console.log(s.timeObject().format(),q,w,e) + var logRow = ([s.timeObject().format()]).concat(...args) + console.log(...logRow) if(config.debugLogVerbose === true){ - console.log(new Error()) + console.log(new Error('VERBOSE STACK TRACE, THIS IS NOT AN ')) } } } diff --git a/libs/cameraThread/singleCamera.js b/libs/cameraThread/singleCamera.js index 0536e665..b922ecf3 100644 --- a/libs/cameraThread/singleCamera.js +++ b/libs/cameraThread/singleCamera.js @@ -18,7 +18,7 @@ var stdioWriters = []; var writeToStderr = function(text){ try{ - stdioWriters[2].write(Buffer.from(`${text}`, 'utf8' )) + process.stderr.write(Buffer.from(`${text}`, 'utf8' )) // stdioWriters[2].write(Buffer.from(`${new Error('writeToStderr').stack}`, 'utf8' )) }catch(err){ // fs.appendFileSync('/home/Shinobi/test.log',text + '\n','utf8') @@ -45,10 +45,14 @@ process.on('uncaughtException', function (err) { writeToStderr(err.stack); }); const exitAction = function(){ - if(isWindows){ - spawn("taskkill", ["/pid", cameraProcess.pid, '/f', '/t']) - }else{ - process.kill(-cameraProcess.pid) + try{ + if(isWindows){ + spawn("taskkill", ["/pid", cameraProcess.pid, '/f', '/t']) + }else{ + process.kill(-cameraProcess.pid) + } + }catch(err){ + } } process.on('SIGTERM', exitAction); @@ -58,7 +62,7 @@ process.on('exit', exitAction); for(var i=0; i < stdioPipes; i++){ switch(i){ case 0: - newPipes[i] = null + newPipes[i] = 'pipe' break; case 1: newPipes[i] = 1 @@ -115,7 +119,7 @@ if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detecto writeToStderr(err.stack) } } - + if(rawMonitorConfig.type === 'jpeg'){ var recordingSnapRequest var recordingSnapper @@ -197,3 +201,13 @@ if(rawMonitorConfig.type === 'jpeg'){ captureOne() },5000) } + +if( + rawMonitorConfig.type === 'dashcam' || + rawMonitorConfig.type === 'socket' +){ + process.stdin.on('data',(data) => { + //confirmed receiving data this way. + cameraProcess.stdin.write(data) + }) +} diff --git a/libs/cameraThread/snapshot.js b/libs/cameraThread/snapshot.js index 793e0589..4e06b647 100644 --- a/libs/cameraThread/snapshot.js +++ b/libs/cameraThread/snapshot.js @@ -1,7 +1,9 @@ const fs = require('fs') const spawn = require('child_process').spawn +const isWindows = process.platform === "win32"; var writeToStderr = function(text){ // fs.appendFileSync(rawMonitorConfig.sdir + 'errors.log',text + '\n','utf8') + process.stderr.write(Buffer.from(`${text}`, 'utf8' )) } if(!process.argv[2] || !process.argv[3]){ return writeToStderr('Missing FFMPEG Command String or no command operator') @@ -16,7 +18,11 @@ const exitAction = function(){ if(isWindows){ spawn("taskkill", ["/pid", snapProcess.pid, '/f', '/t']) }else{ - process.kill(-snapProcess.pid) + try{ + process.kill(-snapProcess.pid) + }catch(err){ + + } } } process.on('SIGTERM', exitAction); @@ -31,7 +37,6 @@ const temporaryImageFile = jsonData.temporaryImageFile const iconImageFile = jsonData.iconImageFile const useIcon = jsonData.useIcon const rawMonitorConfig = jsonData.rawMonitorConfig - // var writeToStderr = function(text){ // process.stderr.write(Buffer.from(text)) // } @@ -44,7 +49,7 @@ snapProcess.stdout.on('data',(data)=>{ writeToStderr(data.toString()) }) snapProcess.on('close',function(data){ - if(useIcon === true){ + if(useIcon){ var fileCopy = fs.createReadStream(temporaryImageFile).pipe(fs.createWriteStream(iconImageFile)) fileCopy.on('close',function(){ process.exit(); diff --git a/libs/childNode.js b/libs/childNode.js index 8610c19c..e7207d65 100644 --- a/libs/childNode.js +++ b/libs/childNode.js @@ -3,6 +3,7 @@ var http = require('http'); var https = require('https'); var express = require('express'); module.exports = function(s,config,lang,app,io){ + const { cameraDestroy } = require('./monitor/utils.js')(s,config,lang) //setup Master for childNodes if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){ s.childNodes = {}; @@ -66,6 +67,11 @@ module.exports = function(s,config,lang,app,io){ cn.emit('c',{f:'sqlCallback',rows:rows,err:err,callbackId:d.callbackId}); }); break; + case'knex': + s.knexQuery(d.options,function(err,rows){ + cn.emit('c',{f:'sqlCallback',rows:rows,err:err,callbackId:d.callbackId}); + }); + break; case'clearCameraFromActiveList': if(s.childNodes[ipAddress])delete(s.childNodes[ipAddress].activeCameras[d.ke + d.id]) break; @@ -152,9 +158,9 @@ module.exports = function(s,config,lang,app,io){ extender(d.d,insert) }) //purge over max - s.purgeDiskForGroup(d) + s.purgeDiskForGroup(d.ke) //send new diskUsage values - s.setDiskUsedForGroup(d,insert.filesizeMB) + s.setDiskUsedForGroup(d.ke,insert.filesizeMB) clearTimeout(s.group[d.ke].activeMonitors[d.mid].recordingChecker) clearTimeout(s.group[d.ke].activeMonitors[d.mid].streamChecker) break; @@ -213,11 +219,19 @@ module.exports = function(s,config,lang,app,io){ s.queuedSqlCallbacks[callbackId] = onMoveOn s.cx({f:'sql',query:query,values:values,callbackId:callbackId}); } - setInterval(function(){ - s.cpuUsage(function(cpu){ - s.cx({f:'cpu',cpu:parseFloat(cpu)}) + s.knexQuery = function(options,onMoveOn){ + var callbackId = s.gid() + if(typeof onMoveOn !== 'function'){onMoveOn=function(){}} + s.queuedSqlCallbacks[callbackId] = onMoveOn + s.cx({f:'knex',options:options,callbackId:callbackId}); + } + setInterval(async () => { + const cpu = await s.cpuUsage() + s.cx({ + f: 'cpu', + cpu: parseFloat(cpu) }) - },2000) + },5000) childIO.on('connect', function(d){ console.log('CHILD CONNECTION SUCCESS') s.cx({ @@ -241,7 +255,7 @@ module.exports = function(s,config,lang,app,io){ break; case'kill': s.initiateMonitorObject(d.d); - s.cameraDestroy(s.group[d.d.ke].activeMonitors[d.d.id].spawn,d.d) + cameraDestroy(d.d) var childNodeIp = s.group[d.d.ke].activeMonitors[d.d.id] break; case'sync': diff --git a/libs/common.js b/libs/common.js new file mode 100644 index 00000000..ce8cab4f --- /dev/null +++ b/libs/common.js @@ -0,0 +1,11 @@ +const async = require("async"); +exports.copyObject = (obj) => { + return Object.assign({},obj) +} +exports.createQueue = (timeoutInSeconds, queueItemsRunningInParallel) => { + return async.queue(function(action, callback) { + setTimeout(function(){ + action(callback) + },timeoutInSeconds * 1000 || 1000) + },queueItemsRunningInParallel || 3) +} diff --git a/libs/control/onvif.js b/libs/control/onvif.js index fa962b8b..bd668524 100644 --- a/libs/control/onvif.js +++ b/libs/control/onvif.js @@ -9,7 +9,7 @@ module.exports = function(s,config,lang,app,io){ const controlURLOptions = s.cameraControlOptionsFromUrl(controlBaseUrl,monitorConfig) //create onvif connection const device = new onvif.OnvifDevice({ - xaddr : 'http://' + controlURLOptions.host + ':' + controlURLOptions.port + '/onvif/device_service', + address : controlURLOptions.host + ':' + controlURLOptions.port, user : controlURLOptions.username, pass : controlURLOptions.password }) diff --git a/libs/control/ptz.js b/libs/control/ptz.js index 5b37d33c..566371b8 100644 --- a/libs/control/ptz.js +++ b/libs/control/ptz.js @@ -1,5 +1,6 @@ var os = require('os'); var exec = require('child_process').exec; +var request = require('request') module.exports = function(s,config,lang,app,io){ const moveLock = {} const startMove = async function(options,callback){ @@ -195,24 +196,28 @@ module.exports = function(s,config,lang,app,io){ }else{ const controlUrlStopTimeout = parseInt(monitorConfig.details.control_url_stop_timeout) || 1000 var stopCamera = function(){ - var stopURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}_stop`] - var options = s.cameraControlOptionsFromUrl(stopURL,monitorConfig) - var requestOptions = { + let stopURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}_stop`] + let controlOptions = s.cameraControlOptionsFromUrl(stopURL,monitorConfig) + let requestOptions = { url : stopURL, - method : options.method, + method : controlOptions.method, auth : { - user : options.username, - pass : options.password + user : controlOptions.username, + pass : controlOptions.password } } if(monitorConfig.details.control_digest_auth === '1'){ requestOptions.sendImmediately = true } request(requestOptions,function(err,data){ + const msg = { + ok: true, + type:'Control Trigger Ended' + } if(err){ - var msg = {ok:false,type:'Control Error',msg:err} - }else{ - var msg = {ok:true,type:'Control Trigger Ended'} + msg.ok = false + msg.type = 'Control Error' + msg.msg = err } callback(msg) s.userLog(e,msg); @@ -221,12 +226,14 @@ module.exports = function(s,config,lang,app,io){ if(options.direction === 'stopMove'){ stopCamera() }else{ - var requestOptions = { + let controlURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}`] + let controlOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig) + let requestOptions = { url: controlURL, - method: controlURLOptions.method, + method: controlOptions.method, auth: { - user: controlURLOptions.username, - pass: controlURLOptions.password + user: controlOptions.username, + pass: controlOptions.password } } if(monitorConfig.details.control_digest_auth === '1'){ diff --git a/libs/customAutoLoad.js b/libs/customAutoLoad.js index 7f954104..96824c5a 100644 --- a/libs/customAutoLoad.js +++ b/libs/customAutoLoad.js @@ -1,153 +1,434 @@ -var fs = require('fs') -var express = require('express') -module.exports = function(s,config,lang,app,io){ - - s.customAutoLoadModules = {} - s.customAutoLoadTree = { - pages: [], - PageBlocks: [], - LibsJs: [], - LibsCss: [], - adminPageBlocks: [], - adminLibsJs: [], - adminLibsCss: [], - superPageBlocks: [], - superLibsJs: [], - superLibsCss: [] +const fs = require('fs-extra'); +const express = require('express') +const request = require('request') +const unzipper = require('unzipper') +const fetch = require("node-fetch") +const spawn = require('child_process').spawn +module.exports = async (s,config,lang,app,io) => { + const runningInstallProcesses = {} + const modulesBasePath = s.mainDirectory + '/libs/customAutoLoad/' + const searchText = function(searchFor,searchIn){ + return searchIn.indexOf(searchFor) > -1 } - var folderPath = s.mainDirectory + '/libs/customAutoLoad' - var search = function(searchFor,searchIn){return searchIn.indexOf(searchFor) > -1} - fs.readdir(folderPath,function(err,folderContents){ - if(!err && folderContents){ - folderContents.forEach(function(filename){ - s.customAutoLoadModules[filename] = {} - var customModulePath = folderPath + '/' + filename - if(filename.indexOf('.js') > -1){ - s.customAutoLoadModules[filename].type = 'file' - try{ - require(customModulePath)(s,config,lang,app,io) - }catch(err){ - console.log('Failed to Load Module : ' + filename) - console.log(err) - } - }else{ - if(fs.lstatSync(customModulePath).isDirectory()){ - s.customAutoLoadModules[filename].type = 'folder' - try{ - require(customModulePath)(s,config,lang,app,io) - fs.readdir(customModulePath,function(err,folderContents){ - folderContents.forEach(function(name){ - switch(name){ - case'web': - var webFolder = s.checkCorrectPathEnding(customModulePath) + 'web/' - fs.readdir(webFolder,function(err,webFolderContents){ - webFolderContents.forEach(function(name){ - switch(name){ - case'libs': - case'pages': - if(name === 'libs'){ - if(config.webPaths.home !== '/'){ - app.use('/libs',express.static(webFolder + '/libs')) - } - app.use(s.checkCorrectPathEnding(config.webPaths.home)+'libs',express.static(webFolder + '/libs')) - app.use(s.checkCorrectPathEnding(config.webPaths.admin)+'libs',express.static(webFolder + '/libs')) - app.use(s.checkCorrectPathEnding(config.webPaths.super)+'libs',express.static(webFolder + '/libs')) - } - var libFolder = webFolder + name + '/' - fs.readdir(libFolder,function(err,webFolderContents){ - webFolderContents.forEach(function(libName){ - var thirdLevelName = libFolder + libName - switch(libName){ - case'js': - case'css': - case'blocks': - fs.readdir(thirdLevelName,function(err,webFolderContents){ - webFolderContents.forEach(function(filename){ - var fullPath = thirdLevelName + '/' + filename - var blockPrefix = '' - switch(true){ - case search('super.',filename): - blockPrefix = 'super' - break; - case search('admin.',filename): - blockPrefix = 'admin' - break; - } - switch(libName){ - case'js': - s.customAutoLoadTree[blockPrefix + 'LibsJs'].push(filename) - break; - case'css': - s.customAutoLoadTree[blockPrefix + 'LibsCss'].push(filename) - break; - case'blocks': - s.customAutoLoadTree[blockPrefix + 'PageBlocks'].push(fullPath) - break; - } - }) - }) - break; - default: - if(libName.indexOf('.ejs') > -1){ - s.customAutoLoadTree.pages.push(thirdLevelName) - } - break; - } - }) - }) - break; - } - }) - }) - break; - case'languages': - var languagesFolder = s.checkCorrectPathEnding(customModulePath) + 'languages/' - fs.readdir(languagesFolder,function(err,files){ - if(err)return console.log(err); - files.forEach(function(filename){ - var fileData = require(languagesFolder + filename) - var rule = filename.replace('.json','') - if(config.language === rule){ - lang = Object.assign(lang,fileData) - } - if(s.loadedLanguages[rule]){ - s.loadedLanguages[rule] = Object.assign(s.loadedLanguages[rule],fileData) - }else{ - s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),fileData) - } - }) - }) - break; - case'definitions': - var definitionsFolder = s.checkCorrectPathEnding(customModulePath) + 'definitions/' - fs.readdir(definitionsFolder,function(err,files){ - if(err)return console.log(err); - files.forEach(function(filename){ - var fileData = require(definitionsFolder + filename) - var rule = filename.replace('.json','').replace('.js','') - if(config.language === rule){ - s.definitions = s.mergeDeep(s.definitions,fileData) - } - if(s.loadedDefinitons[rule]){ - s.loadedDefinitons[rule] = s.mergeDeep(s.loadedDefinitons[rule],fileData) - }else{ - s.loadedDefinitons[rule] = s.mergeDeep(s.copySystemDefaultDefinitions(),fileData) - } - }) - }) - break; - } - }) + const extractNameFromPackage = (filePath) => { + const filePathParts = filePath.split('/') + const packageName = filePathParts[filePathParts.length - 1].split('.')[0] + return packageName + } + const getModulePath = (name) => { + return modulesBasePath + name + '/' + } + const getModule = (moduleName) => { + const modulePath = modulesBasePath + moduleName + const stats = fs.lstatSync(modulePath) + const isDirectory = stats.isDirectory() + const newModule = { + name: moduleName, + path: modulePath + '/', + size: stats.size, + lastModified: stats.mtime, + created: stats.ctime, + isDirectory: isDirectory, + } + if(isDirectory){ + var hasInstaller = false + if(!fs.existsSync(modulePath + '/index.js')){ + hasInstaller = true + newModule.noIndex = true + } + if(fs.existsSync(modulePath + '/package.json')){ + hasInstaller = true + newModule.properties = getModuleProperties(moduleName) + }else{ + newModule.properties = { + name: moduleName + } + } + newModule.hasInstaller = hasInstaller + }else{ + newModule.isIgnitor = (moduleName.indexOf('.js') > -1) + newModule.properties = { + name: moduleName + } + } + return newModule + } + const getModules = (asArray) => { + const foundModules = {} + fs.readdirSync(modulesBasePath).forEach((moduleName) => { + foundModules[moduleName] = getModule(moduleName) + }) + return asArray ? Object.values(foundModules) : foundModules + } + const downloadModule = (downloadUrl,packageName) => { + const downloadPath = modulesBasePath + packageName + fs.mkdirSync(downloadPath) + return new Promise(async (resolve, reject) => { + fs.mkdir(downloadPath, () => { + request(downloadUrl).pipe(fs.createWriteStream(downloadPath + '.zip')) + .on('finish',() => { + zip = fs.createReadStream(downloadPath + '.zip') + .pipe(unzipper.Parse()) + .on('entry', async (file) => { + if(file.type === 'Directory'){ + try{ + fs.mkdirSync(modulesBasePath + file.path, { recursive: true }) + }catch(err){ + + } + }else{ + const content = await file.buffer(); + fs.writeFile(modulesBasePath + file.path,content,(err) => { + if(err)console.log(err) }) - }catch(err){ - console.log('Failed to Load Module : ' + filename) - console.log(err) } + }) + .promise() + .then(() => { + fs.remove(downloadPath + '.zip', () => {}) + resolve() + }) + }) + }) + }) + } + const getModuleProperties = (name) => { + const modulePath = getModulePath(name) + const propertiesPath = modulePath + 'package.json' + const properties = fs.existsSync(propertiesPath) ? s.parseJSON(fs.readFileSync(propertiesPath)) : { + name: name + } + return properties + } + const installModule = (name) => { + return new Promise((resolve, reject) => { + if(!runningInstallProcesses[name]){ + //depending on module this may only work for Ubuntu + const modulePath = getModulePath(name) + const properties = getModuleProperties(name); + const installerPath = modulePath + `INSTALL.sh` + const propertiesPath = modulePath + 'package.json' + var installProcess + // check for INSTALL.sh (ubuntu only) + if(fs.existsSync(installerPath)){ + installProcess = spawn(`sh`,[installerPath]) + }else if(fs.existsSync(propertiesPath)){ + // no INSTALL.sh found, check for package.json and do `npm install --unsafe-perm` + installProcess = spawn(`npm`,['install','--unsafe-perm','--prefix',modulePath]) + }else{ + resolve() + } + if(installProcess){ + const sendData = (data,channel) => { + const clientData = { + f: 'module-info', + module: name, + process: 'install-' + channel, + data: data.toString(), + } + s.tx(clientData,'$') + s.debugLog(clientData) } + installProcess.stderr.on('data',(data) => { + sendData(data,'stderr') + }) + installProcess.stdout.on('data',(data) => { + sendData(data,'stdout') + }) + installProcess.on('exit',(data) => { + runningInstallProcesses[name] = null; + resolve() + }) + runningInstallProcesses[name] = installProcess + } + }else{ + resolve(lang['Already Installing...']) + } + }) + } + const disableModule = (name,status) => { + // set status to `false` to enable + const modulePath = getModulePath(name) + const properties = getModuleProperties(name); + const propertiesPath = modulePath + 'package.json' + var packageJson = { + name: name + } + try{ + packageJson = JSON.parse(fs.readFileSync(propertiesPath)) + }catch(err){ + + } + packageJson.disabled = status; + fs.writeFileSync(propertiesPath,s.prettyPrint(packageJson)) + } + const deleteModule = (name) => { + // requires restart for changes to take effect + try{ + const modulePath = modulesBasePath + name + fs.remove(modulePath, (err) => { + console.log(err) + }) + return true + }catch(err){ + console.log(err) + return false + } + } + const loadModule = (shinobiModule) => { + const moduleName = shinobiModule.name + s.customAutoLoadModules[moduleName] = {} + var customModulePath = modulesBasePath + '/' + moduleName + if(shinobiModule.isIgnitor){ + s.customAutoLoadModules[moduleName].type = 'file' + try{ + require(customModulePath)(s,config,lang,app,io) + }catch(err){ + s.systemLog('Failed to Load Module : ' + moduleName) + s.systemLog(err) + } + }else if(shinobiModule.isDirectory){ + s.customAutoLoadModules[moduleName].type = 'folder' + try{ + require(customModulePath)(s,config,lang,app,io) + fs.readdir(customModulePath,function(err,folderContents){ + folderContents.forEach(function(name){ + switch(name){ + case'web': + var webFolder = s.checkCorrectPathEnding(customModulePath) + 'web/' + fs.readdir(webFolder,function(err,webFolderContents){ + webFolderContents.forEach(function(name){ + switch(name){ + case'libs': + case'pages': + if(name === 'libs'){ + if(config.webPaths.home !== '/'){ + app.use('/libs',express.static(webFolder + '/libs')) + } + app.use(s.checkCorrectPathEnding(config.webPaths.home)+'libs',express.static(webFolder + '/libs')) + app.use(s.checkCorrectPathEnding(config.webPaths.admin)+'libs',express.static(webFolder + '/libs')) + app.use(s.checkCorrectPathEnding(config.webPaths.super)+'libs',express.static(webFolder + '/libs')) + } + var libFolder = webFolder + name + '/' + fs.readdir(libFolder,function(err,webFolderContents){ + webFolderContents.forEach(function(libName){ + var thirdLevelName = libFolder + libName + switch(libName){ + case'js': + case'css': + case'blocks': + fs.readdir(thirdLevelName,function(err,webFolderContents){ + webFolderContents.forEach(function(filename){ + var fullPath = thirdLevelName + '/' + filename + var blockPrefix = '' + switch(true){ + case searchText('super.',filename): + blockPrefix = 'super' + break; + case searchText('admin.',filename): + blockPrefix = 'admin' + break; + } + switch(libName){ + case'js': + s.customAutoLoadTree[blockPrefix + 'LibsJs'].push(filename) + break; + case'css': + s.customAutoLoadTree[blockPrefix + 'LibsCss'].push(filename) + break; + case'blocks': + s.customAutoLoadTree[blockPrefix + 'PageBlocks'].push(fullPath) + break; + } + }) + }) + break; + default: + if(libName.indexOf('.ejs') > -1){ + s.customAutoLoadTree.pages.push(thirdLevelName) + } + break; + } + }) + }) + break; + } + }) + }) + break; + case'languages': + var languagesFolder = s.checkCorrectPathEnding(customModulePath) + 'languages/' + fs.readdir(languagesFolder,function(err,files){ + if(err)return console.log(err); + files.forEach(function(filename){ + var fileData = require(languagesFolder + filename) + var rule = filename.replace('.json','') + if(config.language === rule){ + lang = Object.assign(lang,fileData) + } + if(s.loadedLanguages[rule]){ + s.loadedLanguages[rule] = Object.assign(s.loadedLanguages[rule],fileData) + }else{ + s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),fileData) + } + }) + }) + break; + case'definitions': + var definitionsFolder = s.checkCorrectPathEnding(customModulePath) + 'definitions/' + fs.readdir(definitionsFolder,function(err,files){ + if(err)return console.log(err); + files.forEach(function(filename){ + var fileData = require(definitionsFolder + filename) + var rule = filename.replace('.json','').replace('.js','') + if(config.language === rule){ + s.definitions = s.mergeDeep(s.definitions,fileData) + } + if(s.loadedDefinitons[rule]){ + s.loadedDefinitons[rule] = s.mergeDeep(s.loadedDefinitons[rule],fileData) + }else{ + s.loadedDefinitons[rule] = s.mergeDeep(s.copySystemDefaultDefinitions(),fileData) + } + }) + }) + break; + } + }) + }) + }catch(err){ + s.systemLog('Failed to Load Module : ' + moduleName) + s.systemLog(err) + } + } + } + const moveModuleToNameInProperties = (modulePath,packageRoot,properties) => { + return new Promise((resolve,reject) => { + const packageRootParts = packageRoot.split('/') + const filename = packageRootParts[packageRootParts.length - 1] + fs.move(modulePath + packageRoot,modulesBasePath + filename,(err) => { + if(packageRoot){ + fs.remove(modulePath, (err) => { + if(err)console.log(err) + resolve(filename) + }) + }else{ + resolve(filename) } }) - }else{ - fs.mkdirSync(folderPath) + }) + } + const initializeAllModules = async () => { + s.customAutoLoadModules = {} + s.customAutoLoadTree = { + pages: [], + PageBlocks: [], + LibsJs: [], + LibsCss: [], + adminPageBlocks: [], + adminLibsJs: [], + adminLibsCss: [], + superPageBlocks: [], + superLibsJs: [], + superLibsCss: [] } + fs.readdir(modulesBasePath,function(err,folderContents){ + if(!err && folderContents.length > 0){ + getModules(true).forEach((shinobiModule) => { + if(shinobiModule.properties.disabled){ + return; + } + loadModule(shinobiModule) + }) + }else{ + fs.mkdir(modulesBasePath,() => {}) + } + }) + } + /** + * API : Superuser : Custom Auto Load Package Download. + */ + app.get(config.webPaths.superApiPrefix+':auth/package/list', async (req,res) => { + s.superAuth(req.params, async (resp) => { + s.closeJsonResponse(res,{ + ok: true, + modules: getModules() + }) + },res,req) }) + /** + * API : Superuser : Custom Auto Load Package Download. + */ + app.post(config.webPaths.superApiPrefix+':auth/package/download', async (req,res) => { + s.superAuth(req.params, async (resp) => { + try{ + const url = req.body.downloadUrl + const packageRoot = req.body.packageRoot || '' + const packageName = req.body.packageName || extractNameFromPackage(url) + const modulePath = getModulePath(packageName) + await downloadModule(url,packageName) + const properties = getModuleProperties(packageName) + const newName = await moveModuleToNameInProperties(modulePath,packageRoot,properties) + const chosenName = newName ? newName : packageName + disableModule(chosenName,true) + s.closeJsonResponse(res,{ + ok: true, + moduleName: chosenName, + newModule: getModule(chosenName) + }) + }catch(err){ + s.closeJsonResponse(res,{ + ok: false, + error: err + }) + } + },res,req) + }) + /** + * API : Superuser : Custom Auto Load Package Install. + */ + app.post(config.webPaths.superApiPrefix+':auth/package/install', (req,res) => { + s.superAuth(req.params, async (resp) => { + const packageName = req.body.packageName + const response = {ok: true} + const error = await installModule(packageName) + if(error){ + response.ok = false + response.msg = error + } + s.closeJsonResponse(res,response) + },res,req) + }) + /** + * API : Superuser : Custom Auto Load Package set Status (Enabled or Disabled). + */ + app.post(config.webPaths.superApiPrefix+':auth/package/status', (req,res) => { + s.superAuth(req.params, async (resp) => { + const status = req.body.status + const packageName = req.body.packageName + const selection = status == 'true' ? true : false + disableModule(packageName,selection) + s.closeJsonResponse(res,{ok: true, status: selection}) + },res,req) + }) + /** + * API : Superuser : Custom Auto Load Package Delete + */ + app.post(config.webPaths.superApiPrefix+':auth/package/delete', async (req,res) => { + s.superAuth(req.params, async (resp) => { + const packageName = req.body.packageName + const response = deleteModule(packageName) + s.closeJsonResponse(res,{ok: response}) + },res,req) + }) + /** + * API : Superuser : Custom Auto Load Package Reload All + */ + app.post(config.webPaths.superApiPrefix+':auth/package/reloadAll', async (req,res) => { + s.superAuth(req.params, async (resp) => { + await initializeAllModules(); + s.closeJsonResponse(res,{ok: true}) + },res,req) + }) + // Initialize Modules on Start + await initializeAllModules(); } diff --git a/libs/dropInEvents.js b/libs/dropInEvents.js index b4e897de..d11ef50b 100644 --- a/libs/dropInEvents.js +++ b/libs/dropInEvents.js @@ -183,46 +183,87 @@ module.exports = function(s,config,lang,app,io){ } // FTP Server if(config.ftpServer === true){ + const authenticateConnection = (connection) => { + return new Promise((resolve,reject) => { + var username = null; + s.debugLog('client connected: ' + connection.remoteAddress); + connection.on('command:user', function(user, success, failure) { + if (user) { + username = user; + success(); + } else { + failure(); + } + }) + + connection.on('command:pass', function(password, success, failure) { + s.basicOrApiAuthentication(username,password,function(err,user){ + if(user){ + connection._user = user + success(username); + } else { + failure(); + } + resolve({ + ok: !!user, + username: username, + password: password + }) + }) + }) + }) + } createDropInEventsDirectory() if(!config.ftpServerPort)config.ftpServerPort = 21 if(!config.ftpServerUrl)config.ftpServerUrl = `ftp://0.0.0.0:${config.ftpServerPort}` config.ftpServerUrl = config.ftpServerUrl.replace('{{PORT}}',config.ftpServerPort) - const FtpSrv = require('ftp-srv') - const ftpServer = new FtpSrv({ - url: config.ftpServerUrl, - log: require('bunyan').createLogger({ - name: 'ftp-srv', - level: 100 - }), + const ftpd = require('shinobi-ftpd') + const ftpServer = new ftpd.FtpServer(config.ftpServerUrl, Object.assign({ + getInitialCwd: function(connection, callback) { + callback(null, s.dir.dropInEvents + '/' + connection._user.ke) + }, + getRoot: function() { + return s.dir.dropInEvents + }, + pasvPortRangeStart: 1025, + pasvPortRangeEnd: 1050, + allowUnauthorizedTls: true, + uploadMaxSlurpSize: 7000 + },config.ftpServerOptions || {})) + + ftpServer.on('error', function(error) { + s.debugLog(['FTP Server error:', error]); }) - ftpServer.on('login', ({connection, username, password}, resolve, reject) => { - s.basicOrApiAuthentication(username,password,function(err,user){ - if(user){ - connection.on('STOR', (error, fileName) => { - if(!fileName)return; - var pathPieces = fileName.replace(s.dir.dropInEvents,'').split('/') - var ke = pathPieces[0] - var mid = pathPieces[1] - var firstDroppedPart = pathPieces[2] - var monitorEventDropDir = s.dir.dropInEvents + ke + '/' + mid + '/' - var deleteKey = monitorEventDropDir + firstDroppedPart - onFileOrFolderFound(monitorEventDropDir + firstDroppedPart,deleteKey,{ke: ke, mid: mid}) - }) - resolve({root: s.dir.dropInEvents + user.ke}) - }else{ - // reject(new Error('Failed Authorization')) - } - }) - }) - ftpServer.on('client-error', ({connection, context, error}) => { - console.log('client-error',error) - }) - ftpServer.listen().then(() => { - s.systemLog(`FTP Server running on port ${config.ftpServerPort}...`) - }).catch(function(err){ - s.systemLog(err) + ftpServer.on('client:connected', async (connection) => { + const response = await authenticateConnection(connection) + if(connection._user){ + connection.cwd = s.dir.dropInEvents + connection._user.ke + connection.on('file:stor', async (eventType, data) => { + const fileName = data.file + const pathPieces = fileName.substr(1).split('/') + const groupKey = connection._user.ke + const monitorId = pathPieces[0] + const firstDroppedPart = pathPieces[1] + const monitorEventDropDir = s.dir.dropInEvents + groupKey + '/' + monitorId + '/' + const deleteKey = monitorEventDropDir + firstDroppedPart + onFileOrFolderFound( + monitorEventDropDir + firstDroppedPart, + deleteKey, + { + ke: groupKey, + mid: monitorId + } + ) + }) + }else{ + s.systemLog(`Failed FTP Login Attempt : ${response.username}/${response.password}`) + throw `Failed to Authenticate FTP : ${response.username}/${response.password}`; + } }) + + ftpServer.listen(config.ftpServerPort) + s.systemLog(`FTP Server running on port ${config.ftpServerPort}...`) } //add extensions s.onMonitorInit(onMonitorInit) @@ -234,7 +275,8 @@ module.exports = function(s,config,lang,app,io){ if(config.smtpServerHideStartTls === undefined)config.smtpServerHideStartTls = null var SMTPServer = require("smtp-server").SMTPServer; if(!config.smtpServerPort && (config.smtpServerSsl && config.smtpServerSsl.enabled !== false || config.ssl)){config.smtpServerPort = 465}else if(!config.smtpServerPort){config.smtpServerPort = 25} - var smtpOptions = { + config.smtpServerOptions = config.smtpServerOptions ? config.smtpServerOptions : {} + var smtpOptions = Object.assign({ logger: config.debugLog || config.smtpServerLog, hideSTARTTLS: config.smtpServerHideStartTls, onAuth(auth, session, callback) { @@ -310,7 +352,7 @@ module.exports = function(s,config,lang,app,io){ callback() } } - } + },config.smtpServerOptions) if(config.smtpServerSsl && config.smtpServerSsl.enabled !== false || config.ssl && config.ssl.cert && config.ssl.key){ var key = config.ssl.key || fs.readFileSync(config.smtpServerSsl.key) var cert = config.ssl.cert || fs.readFileSync(config.smtpServerSsl.cert) diff --git a/libs/events.js b/libs/events.js index 43c26792..a1378dbd 100644 --- a/libs/events.js +++ b/libs/events.js @@ -170,7 +170,7 @@ module.exports = function(s,config,lang){ extender(x,d) }) } - s.triggerEvent = function(d,forceSave){ + s.triggerEvent = async (d,forceSave) => { var didCountingAlready = false var filter = { halt : false, @@ -375,7 +375,9 @@ module.exports = function(s,config,lang){ time : s.formattedTime(), frame : s.group[d.ke].activeMonitors[d.id].lastJpegDetectorFrame }) - }else{ + } + // + if(currentConfig.detector_use_motion === '0' || d.doObjectDetection !== true ){ if(currentConfig.det_multi_trig === '1'){ s.getCamerasForMultiTrigger(d.mon).forEach(function(monitor){ if(monitor.mid !== d.id){ @@ -394,7 +396,16 @@ module.exports = function(s,config,lang){ } //save this detection result in SQL, only coords. not image. if(forceSave || (filter.save && currentConfig.detector_save === '1')){ - s.sqlQuery('INSERT INTO Events (ke,mid,details,time) VALUES (?,?,?,?)',[d.ke,d.id,detailString,eventTime]) + s.knexQuery({ + action: "insert", + table: "Events", + insert: { + ke: d.ke, + mid: d.id, + details: detailString, + time: eventTime, + } + }) } if(currentConfig.detector === '1' && currentConfig.detector_notrigger === '1'){ s.setNoEventsDetector(s.group[d.ke].rawMonitorConfigurations[d.id]) @@ -438,13 +449,9 @@ module.exports = function(s,config,lang){ } d.currentTime = new Date() d.currentTimestamp = s.timeObject(d.currentTime).format() - d.screenshotName = 'Motion_'+(d.mon.name.replace(/[^\w\s]/gi,''))+'_'+d.id+'_'+d.ke+'_'+s.formattedTime() + d.screenshotName = d.details.reason + '_'+(d.mon.name.replace(/[^\w\s]/gi,''))+'_'+d.id+'_'+d.ke+'_'+s.formattedTime() d.screenshotBuffer = null - s.onEventTriggerExtensions.forEach(function(extender){ - extender(d,filter) - }) - if(filter.webhook && currentConfig.detector_webhook === '1'){ var detector_webhook_url = s.addEventDetailsToString(d,currentConfig.detector_webhook_url) var webhookMethod = currentConfig.detector_webhook_method @@ -464,6 +471,11 @@ module.exports = function(s,config,lang){ if(err)s.debugLog(err) }) } + + for (var i = 0; i < s.onEventTriggerExtensions.length; i++) { + const extender = s.onEventTriggerExtensions[i] + await extender(d,filter) + } } //show client machines the event d.cx={f:'detector_trigger',id:d.id,ke:d.ke,details:d.details,doObjectDetection:d.doObjectDetection}; diff --git a/libs/ffmpeg.js b/libs/ffmpeg.js index 33c901ee..a30a0cf0 100644 --- a/libs/ffmpeg.js +++ b/libs/ffmpeg.js @@ -103,6 +103,8 @@ module.exports = function(s,config,lang,onFinish){ auto: {label:lang['Auto'],value:'auto'}, drm: {label:lang['drm'],value:'drm'}, cuvid: {label:lang['cuvid'],value:'cuvid'}, + cuda: {label:lang['cuda'],value:'cuda'}, + opencl: {label:lang['opencl'],value:'opencl'}, vaapi: {label:lang['vaapi'],value:'vaapi'}, qsv: {label:lang['qsv'],value:'qsv'}, vdpau: {label:lang['vdpau'],value:'vdpau'}, @@ -424,7 +426,7 @@ module.exports = function(s,config,lang,onFinish){ x.hwaccel = '' x.cust_input = '' //wallclock fix for strangely long, single frame videos - if(e.details.wall_clock_timestamp_ignore !== '1' && e.type === 'h264' && x.cust_input.indexOf('-use_wallclock_as_timestamps 1') === -1){x.cust_input+=' -use_wallclock_as_timestamps 1';} + if((config.wallClockTimestampAsDefault || e.details.wall_clock_timestamp_ignore !== '1') && e.type === 'h264' && x.cust_input.indexOf('-use_wallclock_as_timestamps 1') === -1){x.cust_input+=' -use_wallclock_as_timestamps 1';} //input - frame rate (capture rate) if(e.details.sfps && e.details.sfps!==''){x.input_fps=' -r '+e.details.sfps}else{x.input_fps=''} //input - analyze duration diff --git a/libs/fileBin.js b/libs/fileBin.js index 02e1f422..22de6f8c 100644 --- a/libs/fileBin.js +++ b/libs/fileBin.js @@ -1,13 +1,254 @@ var fs = require('fs') var moment = require('moment') module.exports = function(s,config,lang,app,io){ - s.getFileBinDirectory = function(e){ - if(e.mid&&!e.id){e.id=e.mid} - s.checkDetails(e) - if(e.details&&e.details.dir&&e.details.dir!==''){ - return s.checkCorrectPathEnding(e.details.dir)+e.ke+'/'+e.id+'/' - }else{ - return s.dir.fileBin+e.ke+'/'+e.id+'/'; - } + const getFileBinDirectory = function(monitor){ + return s.dir.fileBin + monitor.ke + '/' + monitor.mid + '/' } + const getFileBinEntry = (options) => { + return new Promise((resolve, reject) => { + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: options + },(err,rows) => { + if(rows[0]){ + resolve(rows[0]) + }else{ + resolve() + } + }) + }) + } + const getFileBinEntries = (options) => { + return new Promise((resolve, reject) => { + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: options + },(err,rows) => { + if(rows){ + resolve(rows) + }else{ + resolve([]) + } + }) + }) + } + const updateFileBinEntry = (options) => { + return new Promise((resolve, reject) => { + const groupKey = options.ke + const monitorId = options.mid + const filename = options.name + const update = options.update + if(!filename){ + resolve('No Filename') + return + } + if(!update){ + resolve('No Update Options') + return + } + s.knexQuery({ + action: "select", + columns: "size", + table: "Files", + where: { + ke: groupKey, + mid: monitorId, + name: filename, + } + },(err,rows) => { + if(rows[0]){ + const fileSize = rows[0].size + s.knexQuery({ + action: "update", + table: "Files", + where: { + ke: groupKey, + mid: monitorId, + name: filename, + }, + update: update + },(err) => { + resolve() + if(update.size){ + s.setDiskUsedForGroup(groupKey,-(fileSize/1048576),'fileBin') + s.setDiskUsedForGroup(groupKey,(update.size/1048576),'fileBin') + s.purgeDiskForGroup(groupKey) + } + }) + }else{ + resolve() + } + }) + }) + } + const deleteFileBinEntry = (options) => { + return new Promise((resolve, reject) => { + const groupKey = options.ke + const monitorId = options.mid + const filename = options.name + if(!filename){ + resolve('No Filename') + return + } + s.knexQuery({ + action: "select", + columns: "size", + table: "Files", + where: { + ke: groupKey, + mid: monitorId, + name: filename, + } + },(err,rows) => { + if(rows[0]){ + const fileSize = rows[0].size + s.knexQuery({ + action: "delete", + table: "Files", + where: { + ke: groupKey, + mid: monitorId, + name: filename, + } + },(err) => { + resolve() + s.setDiskUsedForGroup(groupKey,-(fileSize/1048576),'fileBin') + s.purgeDiskForGroup(groupKey) + }) + }else{ + resolve() + } + }) + }) + } + const insertFileBinEntry = (options) => { + return new Promise((resolve, reject) => { + const groupKey = options.ke + const monitorId = options.mid + const filename = options.name + if(!filename){ + resolve('No Filename') + return + } + const monitorFileBinDirectory = getFileBinDirectory({ke: groupKey,mid: monitorId,}) + const fileSize = options.size || fs.lstatSync(monitorFileBinDirectory + filename).size + const details = options.details instanceof Object ? JSON.stringify(options.details) : options.details + const status = options.status || 0 + const time = options.time || new Date() + s.knexQuery({ + action: "insert", + table: "Files", + insert: { + ke: groupKey, + mid: monitorId, + name: filename, + size: fileSize, + details: details, + status: status, + time: time, + } + },(err) => { + resolve() + s.setDiskUsedForGroup(groupKey,(fileSize/1048576),'fileBin') + s.purgeDiskForGroup(groupKey) + }) + }) + } + s.getFileBinDirectory = getFileBinDirectory + s.getFileBinEntry = getFileBinEntry + s.insertFileBinEntry = insertFileBinEntry + s.updateFileBinEntry = updateFileBinEntry + s.deleteFileBinEntry = deleteFileBinEntry + /** + * API : Get fileBin file rows + */ + app.get([config.webPaths.apiPrefix+':auth/fileBin/:ke',config.webPaths.apiPrefix+':auth/fileBin/:ke/:id'],async (req,res) => { + s.auth(req.params,(user) => { + const userDetails = user.details + const monitorId = req.params.id + const groupKey = req.params.ke + const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1'; + s.sqlQueryBetweenTimesWithPermissions({ + table: 'Files', + user: user, + groupKey: req.params.ke, + monitorId: req.params.id, + startTime: req.query.start, + endTime: req.query.end, + startTimeOperator: req.query.startOperator, + endTimeOperator: req.query.endOperator, + limit: req.query.limit, + endIsStartTo: true, + noFormat: true, + noCount: true, + preliminaryValidationFailed: ( + user.permissions.get_monitors === "0" + ) + },(response) => { + response.forEach(function(v){ + v.details = s.parseJSON(v.details) + v.href = '/'+req.params.auth+'/fileBin/'+req.params.ke+'/'+req.params.id+'/'+v.details.year+'/'+v.details.month+'/'+v.details.day+'/'+v.name; + }) + s.closeJsonResponse(res,{ + ok: true, + files: response + }) + }) + },res,req); + }); + /** + * API : Get fileBin file + */ + app.get(config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:year/:month/:day/:file', async (req,res) => { + s.auth(req.params,function(user){ + var failed = function(){ + res.end(user.lang['File Not Found']) + } + if (!s.group[req.params.ke].fileBin[req.params.id+'/'+req.params.file]){ + const groupKey = req.params.ke + const monitorId = req.params.id + const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId) + if(user.details.sub && user.details.allmonitors === '0' && (user.permissions.get_monitors === "0" || monitorRestrictions.length === 0)){ + s.closeJsonResponse(res,{ + ok: false, + msg: lang['Not Permitted'] + }) + return + } + s.knexQuery({ + action: "select", + columns: "*", + table: "Files", + where: [ + ['ke','=',groupKey], + ['mid','=',req.params.id], + ['name','=',req.params.file], + monitorRestrictions + ] + },(err,r) => { + if(r && r[0]){ + r = r[0] + r.details = JSON.parse(r.details) + req.dir = s.dir.fileBin + req.params.ke + '/' + req.params.id + '/' + r.details.year + '/' + r.details.month + '/' + r.details.day + '/' + req.params.file; + fs.stat(req.dir,function(err,stats){ + if(!err){ + res.on('finish',function(){res.end()}) + fs.createReadStream(req.dir).pipe(res) + }else{ + failed() + } + }) + }else{ + failed() + } + }) + }else{ + res.end(user.lang['Please Wait for Completion']) + } + },res,req); + }); } diff --git a/libs/folders.js b/libs/folders.js index e0c9fc91..54f9acc6 100644 --- a/libs/folders.js +++ b/libs/folders.js @@ -11,6 +11,7 @@ module.exports = function(s,config,lang){ }else{ config.streamDir = config.windowsTempDir } + config.shmDir = `${s.checkCorrectPathEnding(config.streamDir)}` if(!fs.existsSync(config.streamDir)){ config.streamDir = s.mainDirectory+'/streams/' }else{ diff --git a/libs/health.js b/libs/health.js index e323a4ab..31d2755e 100644 --- a/libs/health.js +++ b/libs/health.js @@ -1,85 +1,130 @@ +var fs = require('fs'); var exec = require('child_process').exec; var spawn = require('child_process').spawn; +const { getCpuUsageOnLinux, getRamUsageOnLinux } = require('./health/utils.js') module.exports = function(s,config,lang,io){ s.heartBeat = function(){ setTimeout(s.heartBeat, 8000); io.sockets.emit('ping',{beat:1}); } s.heartBeat() - s.cpuUsage = function(callback){ - var k = {} - switch(s.platform){ - case'win32': - k.cmd = "@for /f \"skip=1\" %p in ('wmic cpu get loadpercentage') do @echo %p%" - break; - case'darwin': - k.cmd = "ps -A -o %cpu | awk '{s+=$1} END {print s}'"; - break; - case'linux': - k.cmd = 'top -b -n 2 | awk \'toupper($0) ~ /^.?CPU/ {gsub("id,","100",$8); gsub("%","",$8); print 100-$8}\' | tail -n 1'; - break; - case'freebsd': - k.cmd = 'vmstat 1 2 | awk \'END{print 100-$19}\'' - break; - case'openbsd': - k.cmd = 'vmstat 1 2 | awk \'END{print 100-$18}\'' - break; + let hasProcStat = false + try{ + fs.statSync("/proc/stat") + hasProcStat = true + }catch(err){ + + } + if(hasProcStat){ + s.cpuUsage = async () => { + const percent = await getCpuUsageOnLinux() + return percent } - if(config.customCpuCommand){ - exec(config.customCpuCommand,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true) { - d = d.replace(/(\r\n|\n|\r)/gm, "").replace(/%/g, "") - } - callback(d) - s.onGetCpuUsageExtensions.forEach(function(extender){ - extender(d) - }) - }) - } else if(k.cmd){ - exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true){ - d=d.replace(/(\r\n|\n|\r)/gm,"").replace(/%/g,"") - } - callback(d) - s.onGetCpuUsageExtensions.forEach(function(extender){ - extender(d) - }) - }) - } else { - callback(0) + }else{ + s.cpuUsage = () => { + return new Promise((resolve, reject) => { + var k = {} + switch(s.platform){ + case'win32': + k.cmd = "@for /f \"skip=1\" %p in ('wmic cpu get loadpercentage') do @echo %p%" + break; + case'darwin': + k.cmd = "ps -A -o %cpu | awk '{s+=$1} END {print s}'"; + break; + case'linux': + k.cmd = 'top -b -n 2 | awk \'toupper($0) ~ /^.?CPU/ {gsub("id,","100",$8); gsub("%","",$8); print 100-$8}\' | tail -n 1'; + break; + case'freebsd': + k.cmd = 'vmstat 1 2 | awk \'END{print 100-$19}\'' + break; + case'openbsd': + k.cmd = 'vmstat 1 2 | awk \'END{print 100-$18}\'' + break; + } + if(config.customCpuCommand){ + exec(config.customCpuCommand,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true) { + d = d.replace(/(\r\n|\n|\r)/gm, "").replace(/%/g, "") + } + resolve(d) + s.onGetCpuUsageExtensions.forEach(function(extender){ + extender(d) + }) + }) + } else if(k.cmd){ + exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true){ + d=d.replace(/(\r\n|\n|\r)/gm,"").replace(/%/g,"") + } + resolve(d) + s.onGetCpuUsageExtensions.forEach(function(extender){ + extender(d) + }) + }) + } else { + resolve(0) + } + }) } } - s.ramUsage = function(callback){ - k={} - switch(s.platform){ - case'win32': - k.cmd = "wmic OS get FreePhysicalMemory /Value" - break; - case'darwin': - k.cmd = "vm_stat | awk '/^Pages free: /{f=substr($3,1,length($3)-1)} /^Pages active: /{a=substr($3,1,length($3-1))} /^Pages inactive: /{i=substr($3,1,length($3-1))} /^Pages speculative: /{s=substr($3,1,length($3-1))} /^Pages wired down: /{w=substr($4,1,length($4-1))} /^Pages occupied by compressor: /{c=substr($5,1,length($5-1)); print ((a+w)/(f+a+i+w+s+c))*100;}'" - break; - case'freebsd': - k.cmd = "echo \"scale=4; $(vmstat -H | awk 'END{print $5}')*1024*100/$(sysctl -n hw.physmem)\" | bc" - break; - case'openbsd': - k.cmd = "echo \"scale=4; $(vmstat | awk 'END{ gsub(\"M\",\"\",$4); print $4 }')*104857600/$(sysctl -n hw.physmem)\" | bc" - break; - default: - k.cmd = "LANG=C free | grep Mem | awk '{print $7/$2 * 100.0}'"; - break; + let hasProcMeminfo = false + try{ + fs.statSync("/proc/meminfo") + hasProcMeminfo = true + }catch(err){ + + } + if(hasProcMeminfo){ + s.ramUsage = async () => { + const used = await getRamUsageOnLinux() + return used } - 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 - } - callback(d) - s.onGetRamUsageExtensions.forEach(function(extender){ - extender(d) - }) - }) - }else{ - callback(0) + }else{ + s.ramUsage = () => { + return new Promise((resolve, reject) => { + k={} + switch(s.platform){ + case'win32': + k.cmd = "wmic OS get FreePhysicalMemory /Value" + break; + case'darwin': + k.cmd = "vm_stat | awk '/^Pages free: /{f=substr($3,1,length($3)-1)} /^Pages active: /{a=substr($3,1,length($3-1))} /^Pages inactive: /{i=substr($3,1,length($3-1))} /^Pages speculative: /{s=substr($3,1,length($3-1))} /^Pages wired down: /{w=substr($4,1,length($4-1))} /^Pages occupied by compressor: /{c=substr($5,1,length($5-1)); print ((a+w)/(f+a+i+w+s+c))*100;}'" + break; + case'freebsd': + k.cmd = "echo \"scale=4; $(vmstat -H | awk 'END{print $5}')*1024*100/$(sysctl -n hw.physmem)\" | bc" + break; + case'openbsd': + k.cmd = "echo \"scale=4; $(vmstat | awk 'END{ gsub(\"M\",\"\",$4); print $4 }')*104857600/$(sysctl -n hw.physmem)\" | bc" + break; + default: + k.cmd = "LANG=C free | grep Mem | awk '{print $7/$2 * 100.0}'"; + break; + } + 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) + }) + }) + }else{ + resolve(0) + } + }) } } + if(config.childNodes.mode !== 'child'){ + setInterval(async () => { + const cpu = await s.cpuUsage() + const ram = await s.ramUsage() + s.tx({ + f: 'os', + cpu: cpu, + ram: ram + },'CPU') + },10000) + } } diff --git a/libs/health/utils.js b/libs/health/utils.js new file mode 100644 index 00000000..98eecdfc --- /dev/null +++ b/libs/health/utils.js @@ -0,0 +1,66 @@ +// This file's contents were referenced from https://gist.github.com/sidwarkd/9578213 +const fs = require('fs'); +const calculateCPUPercentage = function(oldVals, newVals){ + var totalDiff = newVals.total - oldVals.total; + var activeDiff = newVals.active - oldVals.active; + return Math.ceil((activeDiff / totalDiff) * 100); +}; +function getValFromLine(line){ + var match = line.match(/[0-9]+/gi); + if(match !== null) + return parseInt(match[0]); + else + return null; +}; +const currentCPUInfo = { + total: 0, + active: 0 +} +const lastCPUInfo = { + total: 0, + active: 0 +} +exports.getCpuUsageOnLinux = () => { + lastCPUInfo.active = currentCPUInfo.active; + lastCPUInfo.idle = currentCPUInfo.idle; + lastCPUInfo.total = currentCPUInfo.total; + return new Promise((resolve,reject) => { + const getUsage = function(callback){ + fs.readFile("/proc/stat" ,'utf8', function(err, data){ + var lines = data.split('\n'); + var cpuTimes = lines[0].match(/[0-9]+/gi); + currentCPUInfo.total = 0; + currentCPUInfo.idle = parseInt(cpuTimes[3]) + parseInt(cpuTimes[4]); + for (var i = 0; i < cpuTimes.length; i++){ + currentCPUInfo.total += parseInt(cpuTimes[i]); + } + currentCPUInfo.active = currentCPUInfo.total - currentCPUInfo.idle + currentCPUInfo.percentUsed = calculateCPUPercentage(lastCPUInfo, currentCPUInfo); + callback(currentCPUInfo.percentUsed) + }) + } + getUsage(function(percentage){ + setTimeout(function(){ + getUsage(function(percentage){ + resolve(percentage); + }) + }, 3000) + }) + }) +} +exports.getRamUsageOnLinux = () => { + return new Promise((resolve,reject) => { + fs.readFile("/proc/meminfo", 'utf8', function(err, data){ + const lines = data.split('\n'); + const total = Math.floor(getValFromLine(lines[0]) / 1024); + const free = Math.floor(getValFromLine(lines[1]) / 1024); + const cached = Math.floor(getValFromLine(lines[4]) / 1024); + const used = total - free; + const percentUsed = Math.ceil(((used - cached) / total) * 100); + resolve({ + used: used, + percent: percentUsed, + }); + }) + }) +} diff --git a/libs/monitor.js b/libs/monitor.js index d2c088af..e28d61b3 100644 --- a/libs/monitor.js +++ b/libs/monitor.js @@ -1,21 +1,19 @@ -var fs = require('fs'); -var events = require('events'); -var spawn = require('child_process').spawn; -var exec = require('child_process').exec; -var Mp4Frag = require('mp4frag'); -var onvif = require('node-onvif'); -var treekill = require('tree-kill'); -var request = require('request'); -var connectionTester = require('connection-tester') -var SoundDetection = require('shinobi-sound-detection') -var async = require("async"); -var URL = require('url') +const fs = require('fs'); +const events = require('events'); +const spawn = require('child_process').spawn; +const exec = require('child_process').exec; +const Mp4Frag = require('mp4frag'); +const onvif = require('node-onvif'); +const treekill = require('tree-kill'); +const request = require('request'); +const connectionTester = require('connection-tester') +const SoundDetection = require('shinobi-sound-detection') +const async = require("async"); +const URL = require('url') +const { copyObject, createQueue } = require('./common.js') module.exports = function(s,config,lang){ - const startMonitorInQueue = async.queue(function(action, callback) { - setTimeout(function(){ - action(callback) - },2000) - }, 3) + const { cameraDestroy } = require('./monitor/utils.js')(s,config,lang) + const startMonitorInQueue = createQueue(1, 3) s.initiateMonitorObject = function(e){ if(!s.group[e.ke]){s.group[e.ke]={}}; if(!s.group[e.ke].activeMonitors){s.group[e.ke].activeMonitors={}} @@ -48,26 +46,26 @@ module.exports = function(s,config,lang){ } s.getMonitorCpuUsage = function(e,callback){ if(s.group[e.ke].activeMonitors[e.mid] && s.group[e.ke].activeMonitors[e.mid].spawn){ - var getUsage = function(callback2){ + const getUsage = function(callback2){ s.readFile("/proc/" + s.group[e.ke].activeMonitors[e.mid].spawn.pid + "/stat", function(err, data){ if(!err){ - var elems = data.toString().split(' '); - var utime = parseInt(elems[13]); - var stime = parseInt(elems[14]); + const elems = data.toString().split(' '); + const utime = parseInt(elems[13]); + const stime = parseInt(elems[14]); callback2(utime + stime); }else{ - clearInterval(s.group[e.ke].activeMonitors[e.mid].getMonitorCpuUsage) + clearInterval(0) } }) } getUsage(function(startTime){ setTimeout(function(){ getUsage(function(endTime){ - var delta = endTime - startTime; - var percentage = 100 * (delta / 10000); + const delta = endTime - startTime; + const percentage = 100 * (delta / 10000); callback(percentage) - }); + }) }, 1000) }) }else{ @@ -104,141 +102,152 @@ module.exports = function(s,config,lang){ }); return x.ar; } - s.getRawSnapshotFromMonitor = function(monitor,options,callback){ - if(!callback){ - callback = options - var options = {flags: ''} - } - s.checkDetails(monitor) - var inputOptions = [] - var outputOptions = [] - var streamDir = s.dir.streams + monitor.ke + '/' + monitor.mid + '/' - var url = options.url - var secondsInward = options.secondsInward || '0' - if(secondsInward.length === 1)secondsInward = '0' + secondsInward - if(options.flags)outputOptions.push(options.flags) - const checkExists = function(streamDir,callback){ - s.fileStats(streamDir,function(err){ - var response = false - if(err){ - // s.debugLog(err) - }else{ - response = true - } - callback(response) - }) - } - const noIconChecks = function(){ - const runExtraction = function(){ - var sendTempImage = function(){ - fs.readFile(temporaryImageFile,function(err,buffer){ - if(!err){ - callback(buffer,false) - } - fs.unlink(temporaryImageFile,function(){}) - }) - } - try{ - var snapBuffer = [] - var temporaryImageFile = streamDir + s.gid(5) + '.jpg' - var iconImageFile = streamDir + 'icon.jpg' - var ffmpegCmd = s.splitForFFPMEG(`-loglevel warning -re -probesize 100000 -analyzeduration 100000 ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -vf "fps=1" -vframes 1 "${temporaryImageFile}"`) - fs.writeFileSync(s.group[monitor.ke].activeMonitors[monitor.id].sdir + 'snapCmd.txt',JSON.stringify({ - cmd: ffmpegCmd, - temporaryImageFile: temporaryImageFile, - iconImageFile: iconImageFile, - useIcon: options.useIcon, - rawMonitorConfig: s.group[monitor.ke].rawMonitorConfigurations[monitor.mid], - },null,3),'utf8') - var cameraCommandParams = [ - s.mainDirectory + '/libs/cameraThread/snapshot.js', - config.ffmpegDir, - s.group[monitor.ke].activeMonitors[monitor.id].sdir + 'snapCmd.txt' - ] - var snapProcess = spawn('node',cameraCommandParams,{detached: true}) - snapProcess.stderr.on('data',function(data){ - console.log(data.toString()) - }) - snapProcess.on('close',function(data){ - clearTimeout(snapProcessTimeout) - sendTempImage() - }) - var snapProcessTimeout = setTimeout(function(){ - var pid = snapProcess.pid - if(s.isWin){ - spawn("taskkill", ["/pid", pid, '/t']) - }else{ - process.kill(-pid, 'SIGTERM') - } - setTimeout(function(){ - if(s.isWin === false){ - treekill(pid) + s.getStreamsDirectory = (monitor) => { + return s.dir.streams + monitor.ke + '/' + monitor.mid + '/' + } + s.getRawSnapshotFromMonitor = function(monitor,options){ + return new Promise((resolve,reject) => { + options = options instanceof Object ? options : {flags: ''} + s.checkDetails(monitor) + var inputOptions = [] + var outputOptions = [] + var streamDir = s.dir.streams + monitor.ke + '/' + monitor.mid + '/' + var url = options.url + var secondsInward = options.secondsInward || '0' + if(secondsInward.length === 1)secondsInward = '0' + secondsInward + if(options.flags)outputOptions.push(options.flags) + const checkExists = function(streamDir,callback){ + s.fileStats(streamDir,function(err){ + var response = false + if(err){ + // s.debugLog(err) + }else{ + response = true + } + callback(response) + }) + } + const noIconChecks = function(){ + const runExtraction = function(){ + var sendTempImage = function(){ + fs.readFile(temporaryImageFile,function(err,buffer){ + if(!err){ + resolve({ + screenShot: buffer, + isStaticFile: false + }) + } + fs.unlink(temporaryImageFile,function(){}) + }) + } + try{ + var snapBuffer = [] + var temporaryImageFile = streamDir + s.gid(5) + '.jpg' + var iconImageFile = streamDir + 'icon.jpg' + var ffmpegCmd = s.splitForFFPMEG(`-loglevel warning -re -probesize 100000 -analyzeduration 100000 ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -vf "fps=1" -vframes 1 "${temporaryImageFile}"`) + fs.writeFileSync(s.getStreamsDirectory(monitor) + 'snapCmd.txt',JSON.stringify({ + cmd: ffmpegCmd, + temporaryImageFile: temporaryImageFile, + iconImageFile: iconImageFile, + useIcon: options.useIcon, + rawMonitorConfig: s.group[monitor.ke].rawMonitorConfigurations[monitor.mid], + },null,3),'utf8') + var cameraCommandParams = [ + s.mainDirectory + '/libs/cameraThread/snapshot.js', + config.ffmpegDir, + s.group[monitor.ke].activeMonitors[monitor.id].sdir + 'snapCmd.txt' + ] + var snapProcess = spawn('node',cameraCommandParams,{detached: true}) + snapProcess.stderr.on('data',function(data){ + s.debugLog(data.toString()) + }) + snapProcess.on('close',function(data){ + clearTimeout(snapProcessTimeout) + sendTempImage() + }) + var snapProcessTimeout = setTimeout(function(){ + var pid = snapProcess.pid + if(s.isWin){ + spawn("taskkill", ["/pid", pid, '/t']) }else{ - snapProcess.kill() + process.kill(-pid, 'SIGTERM') } - },10000) - },30000) - }catch(err){ - console.log(err) + setTimeout(function(){ + if(s.isWin === false){ + treekill(pid) + }else{ + snapProcess.kill() + } + },10000) + },30000) + }catch(err){ + console.log(err) + } + } + if(url){ + runExtraction() + }else{ + checkExists(streamDir + 's.jpg',function(success){ + if(success === false){ + checkExists(streamDir + 'detectorStream.m3u8',function(success){ + if(success === false){ + checkExists(streamDir + 's.m3u8',function(success){ + if(success === false){ + switch(monitor.type){ + case'h264': + switch(monitor.protocol){ + case'rtsp': + if( + monitor.details.rtsp_transport + && monitor.details.rtsp_transport !== '' + && monitor.details.rtsp_transport !== 'no' + ){ + inputOptions.push('-rtsp_transport ' + monitor.details.rtsp_transport) + } + break; + } + break; + } + url = s.buildMonitorUrl(monitor) + }else{ + outputOptions.push(`-ss 00:00:${secondsInward}`) + url = streamDir + 's.m3u8' + } + runExtraction() + }) + }else{ + outputOptions.push(`-ss 00:00:${secondsInward}`) + url = streamDir + 'detectorStream.m3u8' + runExtraction() + } + }) + }else{ + s.readFile(streamDir + 's.jpg',function(err,snapBuffer){ + resolve({ + screenShot: snapBuffer, + isStaticFile: true + }) + }) + } + }) } } - if(url){ - runExtraction() - }else{ - checkExists(streamDir + 's.jpg',function(success){ + if(options.useIcon === true){ + checkExists(streamDir + 'icon.jpg',function(success){ if(success === false){ - checkExists(streamDir + 'detectorStream.m3u8',function(success){ - if(success === false){ - checkExists(streamDir + 's.m3u8',function(success){ - if(success === false){ - switch(monitor.type){ - case'h264': - switch(monitor.protocol){ - case'rtsp': - if( - monitor.details.rtsp_transport - && monitor.details.rtsp_transport !== '' - && monitor.details.rtsp_transport !== 'no' - ){ - inputOptions.push('-rtsp_transport ' + monitor.details.rtsp_transport) - } - break; - } - break; - } - url = s.buildMonitorUrl(monitor) - }else{ - outputOptions.push(`-ss 00:00:${secondsInward}`) - url = streamDir + 's.m3u8' - } - runExtraction() - }) - }else{ - outputOptions.push(`-ss 00:00:${secondsInward}`) - url = streamDir + 'detectorStream.m3u8' - runExtraction() - } - }) + noIconChecks() }else{ - s.readFile(streamDir + 's.jpg',function(err,snapBuffer){ - callback(snapBuffer,true) + var snapBuffer = fs.readFileSync(streamDir + 'icon.jpg') + resolve({ + screenShot: snapBuffer, + isStaticFile: false }) } }) + }else{ + noIconChecks() } - } - if(options.useIcon === true){ - checkExists(streamDir + 'icon.jpg',function(success){ - if(success === false){ - noIconChecks() - }else{ - var snapBuffer = fs.readFileSync(streamDir + 'icon.jpg') - callback(snapBuffer,false) - } - }) - }else{ - noIconChecks() - } + }) } s.mergeDetectorBufferChunks = function(monitor,callback){ var pathDir = s.dir.streams+monitor.ke+'/'+monitor.id+'/' @@ -363,89 +372,7 @@ module.exports = function(s,config,lang){ }) return items } - - const cameraDestroy = function(e,p){ - if(s.group[e.ke]&&s.group[e.ke].activeMonitors[e.id]&&s.group[e.ke].activeMonitors[e.id].spawn !== undefined){ - const activeMonitor = s.group[e.ke].activeMonitors[e.id]; - const proc = s.group[e.ke].activeMonitors[e.id].spawn; - if(proc){ - activeMonitor.allowStdinWrite = false - s.txToDashcamUsers({ - f : 'disable_stream', - ke : e.ke, - mid : e.id - },e.ke) - // if(activeMonitor.p2pStream){activeMonitor.p2pStream.unpipe();} - try{ - proc.removeListener('end',activeMonitor.spawn_exit); - proc.removeListener('exit',activeMonitor.spawn_exit); - delete(activeMonitor.spawn_exit); - }catch(er){ - - } - } - if(activeMonitor.audioDetector){ - activeMonitor.audioDetector.stop() - delete(activeMonitor.audioDetector) - } - activeMonitor.firstStreamChunk = {} - clearTimeout(activeMonitor.recordingChecker); - delete(activeMonitor.recordingChecker); - clearTimeout(activeMonitor.streamChecker); - delete(activeMonitor.streamChecker); - clearTimeout(activeMonitor.checkSnap); - delete(activeMonitor.checkSnap); - clearTimeout(activeMonitor.watchdog_stop); - delete(activeMonitor.watchdog_stop); - delete(activeMonitor.lastJpegDetectorFrame); - delete(activeMonitor.detectorFrameSaveBuffer); - clearTimeout(activeMonitor.recordingSnapper); - clearInterval(activeMonitor.getMonitorCpuUsage); - clearInterval(activeMonitor.objectCountIntervals); - delete(activeMonitor.onvifConnection) - if(activeMonitor.onChildNodeExit){ - activeMonitor.onChildNodeExit() - } - activeMonitor.spawn.stdio.forEach(function(stdio){ - try{ - stdio.unpipe() - }catch(err){ - console.log(err) - } - }) - if(activeMonitor.mp4frag){ - var mp4FragChannels = Object.keys(activeMonitor.mp4frag) - mp4FragChannels.forEach(function(channel){ - activeMonitor.mp4frag[channel].removeAllListeners() - delete(activeMonitor.mp4frag[channel]) - }) - } - if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ - s.cx({f:'clearCameraFromActiveList',ke:e.ke,id:e.id}) - } - if(activeMonitor.childNode){ - s.cx({f:'kill',d:s.cleanMonitorObject(e)},activeMonitor.childNodeId) - }else{ - s.coSpawnClose(e) - if(proc && proc.kill){ - if(s.isWin){ - spawn("taskkill", ["/pid", proc.pid, '/t']) - }else{ - proc.kill('SIGTERM') - } - setTimeout(function(){ - try{ - proc.kill() - }catch(err){ - s.debugLog(err) - } - },1000) - } - } - } - } - - s.cameraCheckObjectsInDetails = function(e){ + const checkObjectsInDetails = function(e){ //parse Objects (['detector_cascades','cords','detector_filters','input_map_choices']).forEach(function(v){ if(e.details && e.details[v]){ @@ -510,28 +437,27 @@ module.exports = function(s,config,lang){ } return options } - s.cameraSendSnapshot = function(e,options){ - if(!options)options = {} + s.cameraSendSnapshot = async (e,options) => { + options = Object.assign({ + flags: '-s 200x200' + },options || {}) s.checkDetails(e) - if(config.doSnapshot === true){ + if(e.ke && config.doSnapshot === true){ if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){ var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/' - s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],Object.assign({ - flags: '-s 200x200' - },options),function(data,isStaticFile){ - if(data && (data[data.length-2] === 0xFF && data[data.length-1] === 0xD9)){ - s.tx({ - f: 'monitor_snapshot', - snapshot: data.toString('base64'), - snapshot_format: 'b64', - mid: e.mid, - ke: e.ke - },'GRP_'+e.ke) - }else{ - console.log('not image') - s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - } - }) + const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options) + if(screenShot && (screenShot[screenShot.length-2] === 0xFF && screenShot[screenShot.length-1] === 0xD9)){ + s.tx({ + f: 'monitor_snapshot', + snapshot: screenShot.toString('base64'), + snapshot_format: 'b64', + mid: e.mid, + ke: e.ke + },'GRP_'+e.ke) + }else{ + console.log('not image') + s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) + } }else{ s.tx({f:'monitor_snapshot',snapshot:'Disabled',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) } @@ -539,6 +465,29 @@ module.exports = function(s,config,lang){ s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) } } + s.getCameraSnapshot = async (e,options) => { + const getDefaultImage = async () => { + return await fs.promises.readFile(config.defaultMjpeg) + } + options = Object.assign({ + flags: '-s 200x200' + },options || {}) + if(e.ke && config.doSnapshot === true){ + if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){ + var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/' + const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options) + if(screenShot && (screenShot[screenShot.length-2] === 0xFF && screenShot[screenShot.length-1] === 0xD9)){ + return screenShot + }else{ + return await getDefaultImage() + } + }else{ + return await getDefaultImage() + } + }else{ + return await getDefaultImage() + } + } const createRecordingDirectory = function(e,callback){ var directory if(e.details && e.details.dir && e.details.dir !== '' && config.childNodes.mode !== 'child'){ @@ -601,11 +550,6 @@ module.exports = function(s,config,lang){ }) }) } - // try{ - // fs.unlinkSync('/home/Shinobi/test.log') - // }catch(err){ - // - // } const createCameraFolders = function(e,callback){ //set the recording directory var activeMonitor = s.group[e.ke].activeMonitors[e.id] @@ -625,6 +569,23 @@ module.exports = function(s,config,lang){ }) }) } + const forceMonitorRestart = (monitor,restartMessage) => { + const monitorConfig = Object.assign(s.group[monitor.ke].rawMonitorConfigurations[monitor.mid],{}) + s.sendMonitorStatus({ + id: monitor.mid, + ke: monitor.ke, + status: lang.Restarting + }) + launchMonitorProcesses(monitorConfig) + s.userLog({ + ke: monitor.ke, + mid: monitor.mid, + },restartMessage) + s.orphanedVideoCheck({ + ke: monitor.ke, + mid: monitor.mid, + },2,null,true) + } s.stripAuthFromHost = function(e){ var host = e.host.split('@'); if(host[1]){ @@ -636,7 +597,7 @@ module.exports = function(s,config,lang){ } return host } - s.resetRecordingCheck = function(e){ + const resetRecordingCheck = function(e){ clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingChecker) var cutoff = e.cutoff + 0 if(e.type === 'dashcam'){ @@ -644,10 +605,15 @@ module.exports = function(s,config,lang){ } s.group[e.ke].activeMonitors[e.id].recordingChecker = setTimeout(function(){ if(s.group[e.ke].activeMonitors[e.id].isStarted === true && s.group[e.ke].rawMonitorConfigurations[e.id].mode === 'record'){ - launchMonitorProcesses(s.cleanMonitorObject(e)); - s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Restarting}); - s.userLog(e,{type:lang['Camera is not recording'],msg:{msg:lang['Restarting Process']}}); - s.orphanedVideoCheck(e,2,null,true) + forceMonitorRestart({ + ke: e.ke, + mid: e.id, + },{ + type: lang['Camera is not recording'], + msg: { + msg: lang['Restarting Process'] + } + }) } },60000 * cutoff * 1.3); } @@ -655,9 +621,15 @@ module.exports = function(s,config,lang){ clearTimeout(s.group[e.ke].activeMonitors[e.id].streamChecker) s.group[e.ke].activeMonitors[e.id].streamChecker = setTimeout(function(){ if(s.group[e.ke].activeMonitors[e.id] && s.group[e.ke].activeMonitors[e.id].isStarted === true){ - launchMonitorProcesses(s.cleanMonitorObject(e)); - s.userLog(e,{type:lang['Camera is not streaming'],msg:{msg:lang['Restarting Process']}}); - s.orphanedVideoCheck(e,2,null,true) + forceMonitorRestart({ + ke: e.ke, + mid: e.id, + },{ + type: lang['Camera is not streaming'], + msg: { + msg: lang['Restarting Process'] + } + }) } },60000*1); } @@ -702,7 +674,7 @@ module.exports = function(s,config,lang){ if(e.details.loglevel!=='quiet'){ s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang['Process Crashed for Monitor'],cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}}); } - s.fatalCameraError(e,'Process Unexpected Exit'); + fatalError(e,'Process Unexpected Exit'); s.orphanedVideoCheck(e,2,null,true) s.onMonitorUnexpectedExitExtensions.forEach(function(extender){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) @@ -712,7 +684,7 @@ module.exports = function(s,config,lang){ s.group[e.ke].activeMonitors[e.id].spawn.on('end',s.group[e.ke].activeMonitors[e.id].spawn_exit) s.group[e.ke].activeMonitors[e.id].spawn.on('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit) s.group[e.ke].activeMonitors[e.id].spawn.on('error',function(er){ - s.userLog(e,{type:'Spawn Error',msg:er});s.fatalCameraError(e,'Spawn Error') + s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error') }) s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}}) if(s.isWin === false){ @@ -764,18 +736,22 @@ module.exports = function(s,config,lang){ const count = Object.keys(tagInfo.count) const times = tagInfo.times const realTag = tagInfo.tag - s.sqlQuery('INSERT INTO `Events Counts` (ke,mid,details,time,end,count,tag) VALUES (?,?,?,?,?,?,?)',[ - groupKey, - monitorId, - JSON.stringify({ - times: times, - count: count, - }), - startTime, - endTime, - count.length, - realTag - ]) + s.knexQuery({ + action: "insert", + table: "Events Counts", + insert: { + ke: groupKey, + mid: monitorId, + details: JSON.stringify({ + times: times, + count: count, + }), + time: startTime, + end: endTime, + count: count.length, + tag: realTag + } + }) }) },60000) //every minute } @@ -1022,7 +998,7 @@ module.exports = function(s,config,lang){ } s.group[e.ke].activeMonitors[e.id].detector_motion_count = [] }) - s.resetRecordingCheck(e) + resetRecordingCheck(e) } }) } @@ -1033,7 +1009,10 @@ module.exports = function(s,config,lang){ switch(true){ case checkLog(d,'No space left on device'): s.checkUserPurgeLock(e.ke) - s.purgeDiskForGroup(e) + s.purgeDiskForGroup(e.ke) + break; + case checkLog(d,'error parsing AU headers'): + s.userLog(e,{type:lang['Error While Decoding'],msg:lang.ErrorWhileDecodingTextAudio}); break; case checkLog(d,'error while decoding'): s.userLog(e,{type:lang['Error While Decoding'],msg:lang.ErrorWhileDecodingText}); @@ -1058,7 +1037,7 @@ module.exports = function(s,config,lang){ //restart setTimeout(function(){ s.userLog(e,{type:lang['Connection timed out'],msg:lang['Retrying...']}); - s.fatalCameraError(e,'Connection timed out'); + fatalError(e,'Connection timed out'); },1000) break; // case checkLog(d,'Immediate exit requested'): @@ -1103,12 +1082,9 @@ module.exports = function(s,config,lang){ }) },detector_notrigger_timeout) } - copyObject = function(obj){ - return Object.assign({},obj) - } //set master based process launcher - launchMonitorProcesses = function(e){ - const activeMonitor = s.group[e.ke].activeMonitors[e.id] + const launchMonitorProcesses = function(e){ + const activeMonitor = s.group[e.ke].activeMonitors[e.id] // e = monitor object clearTimeout(activeMonitor.resetFatalErrorCountTimer) activeMonitor.resetFatalErrorCountTimer = setTimeout(()=>{ @@ -1169,7 +1145,7 @@ module.exports = function(s,config,lang){ activeMonitor.fswatch = fs.watch(e.dir, {encoding : 'utf8'}, (event, filename) => { switch(event){ case'change': - s.resetRecordingCheck(e) + resetRecordingCheck(e) break; } }); @@ -1209,7 +1185,7 @@ module.exports = function(s,config,lang){ createCameraFfmpegProcess(e) createCameraStreamHandlers(e) createEventCounter(e) - if(e.type === 'dashcam'){ + if(e.type === 'dashcam' || e.type === 'socket'){ setTimeout(function(){ activeMonitor.allowStdinWrite = true s.txToDashcamUsers({ @@ -1245,7 +1221,7 @@ module.exports = function(s,config,lang){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) }) s.userLog(e,{type:lang["Ping Failed"],msg:lang.skipPingText1}); - s.fatalCameraError(e,"Ping Failed");return; + fatalError(e,"Ping Failed");return; } } if( @@ -1295,7 +1271,7 @@ module.exports = function(s,config,lang){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) }) s.userLog(e,{type:lang["Ping Failed"],msg:lang.skipPingText1}); - s.fatalCameraError(e,"Ping Failed");return; + fatalError(e,"Ping Failed");return; } }) }else{ @@ -1343,7 +1319,7 @@ module.exports = function(s,config,lang){ console.log(err) } } - s.fatalCameraError = function(e,errorMessage){ + const fatalError = function(e,errorMessage){ const activeMonitor = s.group[e.ke].activeMonitors[e.id] clearTimeout(activeMonitor.err_fatal_timeout); ++activeMonitor.errorFatalCount; @@ -1363,27 +1339,27 @@ module.exports = function(s,config,lang){ extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e) }) } - s.isWatchCountable = function(d){ - try{ - var variableMethodsToAllow = [ - 'mp4ws', //Poseidon over Websocket - 'flvws', - 'h265ws', - ]; - var indefiniteIgnore = [ - 'mjpeg', - 'h264', - ]; - var monConfig = s.group[d.ke].rawMonitorConfigurations[d.id] - if( - variableMethodsToAllow.indexOf(monConfig.details.stream_type + monConfig.details.stream_flv_type) > -1 && - indefiniteIgnore.indexOf(monConfig.details.stream_type) === -1 - ){ - return true - } - }catch(err){} - return false - } + // s.isWatchCountable = function(d){ + // try{ + // var variableMethodsToAllow = [ + // 'mp4ws', //Poseidon over Websocket + // 'flvws', + // 'h265ws', + // ]; + // var indefiniteIgnore = [ + // 'mjpeg', + // 'h264', + // ]; + // var monConfig = s.group[d.ke].rawMonitorConfigurations[d.id] + // if( + // variableMethodsToAllow.indexOf(monConfig.details.stream_type + monConfig.details.stream_flv_type) > -1 && + // indefiniteIgnore.indexOf(monConfig.details.stream_type) === -1 + // ){ + // return true + // } + // }catch(err){} + // return false + // } s.addOrEditMonitor = function(form,callback,user){ var endData = { ok: false @@ -1395,10 +1371,17 @@ module.exports = function(s,config,lang){ } form.mid = form.mid.replace(/[^\w\s]/gi,'').replace(/ /g,'') form = s.cleanMonitorObjectForDatabase(form) - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[form.ke,form.mid],function(er,r){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: [ + ['ke','=',form.ke], + ['mid','=',form.mid], + ] + },(err,r) => { var affectMonitor = false - var monitorQuery = [] - var monitorQueryValues = [] + var monitorQuery = {} var txData = { f: 'monitor_edit', mid: form.mid, @@ -1416,19 +1399,23 @@ module.exports = function(s,config,lang){ form[v] !== false && form[v] !== `false` ){ - monitorQuery.push(v+'=?') if(form[v] instanceof Object){ form[v] = s.s(form[v]) } - monitorQueryValues.push(form[v]) + monitorQuery[v] = form[v] } }) - monitorQuery = monitorQuery.join(',') - monitorQueryValues.push(form.ke) - monitorQueryValues.push(form.mid) s.userLog(form,{type:'Monitor Updated',msg:'by user : '+user.uid}) endData.msg = user.lang['Monitor Updated by user']+' : '+user.uid - s.sqlQuery('UPDATE Monitors SET '+monitorQuery+' WHERE ke=? AND mid=?',monitorQueryValues) + s.knexQuery({ + action: "update", + table: "Monitors", + update: monitorQuery, + where: [ + ['ke','=',form.ke], + ['mid','=',form.mid], + ] + }) affectMonitor = true }else if( !s.group[form.ke].init.max_camera || @@ -1436,22 +1423,21 @@ module.exports = function(s,config,lang){ Object.keys(s.group[form.ke].activeMonitors).length <= parseInt(s.group[form.ke].init.max_camera) ){ txData.new = true - monitorQueryInsertValues = [] Object.keys(form).forEach(function(v){ if(form[v] && form[v] !== ''){ - monitorQuery.push(v) - monitorQueryInsertValues.push('?') if(form[v] instanceof Object){ form[v] = s.s(form[v]) } - monitorQueryValues.push(form[v]) + monitorQuery[v] = form[v] } }) - monitorQuery = monitorQuery.join(',') - monitorQueryInsertValues = monitorQueryInsertValues.join(',') s.userLog(form,{type:'Monitor Added',msg:'by user : '+user.uid}) endData.msg = user.lang['Monitor Added by user']+' : '+user.uid - s.sqlQuery('INSERT INTO Monitors ('+monitorQuery+') VALUES ('+monitorQueryInsertValues+')',monitorQueryValues) + s.knexQuery({ + action: "insert", + table: "Monitors", + insert: monitorQuery + }) affectMonitor = true }else{ txData.f = 'monitor_edit_failed' @@ -1488,7 +1474,7 @@ module.exports = function(s,config,lang){ e.functionMode = x if(!e.mode){e.mode = x} s.checkDetails(e) - s.cameraCheckObjectsInDetails(e) + checkObjectsInDetails(e) s.initiateMonitorObject({ke:e.ke,mid:e.id}) switch(e.functionMode){ case'watch_on'://live streamers - join @@ -1640,15 +1626,24 @@ module.exports = function(s,config,lang){ if(notFound === false){ var sqlQuery = 'SELECT * FROM Monitors WHERE ke=? AND ' var monitorQuery = [] - var sqlQueryValues = [groupKey] var monitorPresets = {} preset.details.monitors.forEach(function(monitor){ - monitorQuery.push('mid=?') - sqlQueryValues.push(monitor.mid) + const whereConditions = {} + if(monitorQuery.length === 0){ + whereConditions.ke = groupKey + monitorQuery.push(['ke','=',groupKey]) + }else{ + monitorQuery.push(['or','ke','=',groupKey]) + } + monitorQuery.push(['mid','=',monitor.mid]) monitorPresets[monitor.mid] = monitor }) - sqlQuery += '('+monitorQuery.join(' OR ')+')' - s.sqlQuery(sqlQuery,sqlQueryValues,function(err,monitors){ + s.knexQuery({ + action: "select", + columns: "*", + table: "Monitors", + where: monitorQuery + },function(err,monitors){ if(monitors && monitors[0]){ monitors.forEach(function(monitor){ s.checkDetails(monitor) @@ -1705,6 +1700,40 @@ module.exports = function(s,config,lang){ }) return cameras } + s.getMonitorRestrictions = (permissions,monitorId) => { + const monitorRestrictions = [] + if( + !monitorId && + permissions.sub && + permissions.monitors && + permissions.allmonitors !== '1' + ){ + try{ + permissions.monitors = s.parseJSON(permissions.monitors) + permissions.monitors.forEach(function(v,n){ + if(n === 0){ + monitorRestrictions.push(['mid','=',v]) + }else{ + monitorRestrictions.push(['or','mid','=',v]) + } + }) + }catch(er){ + } + }else if( + monitorId && ( + !permissions.sub || + permissions.allmonitors !== '0' || + permissions.monitors.indexOf(monitorId) >- 1 + ) + ){ + monitorRestrictions.push(['mid','=',monitorId]) + }else if( + !monitorId && + permissions.sub && + permissions.allmonitors !== '0' + ){} + return monitorRestrictions + } // s.checkViewerConnectionsForMonitor = function(monitorObject){ // var monitorConfig = s.group[monitorObject.ke].rawMonitorConfigurations[monitorObject.mid] // if(monitorConfig.mode === 'start'){ diff --git a/libs/monitor/utils.js b/libs/monitor/utils.js new file mode 100644 index 00000000..fa8e01c9 --- /dev/null +++ b/libs/monitor/utils.js @@ -0,0 +1,89 @@ +module.exports = (s,config,lang) => { + const cameraDestroy = function(e,p){ + if( + s.group[e.ke] && + s.group[e.ke].activeMonitors[e.id] && + s.group[e.ke].activeMonitors[e.id].spawn !== undefined + ){ + const activeMonitor = s.group[e.ke].activeMonitors[e.id]; + const proc = s.group[e.ke].activeMonitors[e.id].spawn; + if(proc){ + activeMonitor.allowStdinWrite = false + s.txToDashcamUsers({ + f : 'disable_stream', + ke : e.ke, + mid : e.id + },e.ke) + // if(activeMonitor.p2pStream){activeMonitor.p2pStream.unpipe();} + try{ + proc.removeListener('end',activeMonitor.spawn_exit); + proc.removeListener('exit',activeMonitor.spawn_exit); + delete(activeMonitor.spawn_exit); + }catch(er){ + + } + } + if(activeMonitor.audioDetector){ + activeMonitor.audioDetector.stop() + delete(activeMonitor.audioDetector) + } + activeMonitor.firstStreamChunk = {} + clearTimeout(activeMonitor.recordingChecker); + delete(activeMonitor.recordingChecker); + clearTimeout(activeMonitor.streamChecker); + delete(activeMonitor.streamChecker); + clearTimeout(activeMonitor.checkSnap); + delete(activeMonitor.checkSnap); + clearTimeout(activeMonitor.watchdog_stop); + delete(activeMonitor.watchdog_stop); + delete(activeMonitor.lastJpegDetectorFrame); + delete(activeMonitor.detectorFrameSaveBuffer); + clearTimeout(activeMonitor.recordingSnapper); + clearInterval(activeMonitor.getMonitorCpuUsage); + clearInterval(activeMonitor.objectCountIntervals); + delete(activeMonitor.onvifConnection) + if(activeMonitor.onChildNodeExit){ + activeMonitor.onChildNodeExit() + } + activeMonitor.spawn.stdio.forEach(function(stdio){ + try{ + stdio.unpipe() + }catch(err){ + console.log(err) + } + }) + if(activeMonitor.mp4frag){ + var mp4FragChannels = Object.keys(activeMonitor.mp4frag) + mp4FragChannels.forEach(function(channel){ + activeMonitor.mp4frag[channel].removeAllListeners() + delete(activeMonitor.mp4frag[channel]) + }) + } + if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ + s.cx({f:'clearCameraFromActiveList',ke:e.ke,id:e.id}) + } + if(activeMonitor.childNode){ + s.cx({f:'kill',d:s.cleanMonitorObject(e)},activeMonitor.childNodeId) + }else{ + s.coSpawnClose(e) + if(proc && proc.kill){ + if(s.isWin){ + spawn("taskkill", ["/pid", proc.pid, '/t']) + }else{ + proc.kill('SIGTERM') + } + setTimeout(function(){ + try{ + proc.kill() + }catch(err){ + s.debugLog(err) + } + },1000) + } + } + } + } + return { + cameraDestroy: cameraDestroy + } +} diff --git a/libs/notification.js b/libs/notification.js index b7aa4b37..c7b8a596 100644 --- a/libs/notification.js +++ b/libs/notification.js @@ -12,7 +12,7 @@ module.exports = function(s,config,lang){ s.userLog({ke:groupKey,mid:'$USER'},{type:lang.DiscordFailedText,msg:lang.DiscordNotEnabledText}) return } - var sendBody = Object.assign({ + const sendBody = Object.assign({ color: 3447003, title: 'Alert from Shinobi', description: "", @@ -23,7 +23,7 @@ module.exports = function(s,config,lang){ text: "Shinobi Systems" } },data) - var discordChannel = bot.channels.get(s.group[groupKey].init.discordbot_channel) + const discordChannel = bot.channels.cache.get(s.group[groupKey].init.discordbot_channel) if(discordChannel && discordChannel.send){ discordChannel.send({ embed: sendBody, @@ -45,10 +45,10 @@ module.exports = function(s,config,lang){ }) } } - var onEventTriggerBeforeFilterForDiscord = function(d,filter){ + const onEventTriggerBeforeFilterForDiscord = function(d,filter){ filter.discord = true } - var onEventTriggerForDiscord = function(d,filter){ + const onEventTriggerForDiscord = async (d,filter) => { // d = event object //discord bot if(filter.discord && s.group[d.ke].discordBot && d.mon.details.detector_discordbot === '1' && !s.group[d.ke].activeMonitors[d.id].detector_discordbot){ @@ -60,28 +60,11 @@ module.exports = function(s,config,lang){ } //lock mailer so you don't get emailed on EVERY trigger event. s.group[d.ke].activeMonitors[d.id].detector_discordbot = setTimeout(function(){ - //unlock so you can mail again. clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_discordbot); delete(s.group[d.ke].activeMonitors[d.id].detector_discordbot); },detector_discordbot_timeout) - var files = [] - var sendAlert = function(){ - s.discordMsg({ - 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" - } - },files,d.ke) - } if(d.mon.details.detector_discordbot_send_video === '1'){ + // change to function that captures on going video capture, waits, grabs new video file, slices portion (max for transmission) and prepares for delivery s.mergeDetectorBufferChunks(d,function(mergedFilepath,filename){ s.discordMsg({ author: { @@ -103,21 +86,34 @@ module.exports = function(s,config,lang){ ],d.ke) }) } - s.getRawSnapshotFromMonitor(d.mon,{ + const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(d.mon,{ secondsInward: d.mon.details.snap_seconds_inward - },function(data){ - if(data[data.length - 2] === 0xFF && data[data.length - 1] === 0xD9){ - d.screenshotBuffer = data - files.push({ - attachment: d.screenshotBuffer, - name: d.screenshotName+'.jpg' - }) - } - sendAlert() }) + if(screenShot[screenShot.length - 2] === 0xFF && screenShot[screenShot.length - 1] === 0xD9){ + d.screenshotBuffer = screenShot + s.discordMsg({ + 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: screenShot, + name: d.screenshotName+'.jpg' + } + ],d.ke) + } } } - var onTwoFactorAuthCodeNotificationForDiscord = function(r){ + const onTwoFactorAuthCodeNotificationForDiscord = function(r){ // r = user if(r.details.factor_discord === '1'){ s.discordMsg({ @@ -136,13 +132,13 @@ module.exports = function(s,config,lang){ },[],r.ke) } } - var loadDiscordBotForUser = function(user){ - ar=JSON.parse(user.details); + const loadDiscordBotForUser = function(user){ + const userDetails = s.parseJSON(user.details); //discordbot if(!s.group[user.ke].discordBot && config.discordBot === true && - ar.discordbot === '1' && - ar.discordbot_token !== '' + userDetails.discordbot === '1' && + userDetails.discordbot_token !== '' ){ s.group[user.ke].discordBot = new Discord.Client() s.group[user.ke].discordBot.on('ready', () => { @@ -154,16 +150,16 @@ module.exports = function(s,config,lang){ msg: s.group[user.ke].discordBot.user.tag }) }) - s.group[user.ke].discordBot.login(ar.discordbot_token) + s.group[user.ke].discordBot.login(userDetails.discordbot_token) } } - var unloadDiscordBotForUser = function(user){ + const unloadDiscordBotForUser = function(user){ if(s.group[user.ke].discordBot && s.group[user.ke].discordBot.destroy){ s.group[user.ke].discordBot.destroy() delete(s.group[user.ke].discordBot) } } - var onDetectorNoTriggerTimeoutForDiscord = function(e){ + const onDetectorNoTriggerTimeoutForDiscord = function(e){ //e = monitor object var currentTime = new Date() if(e.details.detector_notrigger_discord === '1'){ @@ -205,10 +201,18 @@ module.exports = function(s,config,lang){ if(config.mail.from === undefined){config.mail.from = '"ShinobiCCTV"${lang['Time Created']} : ${module.created}${lang['Last Modified']} : ${module.lastModified}