Merge branch 'dashboard-v3' into 'dev'
Dashboard v3 : Tempered Steel See merge request Shinobi-Systems/Shinobi!319cron-as-worker-process
commit
77792b5da2
|
@ -11,3 +11,4 @@ npm-debug.log
|
|||
shinobi.sqlite
|
||||
dist
|
||||
._*
|
||||
generatedLanguageFiles
|
||||
|
|
|
@ -85,7 +85,7 @@ echo "========================================================="
|
|||
#Check if Node.js is installed
|
||||
if ! [ -x "$(command -v node)" ]; then
|
||||
echo "Node.js not found, installing..."
|
||||
sudo curl --silent --location https://rpm.nodesource.com/setup_12.x | sudo bash -
|
||||
sudo curl --silent --location https://rpm.nodesource.com/setup_16.x | sudo bash -
|
||||
sudo "$pkgmgr" install nodejs -y -q -e 0
|
||||
else
|
||||
echo "Node.js is already installed..."
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
#!/bin/sh
|
||||
echo "------------------------------------------"
|
||||
echo "-- Installing CUDA Toolkit and CUDA DNN --"
|
||||
echo "------------------------------------------"
|
||||
# Install CUDA Drivers and Toolkit
|
||||
if [ -x "$(command -v apt)" ]; then
|
||||
wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2004/x86_64/cuda-ubuntu2004.pin
|
||||
sudo mv cuda-ubuntu2004.pin /etc/apt/preferences.d/cuda-repository-pin-600
|
||||
wget http://developer.download.nvidia.com/compute/cuda/11.0.2/local_installers/cuda-repo-ubuntu2004-11-0-local_11.0.2-450.51.05-1_amd64.deb
|
||||
sudo dpkg -i cuda-repo-ubuntu2004-11-0-local_11.0.2-450.51.05-1_amd64.deb
|
||||
sudo apt-key add /var/cuda-repo-ubuntu2004-11-0-local/7fa2af80.pub
|
||||
|
||||
sudo apt-get update -y
|
||||
|
||||
sudo apt-get -o Dpkg::Options::="--force-overwrite" install cuda-toolkit-11-0 -y --no-install-recommends
|
||||
sudo apt-get -o Dpkg::Options::="--force-overwrite" install --fix-broken -y
|
||||
sudo apt install nvidia-utils-495 nvidia-headless-495 -y
|
||||
|
||||
# Install CUDA DNN
|
||||
wget https://cdn.shinobi.video/installers/libcudnn8_8.2.1.32-1+cuda11.3_amd64.deb -O cuda-dnn.deb
|
||||
sudo dpkg -i cuda-dnn.deb
|
||||
wget https://cdn.shinobi.video/installers/libcudnn8-dev_8.2.0.53-1+cuda11.3_amd64.deb -O cuda-dnn-dev.deb
|
||||
sudo dpkg -i cuda-dnn-dev.deb
|
||||
echo "-- Cleaning Up --"
|
||||
# Cleanup
|
||||
sudo rm cuda-dnn.deb
|
||||
sudo rm cuda-dnn-dev.deb
|
||||
fi
|
||||
echo "------------------------------"
|
||||
echo "Reboot is required. Do it now?"
|
||||
echo "------------------------------"
|
||||
echo "(y)es or (N)o. Default is No."
|
||||
read rebootTheMachineHomie
|
||||
if [ "$rebootTheMachineHomie" = "y" ] || [ "$rebootTheMachineHomie" = "Y" ]; then
|
||||
sudo reboot
|
||||
fi
|
|
@ -46,7 +46,7 @@ npm install --unsafe-perm
|
|||
# sudo npm audit fix --force
|
||||
echo "============="
|
||||
echo "Shinobi - Install PM2"
|
||||
npm install pm2@3.0.0 -g
|
||||
npm install pm2@latest -g
|
||||
if (! -e "./conf.json" ) then
|
||||
cp conf.sample.json conf.json
|
||||
endif
|
||||
|
|
|
@ -18,7 +18,7 @@ npm i npm -g
|
|||
#There are some errors in here that I don't want you to see. Redirecting to dev null :D
|
||||
npm install --unsafe-perm > & /dev/null
|
||||
# npm audit fix --force > & /dev/null
|
||||
npm install pm2@3.0.0 -g
|
||||
npm install pm2@latest -g
|
||||
cp conf.sample.json conf.json
|
||||
cp super.sample.json super.json
|
||||
pm2 start camera.js
|
||||
|
|
|
@ -18,7 +18,7 @@ sudo npm install --unsafe-perm
|
|||
# sudo npm audit fix --unsafe-perm
|
||||
echo "============="
|
||||
echo "Shinobi - Install PM2"
|
||||
sudo npm install pm2@3.0.0 -g
|
||||
sudo npm install pm2@latest -g
|
||||
if [ ! -e "./conf.json" ]; then
|
||||
sudo cp conf.sample.json conf.json
|
||||
fi
|
||||
|
|
|
@ -10,7 +10,7 @@ echo "Shinobi - Do you want to Install Node.js?"
|
|||
echo "(y)es or (N)o"
|
||||
read -r nodejsinstall
|
||||
if [ "$nodejsinstall" = "y" ]; then
|
||||
curl -o node-installer.pkg https://nodejs.org/dist/v11.9.0/node-v11.9.0.pkg
|
||||
curl -o node-installer.pkg https://nodejs.org/dist/v16.15.0/node-v16.15.0.pkg
|
||||
sudo installer -pkg node-installer.pkg -target /
|
||||
rm node-installer.pkg
|
||||
sudo ln -s /usr/local/bin/node /usr/bin/nodejs
|
||||
|
@ -34,18 +34,13 @@ if [ "$ffmpeginstall" = "y" ]; then
|
|||
sudo chmod +x /usr/local/bin/ffserver
|
||||
fi
|
||||
echo "============="
|
||||
if [ ! -e "./shinobi.sqlite" ]; then
|
||||
sudo npm install jsonfile
|
||||
sudo cp sql/shinobi.sample.sqlite shinobi.sqlite
|
||||
sudo node tools/modifyConfiguration.js databaseType=sqlite3
|
||||
fi
|
||||
echo "Shinobi - Install NPM Libraries"
|
||||
sudo npm i npm -g
|
||||
sudo npm install --unsafe-perm
|
||||
# sudo npm audit fix --unsafe-perm
|
||||
echo "============="
|
||||
echo "Shinobi - Install PM2"
|
||||
sudo npm install pm2@3.0.0 -g
|
||||
sudo npm install pm2@latest -g
|
||||
if [ ! -e "./conf.json" ]; then
|
||||
sudo cp conf.sample.json conf.json
|
||||
fi
|
||||
|
|
|
@ -89,7 +89,7 @@ npm install --unsafe-perm
|
|||
# sudo npm audit fix --force
|
||||
echo "============="
|
||||
echo "Shinobi - Install PM2"
|
||||
sudo npm install pm2@3.0.0 -g
|
||||
sudo npm install pm2@latest -g
|
||||
echo "Shinobi - Finished"
|
||||
sudo chmod -R 755 .
|
||||
touch INSTALL/installed.txt
|
||||
|
|
|
@ -154,7 +154,7 @@ echo "============="
|
|||
|
||||
#Install PM2
|
||||
echo "Shinobi - Install PM2"
|
||||
sudo npm install pm2@3.0.0 -g
|
||||
sudo npm install pm2@latest -g
|
||||
if [ ! -e "./conf.json" ]; then
|
||||
cp conf.sample.json conf.json
|
||||
fi
|
||||
|
|
|
@ -100,7 +100,7 @@ sudo npm install --unsafe-perm
|
|||
# sudo npm audit fix --force
|
||||
echo "============="
|
||||
echo "Shinobi - Install PM2"
|
||||
sudo npm install pm2@3.0.0 -g
|
||||
sudo npm install pm2@latest -g
|
||||
echo "Shinobi - Finished"
|
||||
sudo chmod -R 755 .
|
||||
touch INSTALL/installed.txt
|
||||
|
|
|
@ -53,19 +53,14 @@ if ! [ -x "$(command -v ifconfig)" ]; then
|
|||
echo "Shinobi - Installing Net-Tools"
|
||||
sudo apt install net-tools -y
|
||||
fi
|
||||
if ! [ -x "$(command -v node)" ]; then
|
||||
echo "============="
|
||||
echo "Shinobi - Installing Node.js"
|
||||
wget https://deb.nodesource.com/setup_12.x
|
||||
chmod +x setup_12.x
|
||||
./setup_12.x
|
||||
sudo apt install nodejs -y
|
||||
sudo apt install node-pre-gyp -y
|
||||
rm setup_12.x
|
||||
else
|
||||
echo "Node.js Found..."
|
||||
echo "Version : $(node -v)"
|
||||
fi
|
||||
echo "============="
|
||||
echo "Shinobi - Installing Node.js"
|
||||
wget https://deb.nodesource.com/setup_16.x
|
||||
chmod +x setup_16.x
|
||||
./setup_16.x
|
||||
sudo apt install nodejs -y
|
||||
sudo apt install node-pre-gyp -y
|
||||
rm setup_16.x
|
||||
if ! [ -x "$(command -v npm)" ]; then
|
||||
sudo apt install npm -y
|
||||
fi
|
||||
|
@ -105,7 +100,7 @@ sudo npm install --unsafe-perm
|
|||
# sudo npm audit fix --force
|
||||
echo "============="
|
||||
echo "Shinobi - Install PM2"
|
||||
sudo npm install pm2@3.0.0 -g
|
||||
sudo npm install pm2@latest -g
|
||||
echo "Shinobi - Finished"
|
||||
sudo chmod -R 755 .
|
||||
touch INSTALL/installed.txt
|
||||
|
|
|
@ -47,11 +47,11 @@ fi
|
|||
if ! [ -x "$(command -v node)" ]; then
|
||||
echo "============="
|
||||
echo "Shinobi - Installing Node.js"
|
||||
wget https://deb.nodesource.com/setup_12.x
|
||||
chmod +x setup_12.x
|
||||
./setup_12.x
|
||||
wget https://deb.nodesource.com/setup_16.x
|
||||
chmod +x setup_16.x
|
||||
./setup_16.x
|
||||
sudo apt install nodejs -y
|
||||
rm setup_12.x
|
||||
rm setup_16.x
|
||||
else
|
||||
echo "Node.js Found..."
|
||||
echo "Version : $(node -v)"
|
||||
|
@ -112,7 +112,7 @@ sudo npm install --unsafe-perm
|
|||
# sudo npm audit fix --force
|
||||
echo "============="
|
||||
echo "Shinobi - Install PM2"
|
||||
sudo npm install pm2@3.0.0 -g
|
||||
sudo npm install pm2@latest -g
|
||||
echo "Shinobi - Finished"
|
||||
sudo chmod -R 755 .
|
||||
touch INSTALL/installed.txt
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
// Subscribe : https://licenses.shinobi.video/subscribe?planSubscribe=plan_G31AZ9mknNCa6z
|
||||
// PayPal : paypal@m03.ca
|
||||
//
|
||||
const io = new (require('socket.io'))()
|
||||
const io = new (require('socket.io').Server)()
|
||||
//process handlers
|
||||
const s = require('./libs/process.js')(process,__dirname)
|
||||
//load extender functions
|
||||
|
@ -33,6 +33,10 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => {
|
|||
require('./libs/auth.js')(s,config,lang)
|
||||
//express web server with ejs
|
||||
const app = require('./libs/webServer.js')(s,config,lang,io)
|
||||
//data port
|
||||
require('./libs/dataPort.js')(s,config,lang,app,io)
|
||||
//page layout load
|
||||
require('./libs/definitions.js')(s,config,lang,app,io)
|
||||
//web server routes : page handling..
|
||||
require('./libs/webServerPaths.js')(s,config,lang,app,io)
|
||||
//web server routes for streams : streams..
|
||||
|
@ -67,8 +71,6 @@ require('./libs/ffmpeg.js')(s,config,lang, async () => {
|
|||
require('./libs/rtmpserver.js')(s,config,lang)
|
||||
//dropInEvents server (file manipulation to create event trigger)
|
||||
require('./libs/dropInEvents.js')(s,config,lang,app,io)
|
||||
//form fields to drive the internals
|
||||
require('./libs/definitions.js')(s,config,lang,app,io)
|
||||
//notifiers : discord..
|
||||
require('./libs/notification.js')(s,config,lang)
|
||||
//branding functions and config defaults
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
"passwordType": "sha256",
|
||||
"detectorMergePamRegionTriggers": true,
|
||||
"wallClockTimestampAsDefault": true,
|
||||
"useBetterP2P": true,
|
||||
"smtpServerOptions": {
|
||||
"allowInsecureAuth": true
|
||||
},
|
||||
"addStorage": [
|
||||
{"name":"second","path":"__DIR__/videos2"}
|
||||
],
|
||||
|
|
File diff suppressed because it is too large
Load Diff
2146
languages/ar.json
2146
languages/ar.json
File diff suppressed because it is too large
Load Diff
|
@ -18,8 +18,23 @@
|
|||
"deleteSubAccountText": "Do you want to delete this Sub-Account? You cannot recover it.",
|
||||
"Turn Speed": "Turn Speed",
|
||||
"Session Key": "Session Key",
|
||||
"Active Monitors": "Active Monitors",
|
||||
"Storage Use": "Storage Use",
|
||||
"Use Raw Snapshot": "Use Raw Snapshot",
|
||||
"Account Edited": "Account Edited",
|
||||
"Failed to Edit Account": "Failed to Edit Account",
|
||||
"Login": "Login",
|
||||
"Substream": "Substream",
|
||||
"Use Substream": "Use Substream",
|
||||
"useSubStreamOnlyWhenWatching": "Only When Watching, Use Substream",
|
||||
"substreamText": "This is an On-Demand method of viewing the Live Stream. You can make it so the viewing process is available only when someone is watching or to be used for switching between Low and High Resolution.",
|
||||
"substreamConnectionText": "You can leave the Connection detail as-is if you want it to use the main Connection information set above.",
|
||||
"substreamOutputText": "Here you can set the On-Demand Stream's configuration. Learn about <a href='https://hub.shinobi.video/articles/view/Eug1dxIdhwY6zTw' target='_blank'>latency of Stream types here.</a>",
|
||||
"Toggle Substream": "Toggle Substream",
|
||||
"Output": "Output",
|
||||
"SubstreamNotConfigured": "Substream not configured. Open your Monitor Settings and configure it.",
|
||||
"Substream Process": "Substream Process",
|
||||
"Welcome": "Welcome!",
|
||||
"API Key Action Failed": "API Key Action Failed",
|
||||
"Authenticate": "Authenticate",
|
||||
"Dashboard": "Dashboard",
|
||||
|
@ -27,6 +42,7 @@
|
|||
"Admin": "Admin",
|
||||
"Superuser": "Superuser",
|
||||
"Dashcam": "Dashcam",
|
||||
"System Level": "System Level",
|
||||
"Email": "Email",
|
||||
"Username": "Username",
|
||||
"Profile": "Profile",
|
||||
|
@ -92,18 +108,18 @@
|
|||
"Allow API Trigger": "Allow API Trigger",
|
||||
"When Detector is Off": "When Detector is Off",
|
||||
"When Detector is On": "When Detector is On",
|
||||
"January" : "January",
|
||||
"February" : "February",
|
||||
"March" : "March",
|
||||
"April" : "April",
|
||||
"January": "January",
|
||||
"February": "February",
|
||||
"March": "March",
|
||||
"April": "April",
|
||||
"May": "May",
|
||||
"June" : "June",
|
||||
"July" : "July",
|
||||
"August" : "August",
|
||||
"September" : "September",
|
||||
"October" : "October",
|
||||
"November" : "November",
|
||||
"December" : "December",
|
||||
"June": "June",
|
||||
"July": "July",
|
||||
"August": "August",
|
||||
"September": "September",
|
||||
"October": "October",
|
||||
"November": "November",
|
||||
"December": "December",
|
||||
"Sunday": "Sunday",
|
||||
"Monday": "Monday",
|
||||
"Tuesday": "Tuesday",
|
||||
|
@ -403,6 +419,7 @@
|
|||
"Autosave": "Autosave",
|
||||
"Save Directory": "Save Directory",
|
||||
"CSS": "CSS <small>Style your dashboard.</small>",
|
||||
"Don't Stretch Monitors": "Don't Stretch Monitors",
|
||||
"Force Monitors Per Row": "Force Monitors Per Row",
|
||||
"Monitors per row": "Monitors per row <small>for Montage</small>",
|
||||
"Browser Console Log": "Browser Console Log",
|
||||
|
@ -496,12 +513,16 @@
|
|||
"monitorEditFailedMaxReached": "Your account has reached the maximum number of cameras that can be created. Speak to an administrator if you would like this changed.",
|
||||
"Sub-Accounts": "Sub-Accounts",
|
||||
"Stream in Background": "Stream in Background",
|
||||
"Carousel in Background": "Carousel in Background",
|
||||
"Last": "Last",
|
||||
"in": "in",
|
||||
"ago": "ago",
|
||||
"a few seconds": "a few seconds",
|
||||
"a minute": "a minute",
|
||||
"minute": "minute",
|
||||
"minutes": "minutes",
|
||||
"an hour": "an hour",
|
||||
"hour": "hour",
|
||||
"hours": "hours",
|
||||
"a day": "a day",
|
||||
"days": "days",
|
||||
|
@ -511,8 +532,11 @@
|
|||
"a year": "a year",
|
||||
"years": "years",
|
||||
"Identity": "Identity",
|
||||
"Additional Inputs": "Additional Inputs",
|
||||
"Input Map": "Input Map",
|
||||
"Input": "Input",
|
||||
"Input Feed": "Input Feed",
|
||||
"Input Feeds Selected": "Input Feed Selected",
|
||||
"Timezone": "Timezone",
|
||||
"Timezone Offset": "Timezone Offset",
|
||||
"Stream": "Stream",
|
||||
|
@ -545,6 +569,15 @@
|
|||
"accountSettingsDescription": "Manage your Profile and set options like Max Storage Amount and Max Number of Days to keep videos.",
|
||||
"eventFiltersDescription": "Setup filters for when Events occur.",
|
||||
"monitorConfigFinderDescription": "This tool will help you search for configurations for cameras posted by the community. All hosted on <a href='https://hub.shinobi.video/explore' target='_blank'>ShinobiHub</a>. You can post yours too, it would really help the community :)",
|
||||
"License Key": "License Key",
|
||||
"License Activation": "License Activation",
|
||||
"License Activation Failed": "License Activation Failed",
|
||||
"License Activated": "License Activated",
|
||||
"Network Manager": "Network Manager",
|
||||
"Nameservers": "Nameservers",
|
||||
"Interface": "Interface",
|
||||
"Optional": "Optional",
|
||||
"Prefix": "Prefix",
|
||||
"Saved": "Saved",
|
||||
"Not Saved": "Not Saved",
|
||||
"Not Connected": "Not Connected",
|
||||
|
@ -566,6 +599,7 @@
|
|||
"Creation Interval": "Creation Interval",
|
||||
"Plugin": "Plugin",
|
||||
"Plugin Manager": "Plugin Manager",
|
||||
"MonitorStatesText": "You can learn about how to use this <a href='https://hub.shinobi.video/articles/view/6ylYHj9MemlZwrM' target='_blank'>here on ShinobiHub</a>.",
|
||||
"IdentityText1": "This is how the system will identify the data for this stream. You cannot change the <b>Monitor ID</b> once you have pressed save. If you want you can make the <b>Monitor ID</b> more human readable before you continue.",
|
||||
"IdentityText2": "You can duplicate a monitor by modifying the <b>Monitor ID</b> then pressing save. You <b>cannot</b> use the ID of a monitor that already exists or it will save over that monitor's database information.",
|
||||
"opencvCascadesText": "If you see nothing here then just download this package of <a href=\"https://cdn.shinobi.video/weights/cascades.zip\">cascades</a>. Drop them into <code>plugins/opencv/cascades</code> then press refresh <i class=\"fa fa-retweet\"></i>.",
|
||||
|
@ -583,9 +617,7 @@
|
|||
"Mode": "Mode",
|
||||
"Run Installer": "Run Installer",
|
||||
"Install": "Install",
|
||||
"Enable": "Enable",
|
||||
"Disable": "Disable",
|
||||
"Delete": "Delete",
|
||||
"Add All": "Add All",
|
||||
"Name": "Name",
|
||||
"Skip Ping": "Skip Ping",
|
||||
|
@ -700,7 +732,7 @@
|
|||
"Allow Next Command": "Allow Next Command",
|
||||
"Allow Next Trigger": "Allow Next Trigger",
|
||||
"Save Events to SQL": "Save Events to SQL",
|
||||
"Email on Trigger": "Email on Trigger <small>Emails go to the main account holder's login address.</small>",
|
||||
"Email on Trigger": "Email on Trigger",
|
||||
"Attach Video Clip": "Attach Video Clip",
|
||||
"Error While Decoding": "Error While Decoding",
|
||||
"ErrorWhileDecodingText": "Your hardware may have an unstable connection to the network. Check your network connections.",
|
||||
|
@ -716,6 +748,7 @@
|
|||
"NotifyErrorText": "Sending Notification caused an Error",
|
||||
"Check the Channel ID": "Check the Channel ID",
|
||||
"Check the Recipient ID": "Check the Recipient ID",
|
||||
"AppNotEnabledText": "App Not Enabled, Enable it in your Account Settings.",
|
||||
"DiscordNotEnabledText": "Discord Bot Not Enabled, Enable it in your Account Settings.",
|
||||
"Account Settings": "Account Settings",
|
||||
"How to Record": "How to Record",
|
||||
|
@ -862,8 +895,8 @@
|
|||
"Restarting": "Restarting",
|
||||
"Starting": "Starting",
|
||||
"Watching": "Watching",
|
||||
"Recording": "Recording",
|
||||
"Stopped": "Stopped",
|
||||
"Stopping": "Stopping",
|
||||
"Died": "Died",
|
||||
"Restart": "Restart",
|
||||
"Monitor Stopped": "Monitor Stopped",
|
||||
|
@ -912,6 +945,8 @@
|
|||
"MailError": "MAIL ERROR : Could not send email, Check conf.json. Skipping any features relying on mailing.",
|
||||
"updateKeyText1": "\"updateKey\" is missing from \"conf.json\", cannot do updates this way until you add it.",
|
||||
"updateKeyText2": "\"updateKey\" is incorrect.",
|
||||
"Control Trigger Started": "Control Trigger Started",
|
||||
"Control Triggered": "Control Triggered",
|
||||
"Control Error": "Control Error",
|
||||
"Database row does not exist": "Database row does not exist",
|
||||
"File Delete Error": "File Delete Error",
|
||||
|
@ -992,52 +1027,51 @@
|
|||
"notPermitted1": "This action is not permitted by the administrator of your account.'",
|
||||
"Not Authorized": "Not Authorized",
|
||||
"Generate Subtitles": "Generate Subtitles",
|
||||
"Video Limit":"Video Limit",
|
||||
"Preview":"Preview",
|
||||
"Websocket Connected":"Websocket Connected",
|
||||
"Websocket Disconnected":"Websocket Disconnected",
|
||||
"Videos Merge":"Videos Merge",
|
||||
"Token":"Token",
|
||||
"Channel ID":"Channel ID",
|
||||
"Recipient ID":"Recipient ID",
|
||||
"New Authentication Token":"New Authentication Token",
|
||||
"All Logs":"All Logs",
|
||||
"For Group":"For Group",
|
||||
"Basic Authentication":"Basic Authentication",
|
||||
"Superuser Logs":"Superuser Logs",
|
||||
"Authentication Failed":"Authentication Failed",
|
||||
"Max Number of Cameras":"Max Number of Cameras",
|
||||
"Can edit Max Storage":"Can edit Max Storage",
|
||||
"Can edit Max Days":"Can edit Max Days",
|
||||
"in Days":"in Days",
|
||||
"Can edit how long to keep Logs":"Can edit how long to keep Logs",
|
||||
"Can use Admin Panel":"Can use Admin Panel",
|
||||
"Can use Discord Bot":"Can use Discord Bot",
|
||||
"Can use WebDAV":"Can use WebDAV",
|
||||
"Can use Amazon S3":"Can use Amazon S3",
|
||||
"Can use SFTP":"Can use SFTP",
|
||||
"Can use Wasabi Hot Cloud Storage":"Can use Wasabi Hot Cloud Storage",
|
||||
"Can use LDAP":"Can use LDAP",
|
||||
"Can View Logs":"Can View Logs",
|
||||
"Can edit how long to keep Events":"Can edit how long to keep Events",
|
||||
"Leave blank for unlimited":"Leave blank for unlimited",
|
||||
"privateKey":"Private Key",
|
||||
"Limited":"Limited",
|
||||
"All Privileges":"All Privileges",
|
||||
"LDAP":"LDAP",
|
||||
"LDAP Success":"LDAP Success",
|
||||
"LDAP User Authenticated":"LDAP User Authenticated",
|
||||
"LDAP User is New":"LDAP User is New",
|
||||
"Creating New Account":"Creating New Account",
|
||||
"bindDN":"bindDN",
|
||||
"Bind Credentials":"Bind Credentials (Password)",
|
||||
"Search Base":"Search Base",
|
||||
"Configuration":"Configuration",
|
||||
"Blank for No Change":"Blank for No Change",
|
||||
"Pop":"Pop",
|
||||
"Recording FPS Change on Start":"Recording FPS Change on Start",
|
||||
"Save Frames to Events":"Save Frames to Events",
|
||||
"Search Filter":"Search Filter",
|
||||
"Video Limit": "Video Limit",
|
||||
"Preview": "Preview",
|
||||
"Websocket Connected": "Websocket Connected",
|
||||
"Websocket Disconnected": "Websocket Disconnected",
|
||||
"Videos Merge": "Videos Merge",
|
||||
"Channel ID": "Channel ID",
|
||||
"Recipient ID": "Recipient ID",
|
||||
"New Authentication Token": "New Authentication Token",
|
||||
"All Logs": "All Logs",
|
||||
"For Group": "For Group",
|
||||
"Basic Authentication": "Basic Authentication",
|
||||
"Superuser Logs": "Superuser Logs",
|
||||
"Authentication Failed": "Authentication Failed",
|
||||
"Max Number of Cameras": "Max Number of Cameras",
|
||||
"Can edit Max Storage": "Can edit Max Storage",
|
||||
"Can edit Max Days": "Can edit Max Days",
|
||||
"in Days": "in Days",
|
||||
"Can edit how long to keep Logs": "Can edit how long to keep Logs",
|
||||
"Can use Admin Panel": "Can use Admin Panel",
|
||||
"Can use Discord Bot": "Can use Discord Bot",
|
||||
"Can use WebDAV": "Can use WebDAV",
|
||||
"Can use Amazon S3": "Can use Amazon S3",
|
||||
"Can use SFTP": "Can use SFTP",
|
||||
"Can use Wasabi Hot Cloud Storage": "Can use Wasabi Hot Cloud Storage",
|
||||
"Can use LDAP": "Can use LDAP",
|
||||
"Can View Logs": "Can View Logs",
|
||||
"Can edit how long to keep Events": "Can edit how long to keep Events",
|
||||
"Leave blank for unlimited": "Leave blank for unlimited",
|
||||
"privateKey": "Private Key",
|
||||
"Limited": "Limited",
|
||||
"All Privileges": "All Privileges",
|
||||
"LDAP": "LDAP",
|
||||
"LDAP Success": "LDAP Success",
|
||||
"LDAP User Authenticated": "LDAP User Authenticated",
|
||||
"LDAP User is New": "LDAP User is New",
|
||||
"Creating New Account": "Creating New Account",
|
||||
"bindDN": "bindDN",
|
||||
"Bind Credentials": "Bind Credentials (Password)",
|
||||
"Search Base": "Search Base",
|
||||
"Configuration": "Configuration",
|
||||
"Blank for No Change": "Blank for No Change",
|
||||
"Pop": "Pop",
|
||||
"Recording FPS Change on Start": "Recording FPS Change on Start",
|
||||
"Save Frames to Events": "Save Frames to Events",
|
||||
"Search Filter": "Search Filter",
|
||||
"h264_cuvid": "H.264 CUVID",
|
||||
"hevc_cuvid": "H.265 CUVID",
|
||||
"mjpeg_cuvid": "MJPEG CUVID",
|
||||
|
@ -1084,72 +1118,71 @@
|
|||
"FLV": "FLV",
|
||||
"FLV Stream Type": "FLV Stream Type",
|
||||
"Link Shinobi": "Link Shinobi",
|
||||
"Show Stream HUD":"Show Stream HUD",
|
||||
"Call Method":"Call Method",
|
||||
"Gender":"Gender",
|
||||
"Emotion":"Emotion",
|
||||
"Age":"Age",
|
||||
"Object":"Object",
|
||||
"Uniform":"Uniform",
|
||||
"Pose":"Pose",
|
||||
"Male":"Male",
|
||||
"Female":"Female",
|
||||
"Channel":"Channel",
|
||||
"Stream Key":"Stream Key",
|
||||
"Server URL":"Server URL",
|
||||
"Video Bit Rate":"Video Bit Rate",
|
||||
"Audio Bit Rate":"Audio Bit Rate",
|
||||
"RTMP Stream Flags":"RTMP Stream Flags",
|
||||
"RTMP Stream":"RTMP Stream",
|
||||
"Stream Channel":"Stream Channel",
|
||||
"Confidence":"Confidence",
|
||||
"Trainer Engine":"Trainer Engine",
|
||||
"Train":"Train",
|
||||
"openImagesDownloadConfirm":"Are you sure you want to begin download images and bounding boxes (preset Matrices) from OpenImages?",
|
||||
"openImagesDownloadConfirmStop":"Are you sure you want to stop training?",
|
||||
"TrainConfirm":"Are you sure you want to begin training? This can take more than 12 hours with over 500 images. This will consume a large amount of resources, like RAM and/or CPU.",
|
||||
"TrainConfirmStop":"Are you sure you want to stop training?",
|
||||
"Batch":"Batch",
|
||||
"Subdivision":"Subdivision",
|
||||
"Map":"Map",
|
||||
"Delay for Snapshot":"Delay for Snapshot",
|
||||
"Add Map":"Add Map",
|
||||
"Add Input Feed":"Add Input Feed",
|
||||
"Add Channel":"Add Channel",
|
||||
"Automatic":"Automatic",
|
||||
"Max Latency":"Max Latency",
|
||||
"Loop Stream":"Loop Stream",
|
||||
"Object Count":"Object Count",
|
||||
"Object Tag":"Object Tag",
|
||||
"Noise Filter":"Noise Filter",
|
||||
"Noise Filter Range":"Noise Filter Range",
|
||||
"TV Channel":"TV Channel",
|
||||
"Channel ID":"Channel ID",
|
||||
"TV Channel ID":"TV Channel ID",
|
||||
"TV Channel Group":"TV Channel Group",
|
||||
"Emotion Average":"Emotion Average",
|
||||
"Require Object to be in Region":"Require Object to be in Region",
|
||||
"Numeric criteria unsupported for Region tests, Ignoring Conditional":"Numeric criteria unsupported for Region tests, Ignoring Conditional",
|
||||
"Text criteria unsupported for Object Count tests, Ignoring Conditional":"Text criteria unsupported for Object Count tests, Ignoring Conditional",
|
||||
"Show Regions of Interest":"Show Regions of Interest",
|
||||
"Confidence of Detection":"Confidence of Detection",
|
||||
"Edit Selected":"Edit Selected",
|
||||
"Copy Stream Channels":"Copy Stream Channels",
|
||||
"Copy Settings":"Copy Settings",
|
||||
"Copy to Settings":"Copy to Settings",
|
||||
"Copy Mode":"Copy Mode",
|
||||
"Copy Group Settings":"Copy Group Settings",
|
||||
"Copy Timelapse Settings":"Copy Timelapse Settings",
|
||||
"Copy Connection Settings":"Copy Connection Settings",
|
||||
"Copy Custom Settings":"Copy Custom Settings",
|
||||
"Copy Logging Settings":"Copy Logging Settings",
|
||||
"Copy JPEG API Settings":"Copy JPEG API Settings",
|
||||
"Copy Input Settings":"Copy Input Settings",
|
||||
"Copy Stream Settings":"Copy Stream Settings",
|
||||
"Copy Stream Channel Settings":"Copy Stream Channel Settings",
|
||||
"Copy Recording Settings":"Copy Recording Settings",
|
||||
"Copy Detector Settings":"Copy Detector Settings",
|
||||
"Monitors to Copy to":"Monitors to Copy to",
|
||||
"Show Stream HUD": "Show Stream HUD",
|
||||
"Call Method": "Call Method",
|
||||
"Gender": "Gender",
|
||||
"Emotion": "Emotion",
|
||||
"Age": "Age",
|
||||
"Object": "Object",
|
||||
"Uniform": "Uniform",
|
||||
"Pose": "Pose",
|
||||
"Male": "Male",
|
||||
"Female": "Female",
|
||||
"Channel": "Channel",
|
||||
"Stream Key": "Stream Key",
|
||||
"Server URL": "Server URL",
|
||||
"Video Bit Rate": "Video Bit Rate",
|
||||
"Audio Bit Rate": "Audio Bit Rate",
|
||||
"RTMP Stream Flags": "RTMP Stream Flags",
|
||||
"RTMP Stream": "RTMP Stream",
|
||||
"Stream Channel": "Stream Channel",
|
||||
"Confidence": "Confidence",
|
||||
"Trainer Engine": "Trainer Engine",
|
||||
"Train": "Train",
|
||||
"openImagesDownloadConfirm": "Are you sure you want to begin download images and bounding boxes (preset Matrices) from OpenImages?",
|
||||
"openImagesDownloadConfirmStop": "Are you sure you want to stop training?",
|
||||
"TrainConfirm": "Are you sure you want to begin training? This can take more than 12 hours with over 500 images. This will consume a large amount of resources, like RAM and/or CPU.",
|
||||
"TrainConfirmStop": "Are you sure you want to stop training?",
|
||||
"Batch": "Batch",
|
||||
"Subdivision": "Subdivision",
|
||||
"Map": "Map",
|
||||
"Delay for Snapshot": "Delay for Snapshot",
|
||||
"Add Map": "Add Map",
|
||||
"Add Input Feed": "Add Input Feed",
|
||||
"Add Channel": "Add Channel",
|
||||
"Automatic": "Automatic",
|
||||
"Max Latency": "Max Latency",
|
||||
"Loop Stream": "Loop Stream",
|
||||
"Object Count": "Object Count",
|
||||
"Object Tag": "Object Tag",
|
||||
"Noise Filter": "Noise Filter",
|
||||
"Noise Filter Range": "Noise Filter Range",
|
||||
"TV Channel": "TV Channel",
|
||||
"TV Channel ID": "TV Channel ID",
|
||||
"TV Channel Group": "TV Channel Group",
|
||||
"Emotion Average": "Emotion Average",
|
||||
"Require Object to be in Region": "Require Object to be in Region",
|
||||
"Numeric criteria unsupported for Region tests, Ignoring Conditional": "Numeric criteria unsupported for Region tests, Ignoring Conditional",
|
||||
"Text criteria unsupported for Object Count tests, Ignoring Conditional": "Text criteria unsupported for Object Count tests, Ignoring Conditional",
|
||||
"Show Regions of Interest": "Show Regions of Interest",
|
||||
"Confidence of Detection": "Confidence of Detection",
|
||||
"Edit Selected": "Edit Selected",
|
||||
"Copy Stream Channels": "Copy Stream Channels",
|
||||
"Copy Settings": "Copy Settings",
|
||||
"Copy to Settings": "Copy to Settings",
|
||||
"Copy Mode": "Copy Mode",
|
||||
"Copy Group Settings": "Copy Group Settings",
|
||||
"Copy Timelapse Settings": "Copy Timelapse Settings",
|
||||
"Copy Connection Settings": "Copy Connection Settings",
|
||||
"Copy Custom Settings": "Copy Custom Settings",
|
||||
"Copy Logging Settings": "Copy Logging Settings",
|
||||
"Copy JPEG API Settings": "Copy JPEG API Settings",
|
||||
"Copy Input Settings": "Copy Input Settings",
|
||||
"Copy Stream Settings": "Copy Stream Settings",
|
||||
"Copy Stream Channel Settings": "Copy Stream Channel Settings",
|
||||
"Copy Recording Settings": "Copy Recording Settings",
|
||||
"Copy Detector Settings": "Copy Detector Settings",
|
||||
"Monitors to Copy to": "Monitors to Copy to",
|
||||
"Video Configuration": "Video Configuration",
|
||||
"ONVIF Device Manager": "ONVIF Device Manager",
|
||||
"UseCount": "UseCount",
|
||||
|
@ -1208,15 +1241,412 @@
|
|||
"getVideos": "Get Videos",
|
||||
"getVideosForMonitor": "Get Videos for Monitor",
|
||||
"No Sound": "No Sound",
|
||||
"Notification Sound":"Notification Sound",
|
||||
"Alert Sound":"Alert Sound",
|
||||
"Alert Sound Delay":"Alert Sound Delay",
|
||||
"onvifdeviceManagerGlobalTip":"ONVIF allows modifying the camera's internal settings. ONVIF is somewhat of an umbrella term, it can mean many things unfortunately. With that being the case you may see an option in this tool but it may not be editable. This is usually because the camera vendor has not added this method or has deviated from its intended usage. In those cases you will need to enter the camera's configuration through the prescribed method of the camera vendor, this is generally opening the IP Address of the camera in your web browser.",
|
||||
"onvifdeviceSavedText":"Camera's internal settings have been saved. You may need to restart the camera to have these changes take effect.",
|
||||
"onvifdeviceSavedFoundErrorText":"Some settings may have reverted to a previous value. Its possible that the modified option is not available with this camera through ONVIF.",
|
||||
"powerVideoEventLimit":"You have set a high event limit. Are you sure you want to make this request?",
|
||||
"There are no monitors that you can view with this account.":"There are no monitors that you can view with this account.",
|
||||
"Notification Sound": "Notification Sound",
|
||||
"Alert Sound": "Alert Sound",
|
||||
"Alert Sound Delay": "Alert Sound Delay",
|
||||
"onvifdeviceManagerGlobalTip": "ONVIF allows modifying the camera's internal settings. ONVIF is somewhat of an umbrella term, it can mean many things unfortunately. With that being the case you may see an option in this tool but it may not be editable. This is usually because the camera vendor has not added this method or has deviated from its intended usage. In those cases you will need to enter the camera's configuration through the prescribed method of the camera vendor, this is generally opening the IP Address of the camera in your web browser.",
|
||||
"onvifdeviceSavedText": "Camera's internal settings have been saved. You may need to restart the camera to have these changes take effect.",
|
||||
"onvifdeviceSavedFoundErrorText": "Some settings may have reverted to a previous value. Its possible that the modified option is not available with this camera through ONVIF.",
|
||||
"powerVideoEventLimit": "You have set a high event limit. Are you sure you want to make this request?",
|
||||
"There are no monitors that you can view with this account.": "There are no monitors that you can view with this account.",
|
||||
"Delete Monitors and Files": "Delete Monitors and Files",
|
||||
"Select atleast one monitor to delete": "Select atleast one monitor to delete.",
|
||||
"Use Built-In":"Use Built-In"
|
||||
"Use Built-In": "Use Built-In",
|
||||
"Add Cameras": "Add Cameras",
|
||||
"Add Camera": "Add Camera",
|
||||
"Delete Camera": "Delete Camera",
|
||||
"Event Rules": "Event Rules",
|
||||
"Other Devices": "Other Devices",
|
||||
"Zones": "Zones",
|
||||
"Information": "Information",
|
||||
"Info": "Info",
|
||||
"Motion Threshold": "Motion Threshold",
|
||||
"Attach Snapshot": "Attach Snapshot",
|
||||
"Invalid Settings": "Invalid Settings",
|
||||
"Detection": "Detection",
|
||||
"Playback": "Playback",
|
||||
"Backup": "Backup",
|
||||
"Close All Monitors": "Close All Monitors",
|
||||
"Daily Events": "Daily Events",
|
||||
"Send Notification": "Send Notification",
|
||||
"Send to": "Send to",
|
||||
"setMaxStorageAmountText": "You should set your Max Storage Amount in your Account Settings located on the left. Find the option under the Profile section. Default is 10 GB.",
|
||||
"Save Events": "Save Events",
|
||||
"Original Choice": "Original Choice",
|
||||
"Legacy Webhook": "Legacy Webhook",
|
||||
"eventFilterActionText": "These are the actions that occur from the filter conditions that have succeeded. \"Original Choice\" refers to the option you had chosen in your Monitor's Settings.",
|
||||
"Telegram": "Telegram",
|
||||
"Before": "Before",
|
||||
"After": "After",
|
||||
"Rule": "Rule",
|
||||
"Event Filter Error": "Event Filter Error",
|
||||
"eventFilterErrorBrackets": "You have an un-even number of brackets. They are being ignored.",
|
||||
"Quick Settings": "Quick Settings",
|
||||
"Copy Stream URL": "Copy Stream URL",
|
||||
"willTriggerAnEvent": "will trigger an event",
|
||||
"Cloud": "Cloud",
|
||||
"Objects to look for": "Objects to look for",
|
||||
"Common Objects": "Common Objects",
|
||||
"Uncommon Objects": "Uncommon Objects",
|
||||
"person": "person",
|
||||
"bicycle": "bicycle",
|
||||
"car": "car",
|
||||
"motorcycle": "motorcycle",
|
||||
"airplane": "airplane",
|
||||
"bus": "bus",
|
||||
"train": "train",
|
||||
"truck": "truck",
|
||||
"boat": "boat",
|
||||
"traffic light": "traffic light",
|
||||
"fire hydrant": "fire hydrant",
|
||||
"stop sign": "stop sign",
|
||||
"parking meter": "parking meter",
|
||||
"bench": "bench",
|
||||
"bird": "bird",
|
||||
"cat": "cat",
|
||||
"dog": "dog",
|
||||
"horse": "horse",
|
||||
"sheep": "sheep",
|
||||
"cow": "cow",
|
||||
"elephant": "elephant",
|
||||
"bear": "bear",
|
||||
"zebra": "zebra",
|
||||
"giraffe": "giraffe",
|
||||
"backpack": "backpack",
|
||||
"umbrella": "umbrella",
|
||||
"handbag": "handbag",
|
||||
"tie": "tie",
|
||||
"suitcase": "suitcase",
|
||||
"frisbee": "frisbee",
|
||||
"skis": "skis",
|
||||
"snowboard": "snowboard",
|
||||
"sports ball": "sports ball",
|
||||
"kite": "kite",
|
||||
"baseball bat": "baseball bat",
|
||||
"baseball glove": "baseball glove",
|
||||
"skateboard": "skateboard",
|
||||
"surfboard": "surfboard",
|
||||
"tennis racket": "tennis racket",
|
||||
"bottle": "bottle",
|
||||
"wine glass": "wine glass",
|
||||
"cup": "cup",
|
||||
"fork": "fork",
|
||||
"knife": "knife",
|
||||
"spoon": "spoon",
|
||||
"bowl": "bowl",
|
||||
"banana": "banana",
|
||||
"apple": "apple",
|
||||
"sandwich": "sandwich",
|
||||
"orange": "orange",
|
||||
"broccoli": "broccoli",
|
||||
"carrot": "carrot",
|
||||
"hot dog": "hot dog",
|
||||
"pizza": "pizza",
|
||||
"donut": "donut",
|
||||
"cake": "cake",
|
||||
"chair": "chair",
|
||||
"couch": "couch",
|
||||
"potted plant": "potted plant",
|
||||
"bed": "bed",
|
||||
"dining table": "dining table",
|
||||
"toilet": "toilet",
|
||||
"tv": "tv",
|
||||
"laptop": "laptop",
|
||||
"mouse": "mouse",
|
||||
"remote": "remote",
|
||||
"keyboard": "keyboard",
|
||||
"cell phone": "cell phone",
|
||||
"microwave": "microwave",
|
||||
"oven": "oven",
|
||||
"toaster": "toaster",
|
||||
"sink": "sink",
|
||||
"refrigerator": "refrigerator",
|
||||
"book": "book",
|
||||
"clock": "clock",
|
||||
"vase": "vase",
|
||||
"scissors": "scissors",
|
||||
"teddy bear": "teddy bear",
|
||||
"hair drier": "hair drier",
|
||||
"toothbrush": "toothbrush",
|
||||
"Detection Event": "Detection Event",
|
||||
"Monitor Edit": "Monitor Edit",
|
||||
"Monitor Start": "Monitor Start",
|
||||
"Monitor Stop": "Monitor Stop",
|
||||
"Monitor Died": "Monitor Died",
|
||||
"Account Save": "Account Save",
|
||||
"User Log": "User Log",
|
||||
"Frigate": "Frigate",
|
||||
"Plain": "Plain",
|
||||
"MQTT Error": "MQTT Error",
|
||||
"MQTT Inbound": "MQTT Inbound",
|
||||
"MQTT Outbound": "MQTT Outbound",
|
||||
"MQTT Client": "MQTT Client",
|
||||
"fieldTextMode": "This is the primary task of the monitor.",
|
||||
"fieldTextModeDisabled": "Inactive monitor, no process will be created in this mode.",
|
||||
"fieldTextModeWatchOnly": "Monitor will only stream, no recording will occur unless otherwise ordered by API or Detector.",
|
||||
"fieldTextModeRecord": "Continuous Recording. Segments are made every 15 minutes by default.",
|
||||
"fieldTextMid": "This is a non-changeable identifier for the monitor. You can duplicate a monitor by double clicking the Monitor ID and changing it.",
|
||||
"fieldTextName": "This is the human-readable display name for the monitor.",
|
||||
"fieldTextMaxKeepDays": "The number of days to keep videos before purging for this monitor specifically.",
|
||||
"fieldTextNotes": "Comments you want to leave for this camera.",
|
||||
"fieldTextDir": "Location of where recorded files will be saved. You can configure more locations with the <code>addStorage</code> variable.",
|
||||
"fieldTextType": "The method that will used to consume the video stream.",
|
||||
"fieldTextTypeJPEG": "Reading snapshots from a URL and making a stream and/or video from them.",
|
||||
"fieldTextTypeMJPEG": "Similar to JPEG except the frame handling is done by FFMPEG, not Shinobi.",
|
||||
"fieldTextTypeH.264/H.265/H.265+": "Reading a high quality video streas that sometimes include audio.",
|
||||
"fieldTextTypeHLS(.m3u8)": "Reading a high quality video streas that sometimes include audio.",
|
||||
"fieldTextTypeMPEG4(.mp4/.ts)": "A static file. Read at a lower rate and should not be used for an actual live stream.",
|
||||
"fieldTextTypeShinobiStreamer": "Websocket JPEG-based P2P stream.",
|
||||
"fieldTextTypeDashcam(StreamerV2)": "Websocket WebM-based P2P stream.",
|
||||
"fieldTextTypeLocal": "Reading Capture Cards, Webcams, or Integrated Cameras.",
|
||||
"fieldTextTypeRTMP": "Learn to connect here : <a href=\"https://shinobi.video/articles/2019-02-14-how-to-push-streams-to-shinobi-with-rtmp\" target=\"_blank\">Article : How to Push Streams via RTMP to Shinobi</a>",
|
||||
"fieldTextTypeMxPEG": "Mobotix MJPEG Stream",
|
||||
"fieldTextRtmpKey": "Stream Key for incoming streams on the RTMP port.",
|
||||
"fieldTextAutoHostEnable": "Feed the individual pieces required to build a stream URL or provide the full URL and allow Shinobi to parse it for you.",
|
||||
"fieldTextAutoHost": "The full Stream URL.",
|
||||
"fieldTextProtocol": "The protocol that will used to consume the video stream.",
|
||||
"fieldTextRtspTransport": "The transport protocol your camera will use. TCP is usually the best choice.",
|
||||
"fieldTextRtspTransportAuto": "Let FFMPEG decide. Normally it will try UDP first.",
|
||||
"fieldTextRtspTransportTCP": "Set it to this if UDP starts giving undesired results.",
|
||||
"fieldTextRtspTransportUDP": "FFMPEG tries this first.",
|
||||
"fieldTextRtspTransportHTTP": "Standard connection method.",
|
||||
"fieldTextMuser": "The user login for your camera",
|
||||
"fieldTextMpass": "The password for your camera",
|
||||
"fieldTextHost": "Connection address",
|
||||
"fieldTextPort": "Separate by Commas or a Range",
|
||||
"fieldTextPortForce": "Using the default web port can allow automatic switch to other ports for streams like RTSP.",
|
||||
"fieldTextPath": "The path to your camera",
|
||||
"fieldTextFatalMax": "The number of times to retry for network connection between the server and camera before setting the monitor to Disabled. No decimals. Set to 0 to retry forever.",
|
||||
"fieldTextSkipPing": "Choose if a successful ping is required before a monitor process is started.",
|
||||
"fieldTextIsOnvif": "Is this an ONVIF compliant camera?",
|
||||
"fieldTextOnvifNonStandard": "Is this a Non-Standard ONVIF camera?",
|
||||
"fieldTextOnvifPort": "ONVIF is usually run on port <code>8000</code>. This can be <code>80</code> as well depending on your camera model.",
|
||||
"fieldTextAduration": "Specify how many microseconds are analyzed to probe the input. Set to 100000 if you are using RTSP and having stream issues.",
|
||||
"fieldTextProbesize": "Specify how big to make the analyzation probe for the input. Set to 100000 if you are using RTSP and having stream issues.",
|
||||
"fieldTextStreamLoop": "Loop a static file so the file stream behaves like a live stream.",
|
||||
"fieldTextSfps": "Specify the Frame Rate (FPS) in which the camera is providing its stream in.",
|
||||
"fieldTextWallClockTimestampIgnore": "Base all incoming camera data in camera time instead of server time.",
|
||||
"fieldTextHeight": "Height of the stream image.",
|
||||
"fieldTextWidth": "Width of the stream image.",
|
||||
"fieldTextAccelerator": "Hardware Acceleration (HWAccel) for decoding streams.",
|
||||
"fieldTextHwaccel": "Decoding Engine",
|
||||
"fieldTextHwaccelVcodec": "Decoding Engine",
|
||||
"fieldTextStreamType": "The method that will used to consume the video stream.",
|
||||
"fieldTextStreamTypePoseidon": "Poseidon is built on Kevin Godell's MP4 processing code. It simulates a streaming MP4 file but using the data of a live stream. Includes Audio. Some browsers can play it like a regular MP4 file. Streams over HTTP or WebSocket.",
|
||||
"fieldTextStreamTypeBase64OverWebsocket": "Sending Base64 encoded frames over WebSocket. This avoids caching but there is no audio.",
|
||||
"fieldTextStreamTypeMJPEG": "Standard Motion JPEG image. No audio.",
|
||||
"fieldTextStreamTypeFLV": "Sending FLV encoded frames over WebSocket.",
|
||||
"fieldTextStreamTypeHLS(includesAudio)": "Similar method to facebook live streams. <b>Includes audio</b> if input provides it. There is a delay of about 4-6 seconds because this method records segments then pushes them to the client rather than push as while it creates them.",
|
||||
"fieldTextStreamFlvType": "This is for the Shinobi dashboard only. Both stream methods are still active and ready to use.",
|
||||
"fieldTextStreamVcodec": "Video codec for streaming.",
|
||||
"fieldTextStreamVcodecAuto": "Let FFMPEG choose.",
|
||||
"fieldTextStreamVcodecLibx264": "Used for MP4 video.",
|
||||
"fieldTextStreamVcodecLibx265": "Used for MP4 video.",
|
||||
"fieldTextStreamVcodecCopy": "Used for MP4 video. Has very low CPU usage but cannot use video filters and filesizes may be gigantic. Best to setup your MP4 settings camera-side when using this option.",
|
||||
"fieldTextStreamAcodec": "Audio codec for streaming.",
|
||||
"fieldTextStreamAcodecAuto": "Let FFMPEG choose.",
|
||||
"fieldTextStreamAcodecNoAudio": "No Audio, this is an option that must be set in some parts of the world due to legal reasons.",
|
||||
"fieldTextStreamAcodecLibvorbis": "Used for WebM video.",
|
||||
"fieldTextStreamAcodecLibopus": "Used for WebM video.",
|
||||
"fieldTextStreamAcodecLibmp3lame": "Used for MP4 video.",
|
||||
"fieldTextStreamAcodecAac": "Used for MP4 video.",
|
||||
"fieldTextStreamAcodecAc3": "Used for MP4 video.",
|
||||
"fieldTextStreamAcodecCopy": "Used for MP4 video. Has very low CPU usage but some audio codecs need custom flags like <code>-strict 2</code> for aac.",
|
||||
"fieldTextHlsTime": "How long each video segment should be, in minutes. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.",
|
||||
"fieldTextHlsListSize": "The number of segments maximum before deleting old segments automatically.",
|
||||
"fieldTextPresetStream": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.",
|
||||
"fieldTextStreamQuality": "Low number means higher quality. Higher number means less quality.",
|
||||
"fieldTextStreamFps": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.",
|
||||
"fieldTextStreamScaleX": "Width of the stream image that is output after processing.",
|
||||
"fieldTextStreamScaleY": "Height of the stream image that is output after processing.",
|
||||
"fieldTextStreamRotate": "Change the viewing angle of the video stream.",
|
||||
"fieldTextSignalCheck": "How often your client will check the stream to see if it is alive. This is calculated in minutes.",
|
||||
"fieldTextSignalCheckLog": "This is for the client side only. It will display in the log thread when client side signal checks occur.",
|
||||
"fieldTextStreamVf": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.",
|
||||
"fieldTextTvChannel": "This monitor will have TV Channel features enabled. You will be able to view it in your TV Channel list.",
|
||||
"fieldTextTvChannelId": "A Custom ID for the Channel.",
|
||||
"fieldTextTvChannelGroupTitle": "A Custom Group for the Channel.",
|
||||
"fieldTextStreamTimestamp": "A clock that is burned onto the frames of the video stream.",
|
||||
"fieldTextStreamTimestampFont": "Font File to style your timestamp.",
|
||||
"fieldTextStreamTimestampFontSize": "Font size in pt.",
|
||||
"fieldTextStreamTimestampColor": "Timstamp text color.",
|
||||
"fieldTextStreamTimestampBoxColor": "Timstamp backdrop color.",
|
||||
"fieldTextStreamTimestampX": "Horiztonal Position of Timestamp",
|
||||
"fieldTextStreamTimestampY": "Vertical Position of Timestamp",
|
||||
"fieldTextStreamWatermark": "An image that is burned onto the frames of the video stream.",
|
||||
"fieldTextStreamWatermarkLocation": "Image Location that will be used as Watermark.",
|
||||
"fieldTextStreamWatermarkPosition": "An image that is burned onto the frames of the video stream.",
|
||||
"fieldTextDetailSubstreamInputRtspTransportAuto": "Let FFMPEG decide. Normally it will try UDP first.",
|
||||
"fieldTextDetailSubstreamInputRtspTransportTCP": "Set it to this if UDP starts giving undesired results.",
|
||||
"fieldTextDetailSubstreamInputRtspTransportUDP": "FFMPEG tries this first.",
|
||||
"fieldTextDetailSubstreamOutputStreamType": "The method that will used to consume the video stream.",
|
||||
"fieldTextDetailSubstreamOutputStreamVcodec": "Video codec for streaming.",
|
||||
"fieldTextDetailSubstreamOutputStreamVcodecAuto": "Let FFMPEG choose.",
|
||||
"fieldTextDetailSubstreamOutputStreamVcodecLibx264": "Used for MP4 video.",
|
||||
"fieldTextDetailSubstreamOutputStreamVcodecLibx265": "Used for MP4 video.",
|
||||
"fieldTextDetailSubstreamOutputStreamVcodecCopy": "Used for MP4 video. Has very low CPU usage but cannot use video filters and filesizes may be gigantic. Best to setup your MP4 settings camera-side when using this option.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodec": "Audio codec for streaming.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecAuto": "Let FFMPEG choose.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecNoAudio": "No Audio, this is an option that must be set in some parts of the world due to legal reasons.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecLibvorbis": "Used for WebM video.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecLibopus": "Used for WebM video.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecLibmp3lame": "Used for MP4 video.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecAac": "Used for MP4 video.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecAc3": "Used for MP4 video.",
|
||||
"fieldTextDetailSubstreamOutputStreamAcodecCopy": "Used for MP4 video. Has very low CPU usage but some audio codecs need custom flags like <code>-strict 2</code> for aac.",
|
||||
"fieldTextDetailSubstreamOutputHlsTime": "How long each video segment should be, in minutes. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.",
|
||||
"fieldTextDetailSubstreamOutputHlsListSize": "The number of segments maximum before deleting old segments automatically.",
|
||||
"fieldTextDetailSubstreamOutputPresetStream": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.",
|
||||
"fieldTextDetailSubstreamOutputStreamQuality": "Low number means higher quality. Higher number means less quality.",
|
||||
"fieldTextDetailSubstreamOutputStreamFps": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.",
|
||||
"fieldTextDetailSubstreamOutputStreamScaleX": "Width of the stream image that is output after processing.",
|
||||
"fieldTextDetailSubstreamOutputStreamScaleY": "Height of the stream image that is output after processing.",
|
||||
"fieldTextDetailSubstreamOutputStreamRotate": "Change the viewing angle of the video stream.",
|
||||
"fieldTextDetailSubstreamOutputSvf": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces.",
|
||||
"fieldTextSnap": "Get the latest frame in JPEG.",
|
||||
"fieldTextExt": "The file type for your recorded video file.",
|
||||
"fieldTextExtMP4": "This file type is playable is almost all modern web browsers, that includes mobile. The filesize just tends to be larger unless you lower the quality.",
|
||||
"fieldTextExtWebM": "Small filesize, low client compatibility. Good for uploading to sites like YouTube.",
|
||||
"fieldTextVcodec": "Video codec for recording.",
|
||||
"fieldTextCrf": "Low number means higher quality. Higher number means less quality.",
|
||||
"fieldTextPresetRecord": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.",
|
||||
"fieldTextAcodec": "Audio codec for recording.",
|
||||
"fieldTextFps": "The speed in which frames are recorded to files, Frames Per Second. Be aware there is no default. This can lead to large files. Best to set this camera-side.",
|
||||
"fieldTextRecordScaleY": "Height of the stream image.",
|
||||
"fieldTextRecordScaleX": "Width of the stream image.",
|
||||
"fieldTextCutoff": "In minutes. When to slice off and start a new video file.",
|
||||
"fieldTextRotate": "Change the recording angle of the video stream.",
|
||||
"fieldTextVf": "Place FFMPEG video filters in this box to affect the recording portion. No spaces.",
|
||||
"fieldTextTimestamp": "A clock that is burned onto the frames of the recorded video.",
|
||||
"fieldTextTimestampFont": "Font File to style your timestamp.",
|
||||
"fieldTextTimestampFontSize": "Font size in pt.",
|
||||
"fieldTextTimestampColor": "Timstamp text color.",
|
||||
"fieldTextTimestampBoxColor": "Timstamp backdrop color.",
|
||||
"fieldTextTimestampX": "Horiztonal Position of Timestamp",
|
||||
"fieldTextTimestampY": "Vertical Position of Timestamp",
|
||||
"fieldTextWatermark": "An image that is burned onto the frames of the recorded video.",
|
||||
"fieldTextWatermarkLocation": "Image Location that will be used as Watermark.",
|
||||
"fieldTextWatermarkPosition": "An image that is burned onto the frames of the recorded video.",
|
||||
"fieldTextRecordTimelapse": "Create a JPEG based timelapse.",
|
||||
"fieldTextRecordTimelapseMp4": "Create an MP4 file at the end of each day for the timelapse.",
|
||||
"fieldTextRecordTimelapseWatermark": "An image that is burned onto the frames of the recorded video.",
|
||||
"fieldTextRecordTimelapseWatermarkLocation": "Image Location that will be used as Watermark.",
|
||||
"fieldTextRecordTimelapseWatermarkPosition": "An image that is burned onto the frames of the recorded video.",
|
||||
"fieldTextCustInput": "Custom Flags that bind to the Input of the FFMPEG process.",
|
||||
"fieldTextCustStream": "Custom Flags that bind to the Stream (client side view) of the FFMPEG process.",
|
||||
"fieldTextCustSnap": "Custom Flags that bind to the Snapshots.",
|
||||
"fieldTextCustRecord": "Custom Flags that bind to the recording of the FFMPEG process.",
|
||||
"fieldTextCustDetect": "Custom Flags that bind to the stream Detector uses for analyzation.",
|
||||
"fieldTextCustDetectObject": "Custom Flags that bind to the stream Detector uses for analyzation.",
|
||||
"fieldTextCustSipRecord": "Custom Flags that bind to the output that the Event-Based Recordings siphon from.",
|
||||
"fieldTextCustomOutput": "Add a custom output like JPEG frames or send data straight to another server.",
|
||||
"fieldTextDetector": "This will add another output in the FFMPEG command for the motion detector.",
|
||||
"fieldTextDetectorHttpApi": "Do you want to allow HTTP triggers to this camera?",
|
||||
"fieldTextDetectorSendFrames": "Push frames to the connected plugin to be analyzed.",
|
||||
"fieldTextDetectorFps": "How many frames a second to send to the motion detector; 2 is the default.",
|
||||
"fieldTextDetectorScaleX": "Width of the image being detected. Smaller sizes take less CPU.",
|
||||
"fieldTextDetectorScaleY": "Height of the image being detected. Smaller sizes take less CPU.",
|
||||
"fieldTextDetectorLockTimeout": "Lockout for when the next trigger is allowed, to avoid overloading the database and receiving clients. Measured in milliseconds.",
|
||||
"fieldTextDetectorSave": "Save Motion Events in SQL. This will allow display of motion over video during the time motion events occured in the Power Viewer.",
|
||||
"fieldTextDetectorRecordMethod": "There are multiple ways to begin recording when an event occurs, like motion. Traditional Recording is the most user-friendly.",
|
||||
"fieldTextDetectorTrigger": "This will order the camera to record if it is set to \"Watch-Only\" when an Event is detected.",
|
||||
"fieldTextDetectorTimeout": "The length of time \"Trigger Record\" will run for. This is read in minutes.",
|
||||
"fieldTextWatchdogReset": "If there is an overlap in trigger record should it reset.",
|
||||
"fieldTextDetectorWebhook": "Send a GET request to a URL with some values from the event.",
|
||||
"fieldTextDetectorWebhookTimeout": "This value is a timer to allow the next running of your Webhook. This value is in minutes.",
|
||||
"fieldTextDetectorCommand": "The command that will run. This is the equivalent of running a shell command from terminal.",
|
||||
"fieldTextDetectorCommandTimeout": "This value is a timer to allow the next running of your script. This value is in minutes.",
|
||||
"fieldTextSnapSecondsInward": "in seconds",
|
||||
"fieldTextDetectorPam": "Use Kevin Godell's Motion Detector. This is built into Shinobi and requires no other configuration to activate.",
|
||||
"fieldTextDetectorSensitivity": "The motion confidence rating must exceed this value to be seen as a trigger. This number correlates directly to the confidence rating returned by the motion detector. This option was previously named \"Indifference\".",
|
||||
"fieldTextDetectorMaxSensitivity": "The motion confidence rating must be lower than this value to be seen as a trigger. Leave blank for no maximum. This option was previously named \"Max Indifference\".",
|
||||
"fieldTextDetectorThreshold": "Minimum number of detections to fire a motion event. Detections must be within the detector the threshold divided by detector fps seconds. For example, if detector fps is 2 and trigger threshold is 3, then three detections must occur within 1.5 seconds to trigger a motion event. This threshold is per detection region.",
|
||||
"fieldTextDetectorColorThreshold": "The amount of difference allowed in a pixel before it is considered motion.",
|
||||
"fieldTextInverseTrigger": "To trigger outside specified regions. Will not trigger with Full Frame Detection enabled.",
|
||||
"fieldTextDetectorFrame": "This will read the entire frame for pixel differences. This is the same as creating a region that covers the entire screen.",
|
||||
"fieldTextDetectorNoiseFilter": "Attempt to filter grain or repeated motion at a particular indifference.",
|
||||
"fieldTextDetectorNoiseFilterRange": "The amount of difference allowed in a pixel before it is considered motion.",
|
||||
"fieldTextDetectorNotrigger": "Check if motion has occured on an interval. If motion has occurred the check will be reset.",
|
||||
"fieldTextDetectorNotriggerTimeout": "Timeout is calculated in minutes.",
|
||||
"fieldTextDetectorNotriggerDiscord": "If motion has not been detected after the timeout period you will recieve an Discord notification.",
|
||||
"fieldTextDetectorNotriggerWebhook": "Send a GET request to a URL with some values from the event.",
|
||||
"fieldTextDetectorNotriggerCommand": "The command that will run. This is the equivalent of running a shell command from terminal.",
|
||||
"fieldTextDetectorNotriggerCommandTimeout": "This value is a timer to allow the next running of your script. This value is in minutes.",
|
||||
"fieldTextDetectorAudio": "Check if Audio has occured at a certiain decible. Decible reading may not be accurate to real-world measurement.",
|
||||
"fieldTextDetectorUseDetectObject": "Create frames for sending to any connected Plugin.",
|
||||
"fieldTextDetectorSendFramesObject": "Push frames to the connected plugin to be analyzed.",
|
||||
"fieldTextDetectorObjCountInRegion": "Count Objects only inside Regions.",
|
||||
"fieldTextDetectorLisencePlate": "Enable License Plate Recognition. OpenALPR plugin has this always enabled.",
|
||||
"fieldTextDetectorLisencePlateCountry": "Choose the type of plates to recognize. Only US and EU are supported at this time.",
|
||||
"fieldTextEventRecordScaleX": "Width of the Event-based Recording image that is output after processing.",
|
||||
"fieldTextEventRecordScaleY": "Height of the Event-based Recording image that is output after processing.",
|
||||
"fieldTextDetectorBufferHlsTime": "How long each video segment should be, in seconds. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.",
|
||||
"fieldTextDetectorBufferHlsListSize": "The number of segments maximum before deleting old segments automatically.",
|
||||
"fieldTextDetectorPtzFollow": "Follow the largest detected object with PTZ? Requires an Object Detector running or matrices provided with events.",
|
||||
"fieldTextDetectorObjCount": "Count detected objects.",
|
||||
"fieldTextControlInvertY": "For When your camera is mounted upside down or uses inverted vertical controls.",
|
||||
"fieldTextDetectorSendVideoLength": "In seconds. The length of the video that gets sent to your Notification service, like Email or Discord.",
|
||||
"fieldTextLoglevel": "The amount of data to provide while doing the job.",
|
||||
"fieldTextLoglevelSilent": "None. This will silence all logging.",
|
||||
"fieldTextLoglevelFatal": "Display only fatal errors.",
|
||||
"fieldTextLoglevelOnError": "Display all important errors. Note : this doesn't always show important information.",
|
||||
"fieldTextLoglevelAllWarnings": "Display all warnings. Use this if you can't find out what's wrong with your camera.",
|
||||
"fieldTextSqllog": "Use this with caution as FFMPEG likes to throw up superfluous data at times which can lead to a lot of database rows.",
|
||||
"fieldTextSqllogNo": "No is the default.",
|
||||
"fieldTextSqllogYes": "Do this if you are having recurring issues only.",
|
||||
"fieldTextFactorAuth": "Enable a secondary requirement for login through one of the enabled methods.",
|
||||
"fieldTextMail": "The login for accounts. The main account holder's email address will get notifications.",
|
||||
"fieldTextPass": "Leave blank to keep the same password during settings modification.",
|
||||
"fieldTextPasswordAgain": "Must match Password field if you desire to change it.",
|
||||
"fieldTextSize": "The amount of disk space Shinobi will allow to be consumed before purging. This value is read in megabytes.",
|
||||
"fieldTextSizeVideoPercent": "Percent of Max Storage Amount the videos can record to.",
|
||||
"fieldTextSizeTimelapsePercent": "Percent of Max Storage Amount the timelapse frames can record to.",
|
||||
"fieldTextSizeFilebinPercent": "Percent of Max Storage Amount the FileBin archive can use.",
|
||||
"fieldTextDays": "The number of days to keep videos before purging.",
|
||||
"fieldTextEventDays": "The number of days to keep events before purging.",
|
||||
"fieldTextLogDays": "The number of days to keep logs before purging.",
|
||||
"fieldTextLang": "The primary language of text elements. For complete translation add your language in conf.json e.g:<code>\"language\": \"en_CA\",</code>",
|
||||
"fieldTextAudioNote": "Sound when information bubble appears.",
|
||||
"fieldTextAudioAlert": "Sound when Event occurs.",
|
||||
"fieldTextAudioDelay": "Delay until next time an Event can start an Alert. Measured in seconds.",
|
||||
"fieldTextEventMonPop": "When an Event occurs popout the monitor stream.",
|
||||
"fieldTextIrCutFilterOn": "Enable Ir cut fiter. Typically Day mode.",
|
||||
"fieldTextIrCutFilterOff": "Disable Ir cut fiter. Typically Night mode.",
|
||||
"fieldTextIrCutFilterAuto": "Ir cut filter is automatically activated by the device.",
|
||||
"fieldTextIp": "Range or Single",
|
||||
"fieldTextActionsHalt": "Make the event do nothing, as if it never happened.",
|
||||
"fieldTextActionsIndifference": "Modify minimum indifference required for event.",
|
||||
"fieldTextActionsCommand": "You may use this to trigger a script on command.",
|
||||
"fieldTextActionsRecord": "Use Traditional Recording, Hotswap, or Delete Motionless with their currently set options in the Global Detection Settings section.",
|
||||
"fieldTextMapRtspTransportAuto": "Let FFMPEG decide. Normally it will try UDP first.",
|
||||
"fieldTextMapRtspTransportTCP": "Set it to this if UDP starts giving undesired results.",
|
||||
"fieldTextMapRtspTransportUDP": "FFMPEG tries this first.",
|
||||
"fieldTextChannelStreamType": "The method that will used to consume the video stream.",
|
||||
"fieldTextChannelStreamTypePoseidon": "Poseidon is built on Kevin Godell's MP4 processing code. It simulates a streaming MP4 file but using the data of a live stream. Includes Audio. Some browsers can play it like a regular MP4 file. Streams over HTTP or WebSocket.",
|
||||
"fieldTextChannelStreamTypeMJPEG": "Standard Motion JPEG image. No audio.",
|
||||
"fieldTextChannelStreamTypeFLV": "Sending FLV encoded frames over WebSocket.",
|
||||
"fieldTextChannelStreamTypeHLS(includesAudio)": "Similar method to facebook live streams. <b>Includes audio</b> if input provides it. There is a delay of about 4-6 seconds because this method records segments then pushes them to the client rather than push as while it creates them.",
|
||||
"fieldTextChannelStreamVcodec": "Video codec for streaming.",
|
||||
"fieldTextChannelStreamVcodecAuto": "Let FFMPEG choose.",
|
||||
"fieldTextChannelStreamVcodecLibx264": "Used for MP4 video.",
|
||||
"fieldTextChannelStreamVcodecLibx265": "Used for MP4 video.",
|
||||
"fieldTextChannelStreamVcodecCopy": "Used for MP4 video. Has very low CPU usage but cannot use video filters and filesizes may be gigantic. Best to setup your MP4 settings camera-side when using this option.",
|
||||
"fieldTextChannelStreamAcodec": "Audio codec for streaming.",
|
||||
"fieldTextChannelStreamAcodecAuto": "Let FFMPEG choose.",
|
||||
"fieldTextChannelStreamAcodecNoAudio": "No Audio, this is an option that must be set in some parts of the world due to legal reasons.",
|
||||
"fieldTextChannelStreamAcodecLibvorbis": "Used for WebM video.",
|
||||
"fieldTextChannelStreamAcodecLibopus": "Used for WebM video.",
|
||||
"fieldTextChannelStreamAcodecLibmp3lame": "Used for MP4 video.",
|
||||
"fieldTextChannelStreamAcodecAac": "Used for MP4 video.",
|
||||
"fieldTextChannelStreamAcodecAc3": "Used for MP4 video.",
|
||||
"fieldTextChannelStreamAcodecCopy": "Used for MP4 video. Has very low CPU usage but some audio codecs need custom flags like <code>-strict 2</code> for aac.",
|
||||
"fieldTextChannelHlsTime": "How long each video segment should be, in minutes. Each segment will be drawn by the client through an m3u8 file. Shorter segments take less space.",
|
||||
"fieldTextChannelHlsListSize": "The number of segments maximum before deleting old segments automatically.",
|
||||
"fieldTextChannelPresetStream": "Preset flag for certain video encoders. If you find your camera is crashing every few seconds : try leaving it blank.",
|
||||
"fieldTextChannelStreamQuality": "Low number means higher quality. Higher number means less quality.",
|
||||
"fieldTextChannelStreamFps": "The speed in which frames are displayed to clients, in Frames Per Second. Be aware there is no default. This can lead to high bandwidth usage.",
|
||||
"fieldTextChannelStreamScaleX": "Width of the stream image that is output after processing.",
|
||||
"fieldTextChannelStreamScaleY": "Height of the stream image that is output after processing.",
|
||||
"fieldTextChannelStreamRotate": "Change the viewing angle of the video stream.",
|
||||
"fieldTextChannelSvf": "Place FFMPEG video filters in this box to affect the streaming portion. No spaces."
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
1555
languages/ja.json
1555
languages/ja.json
File diff suppressed because it is too large
Load Diff
2145
languages/ru.json
2145
languages/ru.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -1,7 +1,9 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const moment = require('moment');
|
||||
const request = require('request');
|
||||
const fetch = require('node-fetch');
|
||||
const { AbortController } = require('node-abort-controller')
|
||||
const DigestFetch = require('digest-fetch')
|
||||
module.exports = (processCwd,config) => {
|
||||
const parseJSON = (string) => {
|
||||
var parsed
|
||||
|
@ -79,6 +81,50 @@ module.exports = (processCwd,config) => {
|
|||
if(!e){e=new Date};if(!x){x='YYYY-MM-DDTHH-mm-ss'};
|
||||
return moment(e).format(x);
|
||||
}
|
||||
const fetchTimeout = (url, ms, { signal, ...options } = {}) => {
|
||||
const controller = new AbortController();
|
||||
const promise = fetch(url, { signal: controller.signal, ...options });
|
||||
if (signal) signal.addEventListener("abort", () => controller.abort());
|
||||
const timeout = setTimeout(() => controller.abort(), ms);
|
||||
return promise.finally(() => clearTimeout(timeout));
|
||||
}
|
||||
async function fetchDownloadAndWrite(downloadUrl,outputPath,readFileAfterWrite,options){
|
||||
const writeStream = fs.createWriteStream(outputPath);
|
||||
const downloadBuffer = await fetch(downloadUrl,options).then((res) => res.buffer());
|
||||
writeStream.write(downloadBuffer);
|
||||
writeStream.end();
|
||||
if(readFileAfterWrite === 1){
|
||||
return fs.createReadStream(outputPath)
|
||||
}else if(readFileAfterWrite === 2){
|
||||
return downloadBuffer
|
||||
}
|
||||
return null
|
||||
}
|
||||
function fetchWithAuthentication(requestUrl,options,callback){
|
||||
let hasDigestAuthEnabled = options.digestAuth;
|
||||
let theRequester;
|
||||
const hasUsernameAndPassword = options.username && typeof options.password === 'string'
|
||||
const requestOptions = {
|
||||
method : options.method || 'GET'
|
||||
}
|
||||
if(typeof options.postData === 'object'){
|
||||
const formData = new fetch.FormData()
|
||||
const formKeys = Object.keys(options.postData)
|
||||
formKeys.forEach(function(key){
|
||||
const value = formKeys[key]
|
||||
formData.set(key, value)
|
||||
})
|
||||
requestOptions.body = formData
|
||||
}
|
||||
if(hasUsernameAndPassword && hasDigestAuthEnabled){
|
||||
theRequester = (new DigestFetch(options.username, options.password)).fetch
|
||||
}else if(hasUsernameAndPassword){
|
||||
theRequester = (new DigestFetch(options.username, options.password, { basic: true })).fetch
|
||||
}else{
|
||||
theRequester = fetch
|
||||
}
|
||||
return theRequester(requestUrl,requestOptions)
|
||||
}
|
||||
const checkSubscription = (subscriptionId,callback) => {
|
||||
function subscriptionFailed(){
|
||||
console.error('!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!')
|
||||
|
@ -92,12 +138,12 @@ module.exports = (processCwd,config) => {
|
|||
if(subscriptionId && subscriptionId !== 'sub_XXXXXXXXXXXX' && !config.disableOnlineSubscriptionCheck){
|
||||
var url = 'https://licenses.shinobi.video/subscribe/check?subscriptionId=' + subscriptionId
|
||||
var hasSubcribed = false
|
||||
request(url,{
|
||||
fetchTimeout(url,30000,{
|
||||
method: 'GET',
|
||||
timeout: 30000
|
||||
}, function(err,resp,body){
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(function(body){
|
||||
var json = s.parseJSON(body)
|
||||
if(err)console.log(err,json)
|
||||
hasSubcribed = json && !!json.ok
|
||||
var i;
|
||||
for (i = 0; i < s.onSubscriptionCheckExtensions.length; i++) {
|
||||
|
@ -113,8 +159,12 @@ module.exports = (processCwd,config) => {
|
|||
}else{
|
||||
subscriptionFailed()
|
||||
}
|
||||
}).catch((err) => {
|
||||
if(err)console.log(err)
|
||||
subscriptionFailed()
|
||||
})
|
||||
}else{
|
||||
var i;
|
||||
for (i = 0; i < s.onSubscriptionCheckExtensions.length; i++) {
|
||||
const extender = s.onSubscriptionCheckExtensions[i]
|
||||
hasSubcribed = extender(false,{},subscriptionId)
|
||||
|
@ -125,6 +175,12 @@ module.exports = (processCwd,config) => {
|
|||
callback(hasSubcribed)
|
||||
}
|
||||
}
|
||||
function isEven(value) {
|
||||
if (value%2 == 0)
|
||||
return true;
|
||||
else
|
||||
return false;
|
||||
}
|
||||
return {
|
||||
parseJSON: parseJSON,
|
||||
stringJSON: stringJSON,
|
||||
|
@ -137,5 +193,9 @@ module.exports = (processCwd,config) => {
|
|||
localToUtc: localToUtc,
|
||||
formattedTime: formattedTime,
|
||||
checkSubscription: checkSubscription,
|
||||
isEven: isEven,
|
||||
fetchTimeout: fetchTimeout,
|
||||
fetchDownloadAndWrite: fetchDownloadAndWrite,
|
||||
fetchWithAuthentication: fetchWithAuthentication,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
const WebSocket = require('cws');
|
||||
function createWebSocketServer(options){
|
||||
const theWebSocket = new WebSocket.Server(options ? options : {
|
||||
noServer: true
|
||||
});
|
||||
theWebSocket.broadcast = function(data) {
|
||||
theWebSocket.clients.forEach((client) => {
|
||||
try{
|
||||
client.sendData(data)
|
||||
}catch(err){
|
||||
// console.log(err)
|
||||
}
|
||||
})
|
||||
};
|
||||
return theWebSocket
|
||||
}
|
||||
function createWebSocketClient(connectionHost,options){
|
||||
const clientConnection = new WebSocket(connectionHost, options.engineOptions);
|
||||
if(options.onMessage){
|
||||
const onMessage = options.onMessage;
|
||||
clientConnection.on('message', message => {
|
||||
const data = JSON.parse(message);
|
||||
onMessage(data);
|
||||
});
|
||||
}
|
||||
return clientConnection
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createWebSocketServer,
|
||||
createWebSocketClient,
|
||||
}
|
|
@ -1,15 +1,46 @@
|
|||
module.exports = function(s,config,lang,app,io){
|
||||
if(config.showPoweredByShinobi === undefined){config.showPoweredByShinobi=true}
|
||||
if(config.poweredByShinobi === undefined){config.poweredByShinobi='Powered by Shinobi.Systems'}
|
||||
if(config.poweredByShinobiClass === undefined){config.poweredByShinobiClass='margin:15px 0 0 0;text-align:center;color:#777;font-family: sans-serif;text-transform: uppercase;letter-spacing: 3;font-size: 8pt;'}
|
||||
if(config.webPageTitle === undefined){config.webPageTitle='Shinobi'}
|
||||
if(config.showLoginCardHeader === undefined){config.showLoginCardHeader=true}
|
||||
if(config.webFavicon === undefined){config.webFavicon='libs/img/icon/favicon.ico'}
|
||||
if(config.logoLocation76x76 === undefined){config.logoLocation76x76='libs/img/icon/apple-touch-icon-76x76.png'}
|
||||
if(config.webFavicon === undefined){config.webFavicon = 'libs/img/icon/favicon.ico'}
|
||||
if(!config.logoLocationAppleTouchIcon)config.logoLocationAppleTouchIcon = 'libs/img/icon/apple-touch-icon.png';
|
||||
if(!config.logoLocation57x57)config.logoLocation57x57 = 'libs/img/icon/apple-touch-icon-57x57.png';
|
||||
if(!config.logoLocation72x72)config.logoLocation72x72 = 'libs/img/icon/apple-touch-icon-72x72.png';
|
||||
if(!config.logoLocation76x76)config.logoLocation76x76 = 'libs/img/icon/apple-touch-icon-76x76.png';
|
||||
if(!config.logoLocation114x114)config.logoLocation114x114 = 'libs/img/icon/apple-touch-icon-114x114.png';
|
||||
if(!config.logoLocation120x120)config.logoLocation120x120 = 'libs/img/icon/apple-touch-icon-120x120.png';
|
||||
if(!config.logoLocation144x144)config.logoLocation144x144 = 'libs/img/icon/apple-touch-icon-144x144.png';
|
||||
if(!config.logoLocation152x152)config.logoLocation152x152 = 'libs/img/icon/apple-touch-icon-152x152.png';
|
||||
if(!config.logoLocation196x196)config.logoLocation196x196 = 'libs/img/icon/favicon-196x196.png';
|
||||
if(config.logoLocation76x76Link === undefined){config.logoLocation76x76Link='https://shinobi.video'}
|
||||
if(config.logoLocation76x76Style === undefined){config.logoLocation76x76Style='border-radius:50%'}
|
||||
if(config.loginScreenBackground === undefined){config.loginScreenBackground='https://shinobi.video/libs/assets/backgrounds/7.jpg'}
|
||||
if(config.showLoginSelector === undefined){config.showLoginSelector=true}
|
||||
|
||||
if(config.defaultTheme === undefined)config.defaultTheme = 'Ice-v3';
|
||||
if(config.socialLinks === undefined){
|
||||
config.socialLinks = [
|
||||
{
|
||||
icon: 'home',
|
||||
href: 'https://shinobi.video',
|
||||
title: 'Homepage'
|
||||
},
|
||||
{
|
||||
icon: 'facebook',
|
||||
href: 'https://www.facebook.com/ShinobiCCTV',
|
||||
title: 'Facebook'
|
||||
},
|
||||
{
|
||||
icon: 'twitter',
|
||||
href: 'https://twitter.com/ShinobiCCTV',
|
||||
title: 'Twitter'
|
||||
},
|
||||
{
|
||||
icon: 'youtube',
|
||||
href: 'https://www.youtube.com/channel/UCbgbBLTK-koTyjOmOxA9msQ',
|
||||
title: 'YouTube'
|
||||
}
|
||||
]
|
||||
}
|
||||
s.getConfigWithBranding = function(domain){
|
||||
var configCopy = Object.assign({},config)
|
||||
if(config.brandingConfig && config.brandingConfig[domain]){
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
module.exports = function(jsonData,onConnected,onError,onClose){
|
||||
const config = jsonData.globalInfo.config;
|
||||
const dataPortToken = jsonData.dataPortToken;
|
||||
const CWS = require('cws');
|
||||
const client = new CWS(`ws://localhost:${config.port}/dataPort`);
|
||||
if(onError)client.on('error',onError);
|
||||
if(onClose)client.on('close',onClose);
|
||||
client.on('open',onConnected);
|
||||
return client;
|
||||
}
|
|
@ -7,6 +7,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){
|
|||
const completeMonitorConfig = jsonData.rawMonitorConfig
|
||||
const groupKey = completeMonitorConfig.ke
|
||||
const monitorId = completeMonitorConfig.mid
|
||||
const monitorName = completeMonitorConfig.name
|
||||
const monitorDetails = completeMonitorConfig.details
|
||||
const triggerTimer = {}
|
||||
let regionJson
|
||||
|
@ -70,13 +71,13 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){
|
|||
var sendDetectedData = function(detectorObject){
|
||||
pamDiffResponder(detectorObject)
|
||||
}
|
||||
}else{
|
||||
var sendDetectedData = function(detectorObject){
|
||||
pamDiffResponder.write(Buffer.from(JSON.stringify(detectorObject)))
|
||||
}
|
||||
}else{
|
||||
var sendDetectedData = function(detectorObject){
|
||||
pamDiffResponder.write(Buffer.from(JSON.stringify(detectorObject)))
|
||||
}
|
||||
}
|
||||
function logData(...args){
|
||||
process.logData(JSON.stringify(args))
|
||||
process.logData(args)
|
||||
}
|
||||
function getRegionsWithMinimumChange(data){
|
||||
try{
|
||||
|
@ -256,6 +257,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){
|
|||
if(!region)return false;
|
||||
region.polygon = [];
|
||||
region.points.forEach(function(points){
|
||||
if(!points || isNaN(points[0]) || isNaN(points[1]))return;
|
||||
var x = parseFloat(points[0]);
|
||||
var y = parseFloat(points[1]);
|
||||
if(x < 0)x = 0;
|
||||
|
@ -265,6 +267,7 @@ module.exports = function(jsonData,pamDiffResponder,alternatePamDiff){
|
|||
y: y
|
||||
})
|
||||
})
|
||||
if(region.polygon.length < 4)return logData(`Failed to Create Region : ${monitorName} : ${region.name}`,region.points);
|
||||
if(region.sensitivity===''){
|
||||
region.sensitivity = globalSensitivity
|
||||
}else{
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const fs = require('fs')
|
||||
const request = require('request')
|
||||
const exec = require('child_process').exec
|
||||
const spawn = require('child_process').spawn
|
||||
const isWindows = (process.platform === 'win32' || process.platform === 'win64')
|
||||
|
@ -14,9 +13,34 @@ const stdioPipes = jsonData.pipes || []
|
|||
var newPipes = []
|
||||
var stdioWriters = [];
|
||||
|
||||
var writeToStderr = function(text){
|
||||
const { fetchTimeout } = require('../basic/utils.js')(process.cwd(),config)
|
||||
const dataPort = require('./libs/dataPortConnection.js')(jsonData,
|
||||
// onConnected
|
||||
() => {
|
||||
dataPort.send(jsonData.dataPortToken)
|
||||
},
|
||||
// onError
|
||||
(err) => {
|
||||
writeToStderr([
|
||||
'dataPort:Connection:Error',
|
||||
err
|
||||
])
|
||||
},
|
||||
// onClose
|
||||
(e) => {
|
||||
writeToStderr([
|
||||
'dataPort:Connection:Closed',
|
||||
e
|
||||
])
|
||||
})
|
||||
|
||||
var writeToStderr = function(argsAsArray){
|
||||
try{
|
||||
process.stderr.write(Buffer.from(`${text}`, 'utf8' ))
|
||||
process.stderr.write(Buffer.from(`${JSON.stringify(argsAsArray)}`, 'utf8' ))
|
||||
// dataPort.send({
|
||||
// f: 'debugLog',
|
||||
// data: argsAsArray,
|
||||
// })
|
||||
// stdioWriters[2].write(Buffer.from(`${new Error('writeToStderr').stack}`, 'utf8' ))
|
||||
}catch(err){
|
||||
}
|
||||
|
@ -111,7 +135,9 @@ writeToStderr('Thread Opening')
|
|||
|
||||
if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detector_pam === '1'){
|
||||
try{
|
||||
const attachPamDetector = require(config.monitorDetectorDaemonPath ? config.monitorDetectorDaemonPath : __dirname + '/detector.js')(jsonData,stdioWriters[3])
|
||||
const attachPamDetector = require(config.monitorDetectorDaemonPath ? config.monitorDetectorDaemonPath : __dirname + '/detector.js')(jsonData,(detectorObject) => {
|
||||
dataPort.send(JSON.stringify(detectorObject))
|
||||
})
|
||||
attachPamDetector(cameraProcess)
|
||||
}catch(err){
|
||||
writeToStderr(err.stack)
|
||||
|
@ -119,7 +145,6 @@ if(rawMonitorConfig.details.detector === '1' && rawMonitorConfig.details.detecto
|
|||
}
|
||||
|
||||
if(rawMonitorConfig.type === 'jpeg'){
|
||||
var recordingSnapRequest
|
||||
var recordingSnapper
|
||||
var errorTimeout
|
||||
var errorCount = 0
|
||||
|
@ -137,16 +162,11 @@ if(rawMonitorConfig.type === 'jpeg'){
|
|||
setTimeout(() => {
|
||||
if(!cameraProcess.stdio[0])return writeToStderr('No Camera Process Found for Snapper');
|
||||
const captureOne = function(f){
|
||||
recordingSnapRequest = request({
|
||||
url: buildMonitorUrl(rawMonitorConfig),
|
||||
fetchTimeout(buildMonitorUrl(rawMonitorConfig),15000,{
|
||||
method: 'GET',
|
||||
encoding: null,
|
||||
timeout: 15000
|
||||
},function(err,data){
|
||||
if(err){
|
||||
writeToStderr(JSON.stringify(err))
|
||||
return;
|
||||
}
|
||||
})
|
||||
.then(response => response.text())
|
||||
.then(function(body){
|
||||
// writeToStderr(data.body.length)
|
||||
cameraProcess.stdio[0].write(data.body)
|
||||
recordingSnapper = setTimeout(function(){
|
||||
|
@ -159,7 +179,7 @@ if(rawMonitorConfig.type === 'jpeg'){
|
|||
delete(errorTimeout)
|
||||
},3000)
|
||||
}
|
||||
}).on('error', function(err){
|
||||
}).catch(function(err){
|
||||
++errorCount
|
||||
clearTimeout(errorTimeout)
|
||||
errorTimeout = null
|
||||
|
|
|
@ -1,215 +1,147 @@
|
|||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var express = require('express');
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const express = require('express');
|
||||
const { createWebSocketServer, createWebSocketClient } = require('./basic/websocketTools.js')
|
||||
module.exports = function(s,config,lang,app,io){
|
||||
const { cameraDestroy } = require('./monitor/utils.js')(s,config,lang)
|
||||
//setup Master for childNodes
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){
|
||||
if(
|
||||
config.childNodes.enabled === true &&
|
||||
config.childNodes.mode === 'master'
|
||||
){
|
||||
const {
|
||||
getIpAddress,
|
||||
initiateDataConnection,
|
||||
initiateVideoTransferConnection,
|
||||
onWebSocketDataFromChildNode,
|
||||
onDataConnectionDisconnect,
|
||||
initiateVideoWriteFromChildNode,
|
||||
initiateTimelapseFrameWriteFromChildNode,
|
||||
} = require('./childNode/utils.js')(s,config,lang,app,io)
|
||||
s.childNodes = {};
|
||||
var childNodeHTTP = express();
|
||||
var childNodeServer = http.createServer(app);
|
||||
var childNodeWebsocket = new (require('socket.io'))()
|
||||
childNodeServer.listen(config.childNodes.port,config.bindip,function(){
|
||||
console.log(lang.Shinobi+' - CHILD NODE PORT : '+config.childNodes.port);
|
||||
});
|
||||
s.debugLog('childNodeWebsocket.attach(childNodeServer)')
|
||||
childNodeWebsocket.attach(childNodeServer,{
|
||||
path:'/socket.io',
|
||||
transports: ['websocket']
|
||||
});
|
||||
//send data to child node function (experimental)
|
||||
s.cx = function(z,y,x){
|
||||
if(!z.mid && !z.d){
|
||||
console.error('Missing ID')
|
||||
}else if(x){
|
||||
x.broadcast.to(y).emit('c',z)
|
||||
}else{
|
||||
childNodeWebsocket.to(y).emit('c',z)
|
||||
const childNodesConnectionIndex = {};
|
||||
const childNodeHTTP = express();
|
||||
const childNodeServer = http.createServer(app);
|
||||
const childNodeWebsocket = createWebSocketServer();
|
||||
const childNodeFileRelay = createWebSocketServer();
|
||||
childNodeServer.on('upgrade', function upgrade(request, socket, head) {
|
||||
const pathname = url.parse(request.url).pathname;
|
||||
if (pathname === '/childNode') {
|
||||
childNodeWebsocket.handleUpgrade(request, socket, head, function done(ws) {
|
||||
childNodeWebsocket.emit('connection', ws, request)
|
||||
})
|
||||
} else if (pathname === '/childNodeFileRelay') {
|
||||
childNodeFileRelay.handleUpgrade(request, socket, head, function done(ws) {
|
||||
childNodeFileRelay.emit('connection', ws, request)
|
||||
})
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
const childNodeBindIP = config.childNodes.ip || config.bindip;
|
||||
childNodeServer.listen(config.childNodes.port,childNodeBindIP,function(){
|
||||
console.log(lang.Shinobi+' - CHILD NODE SERVER : ' + config.childNodes.port);
|
||||
});
|
||||
//send data to child node function
|
||||
s.cx = function(data,connectionId){
|
||||
childNodesConnectionIndex[connectionId].sendJson(data)
|
||||
}
|
||||
//child Node Websocket
|
||||
childNodeWebsocket.on('connection', function (cn) {
|
||||
childNodeWebsocket.on('connection', function (client, req) {
|
||||
//functions for dispersing work to child servers;
|
||||
var ipAddress
|
||||
cn.on('c',function(d){
|
||||
if(config.childNodes.key.indexOf(d.socketKey) > -1){
|
||||
if(!cn.shinobi_child&&d.f=='init'){
|
||||
ipAddress = cn.request.connection.remoteAddress.replace('::ffff:','')+':'+d.port
|
||||
cn.ip = ipAddress
|
||||
cn.shinobi_child = 1
|
||||
cn.tx = function(z){
|
||||
cn.emit('c',z)
|
||||
}
|
||||
if(!s.childNodes[cn.ip]){
|
||||
s.childNodes[cn.ip] = {}
|
||||
};
|
||||
s.childNodes[cn.ip].dead = false
|
||||
s.childNodes[cn.ip].cnid = cn.id
|
||||
s.childNodes[cn.ip].cpu = 0
|
||||
s.childNodes[cn.ip].ip = ipAddress
|
||||
s.childNodes[cn.ip].activeCameras = {}
|
||||
d.availableHWAccels.forEach(function(accel){
|
||||
if(config.availableHWAccels.indexOf(accel) === -1)config.availableHWAccels.push(accel)
|
||||
})
|
||||
cn.tx({
|
||||
f : 'init_success',
|
||||
childNodes : s.childNodes
|
||||
})
|
||||
s.childNodes[cn.ip].coreCount = d.coreCount
|
||||
}else{
|
||||
switch(d.f){
|
||||
case'cpu':
|
||||
s.childNodes[ipAddress].cpu = d.cpu;
|
||||
break;
|
||||
case'sql':
|
||||
s.sqlQuery(d.query,d.values,function(err,rows){
|
||||
cn.emit('c',{f:'sqlCallback',rows:rows,err:err,callbackId:d.callbackId});
|
||||
});
|
||||
break;
|
||||
case'knex':
|
||||
s.knexQuery(d.options,function(err,rows){
|
||||
cn.emit('c',{f:'sqlCallback',rows:rows,err:err,callbackId:d.callbackId});
|
||||
});
|
||||
break;
|
||||
case'clearCameraFromActiveList':
|
||||
if(s.childNodes[ipAddress])delete(s.childNodes[ipAddress].activeCameras[d.ke + d.id])
|
||||
break;
|
||||
case'camera':
|
||||
s.camera(d.mode,d.data)
|
||||
break;
|
||||
case's.tx':
|
||||
s.tx(d.data,d.to)
|
||||
break;
|
||||
case's.userLog':
|
||||
if(!d.mon || !d.data)return console.log('LOG DROPPED',d.mon,d.data);
|
||||
s.userLog(d.mon,d.data)
|
||||
break;
|
||||
case'open_timelapse_file_transfer':
|
||||
var location = s.getTimelapseFrameDirectory(d.d) + `${d.currentDate}/`
|
||||
if(!fs.existsSync(location)){
|
||||
fs.mkdirSync(location)
|
||||
}
|
||||
break;
|
||||
case'created_timelapse_file_chunk':
|
||||
if(!s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename]){
|
||||
var dir = s.getTimelapseFrameDirectory(d.d) + `${d.currentDate}/`
|
||||
s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename] = fs.createWriteStream(dir+d.filename)
|
||||
}
|
||||
s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename].write(d.chunk)
|
||||
break;
|
||||
case'created_timelapse_file':
|
||||
if(!s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename]){
|
||||
return console.log('FILE NOT EXIST')
|
||||
}
|
||||
s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename].end()
|
||||
cn.tx({
|
||||
f: 'deleteTimelapseFrame',
|
||||
file: d.filename,
|
||||
currentDate: d.currentDate,
|
||||
d: d.d, //monitor config
|
||||
ke: d.ke,
|
||||
mid: d.mid
|
||||
})
|
||||
s.insertTimelapseFrameDatabaseRow({
|
||||
ke: d.ke
|
||||
},d.queryInfo)
|
||||
break;
|
||||
case'created_file_chunk':
|
||||
if(!s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename]){
|
||||
d.dir = s.getVideoDirectory(s.group[d.ke].rawMonitorConfigurations[d.mid])
|
||||
if (!fs.existsSync(d.dir)) {
|
||||
fs.mkdirSync(d.dir, {recursive: true}, (err) => {s.debugLog(err)})
|
||||
}
|
||||
s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename] = fs.createWriteStream(d.dir+d.filename)
|
||||
}
|
||||
s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename].write(d.chunk)
|
||||
break;
|
||||
case'created_file':
|
||||
if(!s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename]){
|
||||
return console.log('FILE NOT EXIST')
|
||||
}
|
||||
s.group[d.ke].activeMonitors[d.mid].childNodeStreamWriters[d.filename].end();
|
||||
cn.tx({
|
||||
f:'delete',
|
||||
file:d.filename,
|
||||
ke:d.ke,
|
||||
mid:d.mid
|
||||
})
|
||||
s.txWithSubPermissions({
|
||||
f:'video_build_success',
|
||||
hrefNoAuth:'/videos/'+d.ke+'/'+d.mid+'/'+d.filename,
|
||||
filename:d.filename,
|
||||
mid:d.mid,
|
||||
ke:d.ke,
|
||||
time:d.time,
|
||||
size:d.filesize,
|
||||
end:d.end
|
||||
},'GRP_'+d.ke,'video_view')
|
||||
//save database row
|
||||
var insert = {
|
||||
startTime : d.time,
|
||||
filesize : d.filesize,
|
||||
endTime : d.end,
|
||||
dir : s.getVideoDirectory(d.d),
|
||||
file : d.filename,
|
||||
filename : d.filename,
|
||||
filesizeMB : parseFloat((d.filesize/1048576).toFixed(2))
|
||||
}
|
||||
s.insertDatabaseRow(d.d,insert)
|
||||
s.insertCompletedVideoExtensions.forEach(function(extender){
|
||||
extender(d.d,insert)
|
||||
})
|
||||
//purge over max
|
||||
s.purgeDiskForGroup(d.ke)
|
||||
//send new diskUsage values
|
||||
s.setDiskUsedForGroup(d.ke,insert.filesizeMB)
|
||||
clearTimeout(s.group[d.ke].activeMonitors[d.mid].recordingChecker)
|
||||
clearTimeout(s.group[d.ke].activeMonitors[d.mid].streamChecker)
|
||||
break;
|
||||
}
|
||||
}
|
||||
const ipAddress = getIpAddress(req)
|
||||
const connectionId = s.gid(10);
|
||||
s.debugLog('Child Node Connection!',new Date(),ipAddress)
|
||||
client.id = connectionId;
|
||||
function onAuthenticate(d){
|
||||
const data = JSON.parse(d);
|
||||
const childNodeKeyAccepted = config.childNodes.key.indexOf(data.socketKey) > -1;
|
||||
if(!client.shinobiChildAlreadyRegistered && data.f === 'init' && childNodeKeyAccepted){
|
||||
initiateDataConnection(client,req,data,connectionId);
|
||||
childNodesConnectionIndex[connectionId] = client;
|
||||
client.removeListener('message',onAuthenticate)
|
||||
client.on('message',(d) => {
|
||||
const data = JSON.parse(d);
|
||||
onWebSocketDataFromChildNode(client,data)
|
||||
})
|
||||
}else{
|
||||
s.debugLog('Child Node Force Disconnected!',new Date(),ipAddress)
|
||||
client.destroy()
|
||||
}
|
||||
}
|
||||
client.on('message',onAuthenticate)
|
||||
client.on('close',() => {
|
||||
onDataConnectionDisconnect(client, req)
|
||||
})
|
||||
cn.on('disconnect',function(){
|
||||
console.log('childNodeWebsocket.disconnect',ipAddress)
|
||||
if(s.childNodes[ipAddress]){
|
||||
var monitors = Object.values(s.childNodes[ipAddress].activeCameras)
|
||||
if(monitors && monitors[0]){
|
||||
var loadCompleted = 0
|
||||
var loadMonitor = function(monitor){
|
||||
setTimeout(function(){
|
||||
var mode = monitor.mode + ''
|
||||
var cleanMonitor = s.cleanMonitorObject(monitor)
|
||||
s.camera('stop',Object.assign(cleanMonitor,{}))
|
||||
delete(s.group[monitor.ke].activeMonitors[monitor.mid].childNode)
|
||||
delete(s.group[monitor.ke].activeMonitors[monitor.mid].childNodeId)
|
||||
setTimeout(function(){
|
||||
s.camera(mode,cleanMonitor)
|
||||
++loadCompleted
|
||||
if(monitors[loadCompleted]){
|
||||
loadMonitor(monitors[loadCompleted])
|
||||
}
|
||||
},1000)
|
||||
},2000)
|
||||
}
|
||||
loadMonitor(monitors[loadCompleted])
|
||||
})
|
||||
childNodeFileRelay.on('connection', function (client, req) {
|
||||
function onAuthenticate(d){
|
||||
const data = JSON.parse(d);
|
||||
const childNodeKeyAccepted = config.childNodes.key.indexOf(data.socketKey) > -1;
|
||||
if(!client.alreadyInitiated && data.fileType && childNodeKeyAccepted){
|
||||
client.alreadyInitiated = true;
|
||||
client.removeListener('message',onAuthenticate)
|
||||
switch(data.fileType){
|
||||
case'video':
|
||||
initiateVideoWriteFromChildNode(client,data.options,data.connectionId)
|
||||
break;
|
||||
case'timelapseFrame':
|
||||
initiateTimelapseFrameWriteFromChildNode(client,data.options,data.connectionId)
|
||||
break;
|
||||
}
|
||||
s.childNodes[ipAddress].activeCameras = {}
|
||||
s.childNodes[ipAddress].dead = true
|
||||
}else{
|
||||
client.destroy()
|
||||
}
|
||||
})
|
||||
}
|
||||
client.on('message',onAuthenticate)
|
||||
})
|
||||
}else
|
||||
//setup Child for childNodes
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){
|
||||
s.connected = false;
|
||||
childIO = require('socket.io-client')('ws://'+config.childNodes.host,{
|
||||
transports: ['websocket']
|
||||
});
|
||||
s.cx = function(x){x.socketKey = config.childNodes.key;childIO.emit('c',x)}
|
||||
s.tx = function(x,y){s.cx({f:'s.tx',data:x,to:y})}
|
||||
s.userLog = function(x,y){s.cx({f:'s.userLog',mon:x,data:y})}
|
||||
if(
|
||||
config.childNodes.enabled === true &&
|
||||
config.childNodes.mode === 'child' &&
|
||||
config.childNodes.host
|
||||
){
|
||||
const {
|
||||
initiateConnectionToMasterNode,
|
||||
onDisconnectFromMasterNode,
|
||||
onDataFromMasterNode,
|
||||
} = require('./childNode/childUtils.js')(s,config,lang,app,io)
|
||||
s.connectedToMasterNode = false;
|
||||
let childIO;
|
||||
function createChildNodeConnection(){
|
||||
childIO = createWebSocketClient('ws://'+config.childNodes.host + '/childNode',{
|
||||
onMessage: onDataFromMasterNode
|
||||
})
|
||||
childIO.on('open', function(){
|
||||
console.error(new Date(),'Child Nodes : Connected to Master Node! Authenticating...');
|
||||
initiateConnectionToMasterNode()
|
||||
})
|
||||
childIO.on('close',function(){
|
||||
onDisconnectFromMasterNode()
|
||||
setTimeout(() => {
|
||||
console.error(new Date(),'Child Nodes : Connection to Master Node Closed. Attempting Reconnect...');
|
||||
createChildNodeConnection()
|
||||
},3000)
|
||||
})
|
||||
childIO.on('error',function(err){
|
||||
console.error(new Date(),'Child Nodes ERROR : ', err.message);
|
||||
childIO.close()
|
||||
})
|
||||
}
|
||||
createChildNodeConnection()
|
||||
function sendDataToMasterNode(data){
|
||||
childIO.send(JSON.stringify(data))
|
||||
}
|
||||
s.cx = sendDataToMasterNode;
|
||||
// replace internal functions with bridges to master node
|
||||
s.tx = function(x,y){
|
||||
sendDataToMasterNode({f:'s.tx',data:x,to:y})
|
||||
}
|
||||
s.userLog = function(x,y){
|
||||
sendDataToMasterNode({f:'s.userLog',mon:x,data:y})
|
||||
}
|
||||
s.queuedSqlCallbacks = {}
|
||||
s.sqlQuery = function(query,values,onMoveOn){
|
||||
var callbackId = s.gid()
|
||||
|
@ -218,84 +150,13 @@ module.exports = function(s,config,lang,app,io){
|
|||
var onMoveOn = values;
|
||||
var values = [];
|
||||
}
|
||||
if(typeof onMoveOn !== 'function'){onMoveOn=function(){}}
|
||||
s.queuedSqlCallbacks[callbackId] = onMoveOn
|
||||
s.cx({f:'sql',query:query,values:values,callbackId:callbackId});
|
||||
if(typeof onMoveOn === 'function')s.queuedSqlCallbacks[callbackId] = onMoveOn;
|
||||
sendDataToMasterNode({f:'sql',query:query,values:values,callbackId:callbackId});
|
||||
}
|
||||
s.knexQuery = function(options,onMoveOn){
|
||||
var callbackId = s.gid()
|
||||
if(typeof onMoveOn !== 'function'){onMoveOn=function(){}}
|
||||
s.queuedSqlCallbacks[callbackId] = onMoveOn
|
||||
s.cx({f:'knex',options:options,callbackId:callbackId});
|
||||
if(typeof onMoveOn === 'function')s.queuedSqlCallbacks[callbackId] = onMoveOn;
|
||||
sendDataToMasterNode({f:'knex',options:options,callbackId:callbackId});
|
||||
}
|
||||
setInterval(async () => {
|
||||
const cpu = await s.cpuUsage()
|
||||
s.cx({
|
||||
f: 'cpu',
|
||||
cpu: parseFloat(cpu)
|
||||
})
|
||||
},5000)
|
||||
childIO.on('connect', function(d){
|
||||
console.log('CHILD CONNECTION SUCCESS')
|
||||
s.cx({
|
||||
f : 'init',
|
||||
port : config.port,
|
||||
coreCount : s.coreCount,
|
||||
availableHWAccels : config.availableHWAccels
|
||||
})
|
||||
})
|
||||
childIO.on('c', function (d) {
|
||||
switch(d.f){
|
||||
case'sqlCallback':
|
||||
if(s.queuedSqlCallbacks[d.callbackId]){
|
||||
s.queuedSqlCallbacks[d.callbackId](d.err,d.rows)
|
||||
delete(s.queuedSqlCallbacks[d.callbackId])
|
||||
}
|
||||
break;
|
||||
case'init_success':
|
||||
s.connected=true;
|
||||
s.other_helpers=d.child_helpers;
|
||||
break;
|
||||
case'kill':
|
||||
s.initiateMonitorObject(d.d);
|
||||
cameraDestroy(d.d)
|
||||
var childNodeIp = s.group[d.d.ke].activeMonitors[d.d.id]
|
||||
break;
|
||||
case'sync':
|
||||
s.initiateMonitorObject(d.sync);
|
||||
Object.keys(d.sync).forEach(function(v){
|
||||
s.group[d.sync.ke].activeMonitors[d.sync.mid][v]=d.sync[v];
|
||||
});
|
||||
break;
|
||||
case'delete'://delete video
|
||||
s.file('delete',s.dir.videos+d.ke+'/'+d.mid+'/'+d.file)
|
||||
break;
|
||||
case'deleteTimelapseFrame'://delete video
|
||||
var filePath = s.getTimelapseFrameDirectory(d.d) + `${d.currentDate}/` + d.file
|
||||
s.file('delete',filePath)
|
||||
break;
|
||||
case'insertCompleted'://close video
|
||||
s.insertCompletedVideo(d.d,d.k)
|
||||
break;
|
||||
case'cameraStop'://start camera
|
||||
s.camera('stop',d.d)
|
||||
break;
|
||||
case'cameraStart'://start or record camera
|
||||
s.camera(d.mode,d.d)
|
||||
break;
|
||||
}
|
||||
})
|
||||
childIO.on('disconnect',function(d){
|
||||
s.connected = false;
|
||||
var groupKeys = Object.keys(s.group)
|
||||
groupKeys.forEach(function(groupKey){
|
||||
var activeMonitorKeys = Object.keys(s.group[groupKey].activeMonitors)
|
||||
activeMonitorKeys.forEach(function(monitorKey){
|
||||
var activeMonitor = s.group[groupKey].activeMonitors[monitorKey]
|
||||
if(activeMonitor && activeMonitor.spawn && activeMonitor.spawn.close)activeMonitor.spawn.close()
|
||||
if(activeMonitor && activeMonitor.spawn && activeMonitor.spawn.kill)activeMonitor.spawn.kill()
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
const fs = require('fs');
|
||||
const { createWebSocketClient } = require('../basic/websocketTools.js')
|
||||
module.exports = function(s,config,lang,app,io){
|
||||
const { cameraDestroy } = require('../monitor/utils.js')(s,config,lang)
|
||||
var checkHwInterval = null;
|
||||
function onDataFromMasterNode(d) {
|
||||
switch(d.f){
|
||||
case'sqlCallback':
|
||||
const callbackId = d.callbackId;
|
||||
if(s.queuedSqlCallbacks[callbackId]){
|
||||
s.queuedSqlCallbacks[callbackId](d.err,d.rows)
|
||||
delete(s.queuedSqlCallbacks[callbackId])
|
||||
}
|
||||
break;
|
||||
case'init_success':
|
||||
console.error(new Date(),'Child Nodes : Authenticated with Master Node!');
|
||||
s.connectedToMasterNode = true;
|
||||
s.other_helpers = d.child_helpers;
|
||||
s.childNodeIdOnMasterNode = d.connectionId
|
||||
break;
|
||||
case'kill':
|
||||
s.initiateMonitorObject(d.d);
|
||||
cameraDestroy(d.d)
|
||||
break;
|
||||
case'sync':
|
||||
s.initiateMonitorObject(d.sync);
|
||||
Object.keys(d.sync).forEach(function(v){
|
||||
s.group[d.sync.ke].activeMonitors[d.sync.mid][v]=d.sync[v];
|
||||
});
|
||||
break;
|
||||
case'delete'://delete video
|
||||
s.file('delete',s.dir.videos+d.ke+'/'+d.mid+'/'+d.file)
|
||||
break;
|
||||
case'deleteTimelapseFrame'://delete timelapse frame
|
||||
var filePath = s.getTimelapseFrameDirectory(d) + `${d.currentDate}/` + d.file
|
||||
s.file('delete',filePath)
|
||||
break;
|
||||
case'cameraStop'://stop camera
|
||||
// s.group[d.d.ke].activeMonitors[d.d.mid].masterSaysToStop = true
|
||||
s.camera('stop',d.d)
|
||||
break;
|
||||
case'cameraStart'://start or record camera
|
||||
s.camera(d.mode,d.d)
|
||||
let activeMonitor = s.group[d.d.ke].activeMonitors[d.d.mid]
|
||||
// activeMonitor.masterSaysToStop = false
|
||||
clearTimeout(activeMonitor.recordingChecker);
|
||||
clearTimeout(activeMonitor.streamChecker);
|
||||
break;
|
||||
}
|
||||
}
|
||||
function initiateConnectionToMasterNode(){
|
||||
s.cx({
|
||||
f: 'init',
|
||||
port: config.port,
|
||||
platform: s.platform,
|
||||
coreCount: s.coreCount,
|
||||
totalmem: s.totalmem / 1048576,
|
||||
availableHWAccels: config.availableHWAccels,
|
||||
socketKey: config.childNodes.key
|
||||
})
|
||||
clearInterval(checkHwInterval)
|
||||
checkHwInterval = setInterval(() => {
|
||||
sendCurrentCpuUsage()
|
||||
sendCurrentRamUsage()
|
||||
},5000)
|
||||
}
|
||||
function onDisconnectFromMasterNode(){
|
||||
s.connectedToMasterNode = false;
|
||||
destroyAllMonitorProcesses()
|
||||
clearInterval(checkHwInterval)
|
||||
}
|
||||
function destroyAllMonitorProcesses(){
|
||||
var groupKeys = Object.keys(s.group)
|
||||
groupKeys.forEach(function(groupKey){
|
||||
var activeMonitorKeys = Object.keys(s.group[groupKey].activeMonitors)
|
||||
activeMonitorKeys.forEach(function(monitorKey){
|
||||
var activeMonitor = s.group[groupKey].activeMonitors[monitorKey]
|
||||
if(activeMonitor && activeMonitor.spawn && activeMonitor.spawn.close)activeMonitor.spawn.close()
|
||||
if(activeMonitor && activeMonitor.spawn && activeMonitor.spawn.kill)activeMonitor.spawn.kill()
|
||||
})
|
||||
})
|
||||
}
|
||||
async function sendCurrentCpuUsage(){
|
||||
const percent = await s.cpuUsage();
|
||||
const use = s.coreCount * (percent / 100)
|
||||
s.cx({
|
||||
f: 'cpu',
|
||||
used: use,
|
||||
percent: percent
|
||||
})
|
||||
}
|
||||
async function sendCurrentRamUsage(){
|
||||
const ram = await s.ramUsage()
|
||||
s.cx({
|
||||
f: 'ram',
|
||||
used: ram.used,
|
||||
percent: ram.percent,
|
||||
})
|
||||
}
|
||||
function createFileTransferToMasterNode(filePath,transferInfo,fileType){
|
||||
const response = {ok: true}
|
||||
return new Promise((resolve,reject) => {
|
||||
const fileTransferConnection = createWebSocketClient('ws://'+config.childNodes.host + '/childNodeFileRelay',{
|
||||
onMessage: () => {}
|
||||
})
|
||||
fileTransferConnection.on('open', function(){
|
||||
fileTransferConnection.send(JSON.stringify({
|
||||
fileType: fileType || 'video',
|
||||
options: transferInfo,
|
||||
socketKey: config.childNodes.key,
|
||||
connectionId: s.childNodeIdOnMasterNode,
|
||||
}))
|
||||
setTimeout(() => {
|
||||
fs.createReadStream(filePath)
|
||||
.on('data',function(data){
|
||||
fileTransferConnection.send(data)
|
||||
})
|
||||
.on('close',function(){
|
||||
fileTransferConnection.close()
|
||||
resolve(response)
|
||||
})
|
||||
},2000)
|
||||
})
|
||||
})
|
||||
}
|
||||
async function sendVideoToMasterNode(filePath,options){
|
||||
const groupKey = options.ke
|
||||
const monitorId = options.mid
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
const response = await createFileTransferToMasterNode(filePath,options,'video');
|
||||
clearTimeout(activeMonitor.recordingChecker);
|
||||
clearTimeout(activeMonitor.streamChecker);
|
||||
return response;
|
||||
}
|
||||
async function sendTimelapseFrameToMasterNode(filePath,options){
|
||||
const response = await createFileTransferToMasterNode(filePath,options,'timelapseFrame');
|
||||
return response;
|
||||
}
|
||||
return {
|
||||
onDataFromMasterNode,
|
||||
initiateConnectionToMasterNode,
|
||||
onDisconnectFromMasterNode,
|
||||
destroyAllMonitorProcesses,
|
||||
sendCurrentCpuUsage,
|
||||
sendCurrentRamUsage,
|
||||
sendVideoToMasterNode,
|
||||
sendTimelapseFrameToMasterNode,
|
||||
}
|
||||
}
|
|
@ -0,0 +1,358 @@
|
|||
const fs = require('fs');
|
||||
module.exports = function(s,config,lang,app,io){
|
||||
const masterDoWorkToo = config.childNodes.masterDoWorkToo;
|
||||
const maxCpuPercent = config.childNodes.maxCpuPercent || 75;
|
||||
const maxRamPercent = config.childNodes.maxRamPercent || 75;
|
||||
function getIpAddress(req){
|
||||
return (req.headers['cf-connecting-ip'] ||
|
||||
req.headers["CF-Connecting-IP"] ||
|
||||
req.headers["'x-forwarded-for"] ||
|
||||
req.connection.remoteAddress).replace('::ffff:','');
|
||||
}
|
||||
function initiateDataConnection(client,req,options,connectionId){
|
||||
const ipAddress = getIpAddress(req)
|
||||
const webAddress = ipAddress + ':' + options.port
|
||||
client.ip = webAddress;
|
||||
client.shinobiChildAlreadyRegistered = true;
|
||||
client.sendJson = (data) => {
|
||||
client.send(JSON.stringify(data))
|
||||
}
|
||||
if(!s.childNodes[webAddress]){
|
||||
s.childNodes[webAddress] = {}
|
||||
};
|
||||
const activeNode = s.childNodes[webAddress];
|
||||
activeNode.dead = false
|
||||
activeNode.cnid = client.id
|
||||
activeNode.cpu = 0
|
||||
activeNode.ip = webAddress
|
||||
activeNode.activeCameras = {}
|
||||
activeNode.platform = options.platform
|
||||
activeNode.coreCount = options.coreCount
|
||||
activeNode.totalmem = options.totalmem
|
||||
options.availableHWAccels.forEach(function(accel){
|
||||
if(config.availableHWAccels.indexOf(accel) === -1)config.availableHWAccels.push(accel)
|
||||
})
|
||||
client.sendJson({
|
||||
f : 'init_success',
|
||||
childNodes : s.childNodes,
|
||||
connectionId: connectionId,
|
||||
})
|
||||
s.debugLog('Authenticated Child Node!',new Date(),webAddress)
|
||||
return webAddress
|
||||
}
|
||||
function onWebSocketDataFromChildNode(client,data){
|
||||
const activeMonitor = data.ke && data.mid && s.group[data.ke] ? s.group[data.ke].activeMonitors[data.mid] : null;
|
||||
const webAddress = client.ip;
|
||||
switch(data.f){
|
||||
case'cpu':
|
||||
s.childNodes[webAddress].cpuUsed = data.used;
|
||||
s.childNodes[webAddress].cpuPercent = data.percent;
|
||||
break;
|
||||
case'ram':
|
||||
s.childNodes[webAddress].ramUsed = data.used;
|
||||
s.childNodes[webAddress].ramPercent = data.percent;
|
||||
break;
|
||||
case'sql':
|
||||
s.sqlQuery(data.query,data.values,function(err,rows){
|
||||
client.sendJson({f:'sqlCallback',rows:rows,err:err,callbackId:data.callbackId});
|
||||
});
|
||||
break;
|
||||
case'knex':
|
||||
s.knexQuery(data.options,function(err,rows){
|
||||
client.sendJson({f:'sqlCallback',rows:rows,err:err,callbackId:data.callbackId});
|
||||
});
|
||||
break;
|
||||
case'clearCameraFromActiveList':
|
||||
if(s.childNodes[webAddress])delete(s.childNodes[webAddress].activeCameras[data.ke + data.id])
|
||||
break;
|
||||
case'camera':
|
||||
s.camera(data.mode,data.data)
|
||||
break;
|
||||
case's.tx':
|
||||
s.tx(data.data,data.to)
|
||||
break;
|
||||
case's.userLog':
|
||||
if(!data.mon || !data.data)return console.log('LOG DROPPED',data.mon,data.data);
|
||||
s.userLog(data.mon,data.data)
|
||||
break;
|
||||
}
|
||||
}
|
||||
function onDataConnectionDisconnect(client, req){
|
||||
const webAddress = client.ip;
|
||||
console.log('childNodeWebsocket.disconnect',webAddress)
|
||||
if(s.childNodes[webAddress]){
|
||||
var monitors = Object.values(s.childNodes[webAddress].activeCameras)
|
||||
if(monitors && monitors[0]){
|
||||
var loadCompleted = 0
|
||||
var loadMonitor = function(monitor){
|
||||
setTimeout(function(){
|
||||
var mode = monitor.mode + ''
|
||||
var cleanMonitor = s.cleanMonitorObject(monitor)
|
||||
s.camera('stop',Object.assign(cleanMonitor,{}))
|
||||
delete(s.group[monitor.ke].activeMonitors[monitor.mid].childNode)
|
||||
delete(s.group[monitor.ke].activeMonitors[monitor.mid].childNodeId)
|
||||
setTimeout(function(){
|
||||
s.camera(mode,cleanMonitor)
|
||||
++loadCompleted
|
||||
if(monitors[loadCompleted]){
|
||||
loadMonitor(monitors[loadCompleted])
|
||||
}
|
||||
},1000)
|
||||
},2000)
|
||||
}
|
||||
loadMonitor(monitors[loadCompleted])
|
||||
}
|
||||
s.childNodes[webAddress].activeCameras = {}
|
||||
s.childNodes[webAddress].dead = true
|
||||
}
|
||||
}
|
||||
function initiateFileWriteFromChildNode(client,data,connectionId,onFinish){
|
||||
const response = {ok: true}
|
||||
const groupKey = data.ke
|
||||
const monitorId = data.mid
|
||||
const filename = data.filename
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
const writeDirectory = data.writeDirectory
|
||||
const fileWritePath = writeDirectory + filename
|
||||
const writeStream = fs.createWriteStream(fileWritePath)
|
||||
if (!fs.existsSync(writeDirectory)) {
|
||||
fs.mkdirSync(writeDirectory, {recursive: true}, (err) => {s.debugLog(err)})
|
||||
}
|
||||
activeMonitor.childNodeStreamWriters[filename] = writeStream
|
||||
client.on('message',(d) => {
|
||||
writeStream.write(d)
|
||||
})
|
||||
client.on('close',(d) => {
|
||||
setTimeout(() => {
|
||||
// response.fileWritePath = fileWritePath
|
||||
// response.writeData = data
|
||||
// response.childNodeId = connectionId
|
||||
try{
|
||||
activeMonitor.childNodeStreamWriters[filename].end();
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
setTimeout(() => {
|
||||
delete(activeMonitor.childNodeStreamWriters[filename])
|
||||
},100)
|
||||
onFinish(response)
|
||||
},2000)
|
||||
})
|
||||
}
|
||||
function initiateVideoWriteFromChildNode(client,data,connectionId){
|
||||
return new Promise((resolve,reject) => {
|
||||
const groupKey = data.ke
|
||||
const monitorId = data.mid
|
||||
const filename = data.filename
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
|
||||
const videoDirectory = s.getVideoDirectory(monitorConfig)
|
||||
data.writeDirectory = videoDirectory
|
||||
initiateFileWriteFromChildNode(client,data,connectionId,(response) => {
|
||||
//delete video file from child node
|
||||
s.cx({
|
||||
f: 'delete',
|
||||
file: filename,
|
||||
ke: data.ke,
|
||||
mid: data.mid
|
||||
},connectionId)
|
||||
//
|
||||
s.txWithSubPermissions({
|
||||
f:'video_build_success',
|
||||
hrefNoAuth:'/videos/'+data.ke+'/'+data.mid+'/'+filename,
|
||||
filename:filename,
|
||||
mid:data.mid,
|
||||
ke:data.ke,
|
||||
time:data.time,
|
||||
size:data.filesize,
|
||||
end:data.end
|
||||
},'GRP_'+data.ke,'video_view')
|
||||
//save database row
|
||||
var insert = {
|
||||
startTime : data.time,
|
||||
filesize : data.filesize,
|
||||
endTime : data.end,
|
||||
dir : videoDirectory,
|
||||
file : filename,
|
||||
filename : filename,
|
||||
filesizeMB : parseFloat((data.filesize/1048576).toFixed(2))
|
||||
}
|
||||
s.insertDatabaseRow(monitorConfig,insert)
|
||||
s.insertCompletedVideoExtensions.forEach(function(extender){
|
||||
extender(monitorConfig,insert)
|
||||
})
|
||||
//purge over max
|
||||
s.purgeDiskForGroup(data.ke)
|
||||
//send new diskUsage values
|
||||
s.setDiskUsedForGroup(data.ke,insert.filesizeMB)
|
||||
clearTimeout(activeMonitor.recordingChecker)
|
||||
clearTimeout(activeMonitor.streamChecker)
|
||||
resolve(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
function initiateTimelapseFrameWriteFromChildNode(client,data,connectionId){
|
||||
return new Promise((resolve,reject) => {
|
||||
const groupKey = data.ke
|
||||
const monitorId = data.mid
|
||||
const filename = data.filename
|
||||
const currentDate = data.currentDate
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
|
||||
const timelapseFrameDirectory = s.getTimelapseFrameDirectory(monitorConfig) + currentDate + `/`
|
||||
const fileWritePath = timelapseFrameDirectory + filename
|
||||
const writeStream = fs.createWriteStream(fileWritePath)
|
||||
data.writeDirectory = timelapseFrameDirectory
|
||||
initiateFileWriteFromChildNode(client,data,connectionId,(response) => {
|
||||
s.cx({
|
||||
f: 'deleteTimelapseFrame',
|
||||
file: filename,
|
||||
currentDate: currentDate,
|
||||
ke: groupKey,
|
||||
mid: monitorId
|
||||
},connectionId)
|
||||
s.insertTimelapseFrameDatabaseRow({
|
||||
ke: groupKey
|
||||
},data.queryInfo)
|
||||
resolve(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
function getActiveCameraCount(){
|
||||
let theCount = 0
|
||||
Object.keys(s.group).forEach(function(groupKey){
|
||||
const theGroup = s.group[groupKey]
|
||||
Object.keys(theGroup.activeMonitors).forEach(function(groupKey){
|
||||
const activeMonitor = theGroup.activeMonitors[monitorId]
|
||||
if(
|
||||
// watching
|
||||
activeMonitor.statusCode === 2 ||
|
||||
// recording
|
||||
activeMonitor.statusCode === 3 ||
|
||||
// starting
|
||||
activeMonitor.statusCode === 1 ||
|
||||
// started
|
||||
activeMonitor.statusCode === 9
|
||||
//// Idle, in memory
|
||||
// activeMonitor.statusCode === 6
|
||||
){
|
||||
++theCount
|
||||
}
|
||||
})
|
||||
})
|
||||
return theCount
|
||||
}
|
||||
function bindMonitorToChildNode(options){
|
||||
const groupKey = options.ke
|
||||
const monitorId = options.mid
|
||||
const childNodeSelected = options.childNodeId
|
||||
const theChildNode = s.childNodes[childNodeSelected]
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId];
|
||||
const monitorConfig = Object.assign({},s.group[groupKey].rawMonitorConfigurations[monitorId])
|
||||
theChildNode.activeCameras[groupKey + monitorId] = monitorConfig;
|
||||
activeMonitor.childNode = childNodeSelected
|
||||
activeMonitor.childNodeId = theChildNode.cnid;
|
||||
}
|
||||
function getNodeWithHighestCpuAndRamUse(){
|
||||
var nodeWithLowestCpuUse = 0
|
||||
var nodeWithLowestRamUse = 0
|
||||
const childNodeList = Object.keys(s.childNodes)
|
||||
childNodeList.forEach(function(webAddress){
|
||||
const theChildNode = s.childNodes[webAddress]
|
||||
if(
|
||||
theChildNode.cpuUsed > nodeWithLowestCpuUse &&
|
||||
theChildNode.ramUsed > nodeWithLowestRamUse
|
||||
){
|
||||
nodeWithLowestCpuUse = theChildNode.cpuUsed + 0.2
|
||||
nodeWithLowestRamUse = theChildNode.ramUsed + 50
|
||||
}
|
||||
})
|
||||
return {
|
||||
nodeWithLowestCpuUse,
|
||||
nodeWithLowestRamUse,
|
||||
}
|
||||
}
|
||||
async function selectNodeForOperation(options){
|
||||
const groupKey = options.ke
|
||||
const monitorId = options.mid
|
||||
const childNodeList = Object.keys(s.childNodes)
|
||||
if(childNodeList.length > 0){
|
||||
let childNodeFound = false
|
||||
let childNodeSelected = null;
|
||||
var nodeWithLowestActiveCamerasCount = 65535
|
||||
var nodeWithLowestActiveCameras = null
|
||||
let nodeWithLowestCpuPercent = 100
|
||||
let nodeWithLowestRamPercent = 100
|
||||
let {
|
||||
nodeWithLowestCpuUse,
|
||||
nodeWithLowestRamUse,
|
||||
} = getNodeWithHighestCpuAndRamUse();
|
||||
childNodeList.forEach(function(webAddress){
|
||||
const theChildNode = s.childNodes[webAddress]
|
||||
delete(theChildNode.activeCameras[groupKey + monitorId])
|
||||
const nodeCameraCount = Object.keys(theChildNode.activeCameras).length
|
||||
if(
|
||||
// child node is connected and available
|
||||
!theChildNode.dead &&
|
||||
// // look for child node with least number of running cameras
|
||||
// nodeCameraCount < nodeWithLowestActiveCamerasCount &&
|
||||
// look for child node with CPU usage below 75% (default)
|
||||
theChildNode.cpuUsed < nodeWithLowestCpuUse &&
|
||||
theChildNode.cpuPercent < maxCpuPercent &&
|
||||
theChildNode.cpuPercent < nodeWithLowestCpuPercent &&
|
||||
// look for child node with RAM usage below 75% (default)
|
||||
theChildNode.ramUsed < nodeWithLowestRamUse &&
|
||||
theChildNode.ramPercent < maxRamPercent &&
|
||||
theChildNode.ramPercent < nodeWithLowestRamPercent
|
||||
){
|
||||
// nodeWithLowestActiveCamerasCount = nodeCameraCount
|
||||
childNodeSelected = `${webAddress}`
|
||||
nodeWithLowestCpuUse = theChildNode.cpuUsed
|
||||
nodeWithLowestCpuPercent = theChildNode.cpuPercent
|
||||
nodeWithLowestRamUse = theChildNode.ramUsed
|
||||
nodeWithLowestRamPercent = theChildNode.ramPercent
|
||||
}
|
||||
})
|
||||
if(childNodeSelected && masterDoWorkToo){
|
||||
// const nodeCameraCount = getActiveCameraCount()
|
||||
const masterNodeHw = await getHwUsage();
|
||||
if(
|
||||
// nodeCameraCount < nodeWithLowestActiveCamerasCount &&
|
||||
masterNodeHw.cpuUsed < nodeWithLowestCpuUse &&
|
||||
masterNodeHw.cpuPercent < maxCpuPercent &&
|
||||
masterNodeHw.cpuPercent < nodeWithLowestCpuPercent &&
|
||||
// look for child node with RAM usage below 75% (default)
|
||||
masterNodeHw.ramUsed < nodeWithLowestRamUse &&
|
||||
masterNodeHw.ramPercent < maxRamPercent &&
|
||||
masterNodeHw.ramPercent < nodeWithLowestRamPercent
|
||||
){
|
||||
// nodeWithLowestActiveCamerasCount = nodeCameraCount
|
||||
// release child node selection and use master node
|
||||
childNodeSelected = null
|
||||
}
|
||||
}
|
||||
return childNodeSelected;
|
||||
}
|
||||
}
|
||||
async function getHwUsage(){
|
||||
const percent = await s.cpuUsage();
|
||||
const use = s.coreCount * (percent / 100)
|
||||
const ram = await s.ramUsage()
|
||||
return {
|
||||
ramUsed: ram.used,
|
||||
ramPercent: ram.percent,
|
||||
cpuUsed: use,
|
||||
cpuPercent: percent,
|
||||
}
|
||||
}
|
||||
return {
|
||||
getIpAddress,
|
||||
initiateDataConnection,
|
||||
onWebSocketDataFromChildNode,
|
||||
onDataConnectionDisconnect,
|
||||
initiateVideoWriteFromChildNode,
|
||||
initiateTimelapseFrameWriteFromChildNode,
|
||||
selectNodeForOperation,
|
||||
bindMonitorToChildNode,
|
||||
}
|
||||
}
|
|
@ -15,11 +15,13 @@ module.exports = function(s,config,lang,app){
|
|||
}
|
||||
if(!customerServerList){
|
||||
config.p2pServerList = {
|
||||
"vancouver-1": {
|
||||
"vancouver-1-v2": {
|
||||
name: 'Vancouver-1',
|
||||
host: 'p2p-vancouver-1.shinobi.cloud',
|
||||
p2pPort: '8084',
|
||||
webPort: '8000',
|
||||
v2: true,
|
||||
p2pPort: '81',
|
||||
webPort: '80',
|
||||
chartPort: '82',
|
||||
maxNetworkSpeed: {
|
||||
up: 5000,
|
||||
down: 5000,
|
||||
|
@ -30,11 +32,13 @@ module.exports = function(s,config,lang,app){
|
|||
lon: -123.1140607
|
||||
}
|
||||
},
|
||||
"toronto-1": {
|
||||
"toronto-1-v2": {
|
||||
name: 'Toronto-1',
|
||||
host: 'p2p-toronto-1.shinobi.cloud',
|
||||
p2pPort: '8084',
|
||||
webPort: '8000',
|
||||
v2: true,
|
||||
p2pPort: '81',
|
||||
webPort: '80',
|
||||
chartPort: '82',
|
||||
maxNetworkSpeed: {
|
||||
up: 5000,
|
||||
down: 5000,
|
||||
|
@ -45,11 +49,13 @@ module.exports = function(s,config,lang,app){
|
|||
lon: -79.3862837
|
||||
}
|
||||
},
|
||||
"paris-1": {
|
||||
"paris-1-v2": {
|
||||
name: 'Paris-1',
|
||||
host: 'p2p-paris-1.shinobi.cloud',
|
||||
p2pPort: '8084',
|
||||
webPort: '8000',
|
||||
v2: true,
|
||||
p2pPort: '81',
|
||||
webPort: '80',
|
||||
chartPort: '82',
|
||||
maxNetworkSpeed: {
|
||||
up: 200,
|
||||
down: 200,
|
||||
|
@ -83,7 +89,7 @@ module.exports = function(s,config,lang,app){
|
|||
const startWorker = () => {
|
||||
stopWorker()
|
||||
// set the first parameter as a string.
|
||||
const pathToWorkerScript = __dirname + '/commander/worker.js'
|
||||
const pathToWorkerScript = __dirname + `/commander/${config.useBetterP2P ? 'workerv2' : 'worker'}.js`
|
||||
const workerProcess = new Worker(pathToWorkerScript)
|
||||
workerProcess.on('message',function(data){
|
||||
switch(data.f){
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
const { parentPort } = require('worker_threads');
|
||||
const request = require('request');
|
||||
const fetch = require('node-fetch');
|
||||
const socketIOClient = require('socket.io-client');
|
||||
const p2pClientConnectionStaticName = 'Commander'
|
||||
const p2pClientConnections = {}
|
||||
|
@ -58,15 +58,28 @@ const initialize = (config,lang) => {
|
|||
if(method === 'GET' && data){
|
||||
requestEndpoint += '?' + createQueryStringFromObject(data)
|
||||
}
|
||||
const theRequest = request(requestEndpoint,{
|
||||
const theRequest = fetch(requestEndpoint,{
|
||||
method: method,
|
||||
json: method !== 'GET' ? (data ? data : null) : null
|
||||
}, typeof callback === 'function' ? (err,resp,body) => {
|
||||
// var json = parseJSON(body)
|
||||
if(err)console.error(err,data)
|
||||
callback(err,body,resp)
|
||||
} : null)
|
||||
.on('data', onDataReceived);
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: method !== 'GET' ? JSON.stringify(data ? data : null) : null
|
||||
})
|
||||
.then(res => {
|
||||
res.body.on('data', onDataReceived);
|
||||
return res
|
||||
});
|
||||
if(typeof callback === 'function'){
|
||||
theRequest.then(res => res.json())
|
||||
.then(json => {
|
||||
callback(null,json);
|
||||
})
|
||||
}
|
||||
theRequest.catch((err) => {
|
||||
console.error(err);
|
||||
if(typeof callback === 'function')callback(err,null);
|
||||
});
|
||||
return theRequest
|
||||
}
|
||||
const createShinobiSocketConnection = (connectionId) => {
|
||||
|
@ -88,12 +101,15 @@ const initialize = (config,lang) => {
|
|||
}
|
||||
//
|
||||
function startBridge(noLog){
|
||||
s.debugLog('p2p',`Connecting to ${selectedHost}...`)
|
||||
console.log('p2p',`Connecting to ${selectedHost}...`)
|
||||
if(connectionToP2PServer && connectionToP2PServer.connected){
|
||||
connectionToP2PServer.allowDisconnect = true;
|
||||
connectionToP2PServer.disconnect()
|
||||
}
|
||||
connectionToP2PServer = socketIOClient('ws://' + selectedHost, {transports:['websocket']});
|
||||
connectionToP2PServer = socketIOClient('ws://' + selectedHost, {
|
||||
transports:['websocket'],
|
||||
reconnection: false
|
||||
});
|
||||
if(!config.p2pApiKey){
|
||||
if(!noLog)s.systemLog('p2p',`Please fill 'p2pApiKey' in your conf.json.`)
|
||||
}
|
||||
|
@ -201,12 +217,7 @@ const initialize = (config,lang) => {
|
|||
})
|
||||
|
||||
});
|
||||
([
|
||||
'h265',
|
||||
'Base64',
|
||||
'FLV',
|
||||
'MP4',
|
||||
]).forEach((target) => {
|
||||
config.workerStreamOutHandlers.forEach((target) => {
|
||||
connectionToP2PServer.on(target,(initData) => {
|
||||
if(connectedUserWebSockets[initData.auth]){
|
||||
const clientConnectionToMachine = createShinobiSocketConnection(initData.auth + initData.ke + initData.id)
|
||||
|
@ -256,7 +267,9 @@ const initialize = (config,lang) => {
|
|||
connectionToP2PServer.on('disconnect',onDisconnect)
|
||||
}
|
||||
startBridge()
|
||||
setInterval(() => {
|
||||
startBridge(true)
|
||||
},1000 * 60 * 60 * 15)
|
||||
setInterval(function(){
|
||||
if(!connectionToP2PServer || !connectionToP2PServer.connected){
|
||||
connectionToP2PServer.connect()
|
||||
}
|
||||
},1000 * 60 * 15)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
const { parentPort } = require('worker_threads');
|
||||
process.on("uncaughtException", function(error) {
|
||||
console.error(error);
|
||||
});
|
||||
let remoteConnectionPort = 8080
|
||||
const net = require("net")
|
||||
const bson = require('bson')
|
||||
const WebSocket = require('cws')
|
||||
const s = {
|
||||
debugLog: (...args) => {
|
||||
parentPort.postMessage({
|
||||
f: 'debugLog',
|
||||
data: args
|
||||
})
|
||||
},
|
||||
systemLog: (...args) => {
|
||||
parentPort.postMessage({
|
||||
f: 'systemLog',
|
||||
data: args
|
||||
})
|
||||
},
|
||||
}
|
||||
parentPort.on('message',(data) => {
|
||||
switch(data.f){
|
||||
case'init':
|
||||
const config = data.config
|
||||
remoteConnectionPort = config.ssl ? config.ssl.port || 443 : config.port || 8080
|
||||
initialize(config,data.lang)
|
||||
break;
|
||||
case'exit':
|
||||
s.debugLog('Closing P2P Connection...')
|
||||
process.exit(0)
|
||||
break;
|
||||
}
|
||||
})
|
||||
var socketCheckTimer = null
|
||||
var heartbeatTimer = null
|
||||
var heartBeatCheckTimout = null
|
||||
let stayDisconnected = false
|
||||
const requestConnections = {}
|
||||
const requestConnectionsData = {}
|
||||
function startConnection(p2pServerAddress,subscriptionId){
|
||||
console.log('P2P : Connecting to Konekta P2P Server...')
|
||||
let tunnelToShinobi
|
||||
stayDisconnected = false
|
||||
const allMessageHandlers = []
|
||||
async function startWebsocketConnection(key,callback){
|
||||
function createWebsocketConnection(){
|
||||
return new Promise((resolve,reject) => {
|
||||
const newTunnel = new WebSocket(p2pServerAddress || 'ws://172.16.101.218:81');
|
||||
newTunnel.on('open', function(){
|
||||
resolve(newTunnel)
|
||||
})
|
||||
newTunnel.on('error', (err) => {
|
||||
console.log(`P2P newTunnel Error : `,err)
|
||||
console.log(`P2P Restarting...`)
|
||||
disconnectedConnection()
|
||||
})
|
||||
newTunnel.on('close', disconnectedConnection);
|
||||
newTunnel.onmessage = function(event){
|
||||
const data = bson.deserialize(Buffer.from(event.data))
|
||||
allMessageHandlers.forEach((handler) => {
|
||||
if(data.f === handler.key){
|
||||
handler.callback(data.data,data.rid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
clearInterval(socketCheckTimer)
|
||||
socketCheckTimer = setInterval(() => {
|
||||
s.debugLog('Tunnel Ready State :',newTunnel.readyState)
|
||||
if(newTunnel.readyState !== 1){
|
||||
s.debugLog('Tunnel NOT Ready! Reconnecting...')
|
||||
disconnectedConnection()
|
||||
}
|
||||
},1000 * 60)
|
||||
})
|
||||
}
|
||||
function disconnectedConnection(code,reason){
|
||||
s.debugLog('stayDisconnected',stayDisconnected)
|
||||
if(stayDisconnected)return;
|
||||
s.debugLog('DISCONNECTED! RESTARTING!')
|
||||
setTimeout(() => {
|
||||
startWebsocketConnection()
|
||||
},2000)
|
||||
}
|
||||
try{
|
||||
if(tunnelToShinobi)tunnelToShinobi.close()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
s.debugLog(p2pServerAddress)
|
||||
tunnelToShinobi = await createWebsocketConnection(p2pServerAddress,allMessageHandlers)
|
||||
console.log('P2P : Connected! Authenticating...')
|
||||
sendDataToTunnel({
|
||||
subscriptionId: subscriptionId
|
||||
})
|
||||
clearInterval(heartbeatTimer)
|
||||
heartbeatTimer = setInterval(() => {
|
||||
sendDataToTunnel({
|
||||
f: 'ping',
|
||||
})
|
||||
}, 1000 * 10)
|
||||
}
|
||||
function sendDataToTunnel(data){
|
||||
tunnelToShinobi.send(
|
||||
bson.serialize(data)
|
||||
)
|
||||
}
|
||||
startWebsocketConnection()
|
||||
function onIncomingMessage(key,callback){
|
||||
allMessageHandlers.push({
|
||||
key: key,
|
||||
callback: callback,
|
||||
})
|
||||
}
|
||||
function outboundMessage(key,data,requestId){
|
||||
sendDataToTunnel({
|
||||
f: key,
|
||||
data: data,
|
||||
rid: requestId
|
||||
})
|
||||
}
|
||||
function createRemoteSocket(host,port,requestId){
|
||||
// if(requestConnections[requestId]){
|
||||
// remotesocket.off('data')
|
||||
// remotesocket.off('drain')
|
||||
// remotesocket.off('close')
|
||||
// requestConnections[requestId].end()
|
||||
// }
|
||||
let remotesocket = new net.Socket();
|
||||
remotesocket.connect(port || remoteConnectionPort, host || 'localhost');
|
||||
requestConnections[requestId] = remotesocket
|
||||
remotesocket.on('data', function(data) {
|
||||
requestConnectionsData[requestId] = data.toString()
|
||||
outboundMessage('data',data,requestId)
|
||||
})
|
||||
remotesocket.on('drain', function() {
|
||||
outboundMessage('resume',{},requestId)
|
||||
});
|
||||
remotesocket.on('close', function() {
|
||||
delete(requestConnectionsData[requestId])
|
||||
outboundMessage('end',{},requestId)
|
||||
});
|
||||
return remotesocket
|
||||
}
|
||||
function writeToServer(data,requestId){
|
||||
var flushed = requestConnections[requestId].write(data.buffer)
|
||||
if (!flushed) {
|
||||
outboundMessage('pause',{},requestId)
|
||||
}
|
||||
}
|
||||
function refreshHeartBeatCheck(){
|
||||
clearTimeout(heartBeatCheckTimout)
|
||||
heartBeatCheckTimout = setTimeout(() => {
|
||||
startWebsocketConnection()
|
||||
},1000 * 10 * 1.5)
|
||||
}
|
||||
// onIncomingMessage('connect',(data,requestId) => {
|
||||
// console.log('New Request Incoming',requestId)
|
||||
// createRemoteSocket('172.16.101.94', 8080, requestId)
|
||||
// })
|
||||
onIncomingMessage('connect',(data,requestId) => {
|
||||
// const hostParts = data.host.split(':')
|
||||
// const host = hostParts[0]
|
||||
// const port = parseInt(hostParts[1]) || 80
|
||||
s.debugLog('New Request Incoming', null, null, requestId)
|
||||
const socket = createRemoteSocket(null, null, requestId)
|
||||
socket.on('ready',() => {
|
||||
s.debugLog('READY')
|
||||
writeToServer(data.init,requestId)
|
||||
})
|
||||
})
|
||||
onIncomingMessage('data',writeToServer)
|
||||
onIncomingMessage('resume',function(data,requestId){
|
||||
requestConnections[requestId].resume()
|
||||
})
|
||||
onIncomingMessage('pause',function(data,requestId){
|
||||
requestConnections[requestId].pause()
|
||||
})
|
||||
onIncomingMessage('pong',function(data,requestId){
|
||||
refreshHeartBeatCheck()
|
||||
s.debugLog('Heartbeat')
|
||||
})
|
||||
onIncomingMessage('init',function(data,requestId){
|
||||
console.log(`P2P : Authenticated!`)
|
||||
})
|
||||
onIncomingMessage('end',function(data,requestId){
|
||||
try{
|
||||
requestConnections[requestId].end()
|
||||
}catch(err){
|
||||
s.debugLog(`Reqest Failed to END ${requestId}`)
|
||||
s.debugLog(`Failed Request ${requestConnectionsData[requestId]}`)
|
||||
delete(requestConnectionsData[requestId])
|
||||
s.debugLog(err)
|
||||
// console.log('requestConnections',requestConnections)
|
||||
}
|
||||
})
|
||||
onIncomingMessage('disconnect',function(data,requestId){
|
||||
stayDisconnected = true
|
||||
})
|
||||
}
|
||||
|
||||
function initialize(config,lang){
|
||||
const selectedP2PServerId = config.p2pServerList[config.p2pHostSelected] ? config.p2pHostSelected : Object.keys(config.p2pServerList)[0]
|
||||
const p2pServerDetails = config.p2pServerList[selectedP2PServerId]
|
||||
const selectedHost = 'ws://' + p2pServerDetails.host + ':' + p2pServerDetails.p2pPort
|
||||
startConnection(selectedHost,config.p2pApiKey)
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
const async = require("async");
|
||||
const fetch = require("node-fetch");
|
||||
const mergeDeep = function(...objects) {
|
||||
const isObject = obj => obj && typeof obj === 'object';
|
||||
|
||||
|
@ -21,7 +22,18 @@ const mergeDeep = function(...objects) {
|
|||
return prev;
|
||||
}, {});
|
||||
}
|
||||
const getBuffer = async (url) => {
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
const arrayBuffer = await response.arrayBuffer();
|
||||
const buffer = Buffer.from(arrayBuffer);
|
||||
return buffer;
|
||||
} catch (error) {
|
||||
return { error };
|
||||
}
|
||||
};
|
||||
module.exports = {
|
||||
getBuffer: getBuffer,
|
||||
mergeDeep: mergeDeep,
|
||||
validateIntValue: (value) => {
|
||||
const newValue = !isNaN(parseInt(value)) ? parseInt(value) : null
|
||||
|
|
|
@ -45,6 +45,7 @@ module.exports = function(s){
|
|||
if(config.orphanedVideoCheckMax === undefined){config.orphanedVideoCheckMax = 2}
|
||||
if(config.detectorMergePamRegionTriggers === undefined){config.detectorMergePamRegionTriggers = false}
|
||||
if(config.probeMonitorOnStart === undefined){config.probeMonitorOnStart = true}
|
||||
if(config.showLoginTypeSelector === undefined){config.showLoginTypeSelector = true}
|
||||
//Child Nodes
|
||||
if(config.childNodes === undefined)config.childNodes = {};
|
||||
//enabled
|
||||
|
|
|
@ -123,13 +123,24 @@ module.exports = function(s,config,lang,app,io){
|
|||
}
|
||||
}
|
||||
async function getSnapshotFromOnvif(onvifOptions){
|
||||
return await createSnapshot({
|
||||
output: ['-s 400x400'],
|
||||
url: addCredentialsToStreamLink({
|
||||
let theUrl;
|
||||
if(onvifOptions.mid && onvifOptions.ke){
|
||||
const groupKey = onvifOptions.ke
|
||||
const monitorId = onvifOptions.mid
|
||||
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
|
||||
theUrl = (await theDevice.services.media.getSnapshotUri({
|
||||
ProfileToken : theDevice.current_profile.token,
|
||||
})).GetSnapshotUriResponse.MediaUri.Uri;
|
||||
}else{
|
||||
theUrl = addCredentialsToStreamLink({
|
||||
username: onvifOptions.username,
|
||||
password: onvifOptions.password,
|
||||
url: onvifOptions.uri
|
||||
}),
|
||||
})
|
||||
}
|
||||
return await createSnapshot({
|
||||
output: ['-s 400x400'],
|
||||
url: theUrl,
|
||||
})
|
||||
}
|
||||
/**
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
var os = require('os');
|
||||
var exec = require('child_process').exec;
|
||||
var request = require('request')
|
||||
module.exports = function(s,config,lang){
|
||||
const { fetchWithAuthentication } = require('../basic/utils.js')(process.cwd(),config)
|
||||
const moveLock = {}
|
||||
const ptzTimeoutsUntilResetToHome = {}
|
||||
const sliceUrlAuth = (url) => {
|
||||
|
@ -219,70 +219,50 @@ module.exports = function(s,config,lang){
|
|||
var stopCamera = function(){
|
||||
let stopURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}_stop`]
|
||||
let controlOptions = s.cameraControlOptionsFromUrl(stopURL,monitorConfig)
|
||||
let requestOptions = {
|
||||
url : controlBaseUrl + controlOptions.path,
|
||||
method : controlOptions.method
|
||||
const hasDigestAuthEnabled = monitorConfig.details.control_digest_auth === '1'
|
||||
const requestUrl = controlBaseUrl + controlOptions.path
|
||||
const response = {
|
||||
ok: true,
|
||||
type:'Control Trigger Ended'
|
||||
}
|
||||
if(controlOptions.username && controlOptions.password){
|
||||
requestOptions.auth = {
|
||||
user: controlOptions.username,
|
||||
pass: controlOptions.password
|
||||
}
|
||||
}
|
||||
if(controlOptions.postData){
|
||||
requestOptions.form = controlOptions.postData
|
||||
}
|
||||
if(monitorConfig.details.control_digest_auth === '1'){
|
||||
requestOptions.uri = sliceUrlAuth(requestOptions.url);
|
||||
delete requestOptions.url;
|
||||
requestOptions.auth.sendImmediately = false;
|
||||
}
|
||||
request(requestOptions,function(err,data){
|
||||
const msg = {
|
||||
ok: true,
|
||||
type:'Control Trigger Ended'
|
||||
}
|
||||
if(err){
|
||||
msg.ok = false
|
||||
msg.type = 'Control Error'
|
||||
msg.msg = err
|
||||
}
|
||||
const theRequest = fetchWithAuthentication(requestUrl,{
|
||||
method: controlOptions.method,
|
||||
digestAuth: hasDigestAuthEnabled,
|
||||
body: controlOptions.postData || null
|
||||
});
|
||||
theRequest.then(res => res.text())
|
||||
.then((data) => {
|
||||
moveLock[options.ke + options.id] = false
|
||||
callback(msg)
|
||||
s.userLog(monitorConfig,msg);
|
||||
s.userLog(monitorConfig,response);
|
||||
});
|
||||
theRequest.catch((err) => {
|
||||
response.ok = false
|
||||
response.type = 'Control Error'
|
||||
response.msg = err
|
||||
callback(response)
|
||||
})
|
||||
}
|
||||
if(options.direction === 'stopMove'){
|
||||
stopCamera()
|
||||
}else{
|
||||
moveLock[options.ke + options.id] = true
|
||||
let controlURL = controlBaseUrl + monitorConfig.details[`control_url_${options.direction}`]
|
||||
let controlOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig)
|
||||
let requestOptions = {
|
||||
url: controlBaseUrl + controlOptions.path,
|
||||
method: controlOptions.method
|
||||
const hasDigestAuthEnabled = monitorConfig.details.control_digest_auth === '1'
|
||||
const requestUrl = controlBaseUrl + controlOptions.path
|
||||
const response = {
|
||||
ok: true,
|
||||
type: lang['Control Triggered']
|
||||
}
|
||||
if(controlOptions.username && controlOptions.password){
|
||||
requestOptions.auth = {
|
||||
user: controlOptions.username,
|
||||
pass: controlOptions.password
|
||||
}
|
||||
}
|
||||
if(controlOptions.postData){
|
||||
requestOptions.form = controlOptions.postData
|
||||
}
|
||||
if(monitorConfig.details.control_digest_auth === '1'){
|
||||
requestOptions.uri = sliceUrlAuth(requestOptions.url);
|
||||
delete requestOptions.url;
|
||||
requestOptions.auth.sendImmediately = false;
|
||||
}
|
||||
moveLock[options.ke + options.id] = true
|
||||
request(requestOptions,function(err,data){
|
||||
if(err){
|
||||
callback({ok:false,type:'Control Error',msg:err})
|
||||
return
|
||||
}
|
||||
const theRequest = fetchWithAuthentication(requestUrl,{
|
||||
method: controlOptions.method,
|
||||
digestAuth: hasDigestAuthEnabled,
|
||||
body: controlOptions.postData || null
|
||||
});
|
||||
theRequest.then(res => res.text())
|
||||
.then((data) => {
|
||||
if(monitorConfig.details.control_stop == '1' && options.direction !== 'center' ){
|
||||
s.userLog(monitorConfig,{type:'Control Triggered Started'});
|
||||
s.userLog(monitorConfig,{type: lang['Control Trigger Started']});
|
||||
if(controlUrlStopTimeout > 0){
|
||||
setTimeout(function(){
|
||||
stopCamera()
|
||||
|
@ -290,8 +270,14 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
}else{
|
||||
moveLock[options.ke + options.id] = false
|
||||
callback({ok:true,type:'Control Triggered'})
|
||||
callback(response)
|
||||
}
|
||||
});
|
||||
theRequest.catch((err) => {
|
||||
response.ok = false
|
||||
response.type = lang['Control Error']
|
||||
response.msg = err
|
||||
callback(response)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
const fs = require('fs-extra');
|
||||
const express = require('express')
|
||||
const request = require('request')
|
||||
const unzipper = require('unzipper')
|
||||
const fetch = require("node-fetch")
|
||||
const spawn = require('child_process').spawn
|
||||
module.exports = async (s,config,lang,app,io) => {
|
||||
const { fetchDownloadAndWrite } = require('./basic/utils.js')(process.cwd(),config)
|
||||
s.debugLog(`+++++++++++CustomAutoLoad Modules++++++++++++`)
|
||||
const runningInstallProcesses = {}
|
||||
const modulesBasePath = __dirname + '/customAutoLoad/'
|
||||
|
@ -65,10 +65,9 @@ module.exports = async (s,config,lang,app,io) => {
|
|||
fs.mkdirSync(downloadPath)
|
||||
return new Promise(async (resolve, reject) => {
|
||||
fs.mkdir(downloadPath, () => {
|
||||
request(downloadUrl).pipe(fs.createWriteStream(downloadPath + '.zip'))
|
||||
.on('finish',() => {
|
||||
zip = fs.createReadStream(downloadPath + '.zip')
|
||||
.pipe(unzipper.Parse())
|
||||
fetchDownloadAndWrite(downloadUrl,downloadPath + '.zip', 1)
|
||||
.then((readStream) => {
|
||||
readStream.pipe(unzipper.Parse())
|
||||
.on('entry', async (file) => {
|
||||
if(file.type === 'Directory'){
|
||||
try{
|
||||
|
@ -225,10 +224,10 @@ module.exports = async (s,config,lang,app,io) => {
|
|||
var fullPath = thirdLevelName + '/' + filename
|
||||
var blockPrefix = ''
|
||||
switch(true){
|
||||
case filename.contains('super.'):
|
||||
case filename.indexOf('super.') > -1:
|
||||
blockPrefix = 'super'
|
||||
break;
|
||||
case filename.contains('admin.'):
|
||||
case filename.indexOf('admin.') > -1:
|
||||
blockPrefix = 'admin'
|
||||
break;
|
||||
}
|
||||
|
@ -278,22 +277,24 @@ module.exports = async (s,config,lang,app,io) => {
|
|||
})
|
||||
break;
|
||||
case'definitions':
|
||||
var definitionsFolder = s.checkCorrectPathEnding(customModulePath) + 'definitions/'
|
||||
fs.readdir(definitionsFolder,function(err,files){
|
||||
if(err)return console.log(err);
|
||||
files.forEach(function(filename){
|
||||
var fileData = require(definitionsFolder + filename)
|
||||
var rule = filename.replace('.json','').replace('.js','')
|
||||
if(config.language === rule){
|
||||
s.definitions = s.mergeDeep(s.definitions,fileData)
|
||||
}
|
||||
if(s.loadedDefinitons[rule]){
|
||||
s.loadedDefinitons[rule] = s.mergeDeep(s.loadedDefinitons[rule],fileData)
|
||||
}else{
|
||||
s.loadedDefinitons[rule] = s.mergeDeep(s.copySystemDefaultDefinitions(),fileData)
|
||||
}
|
||||
})
|
||||
})
|
||||
console.error('This Method has been deprecated. Could not load : ', customModulePath + 'defintions/')
|
||||
console.error('Make your module\'s index.js file make the changes directly.')
|
||||
// var definitionsFolder = s.checkCorrectPathEnding(customModulePath) + 'definitions/'
|
||||
// fs.readdir(definitionsFolder,function(err,files){
|
||||
// if(err)return console.log(err);
|
||||
// files.forEach(function(filename){
|
||||
// var fileData = require(definitionsFolder + filename)
|
||||
// var rule = filename.replace('.json','').replace('.js','')
|
||||
// if(config.language === rule){
|
||||
// s.definitions = s.mergeDeep(s.definitions,fileData)
|
||||
// }
|
||||
// if(s.loadedDefinitons[rule]){
|
||||
// s.loadedDefinitons[rule] = s.mergeDeep(s.loadedDefinitons[rule],fileData)
|
||||
// }else{
|
||||
// s.loadedDefinitons[rule] = s.mergeDeep(s.copySystemDefaultDefinitions(),fileData)
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
break;
|
||||
}
|
||||
})
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
const { createWebSocketServer } = require('./basic/websocketTools.js')
|
||||
module.exports = function(s,config,lang,app,io){
|
||||
const {
|
||||
triggerEvent,
|
||||
} = require('./events/utils.js')(s,config,lang)
|
||||
s.dataPortTokens = {}
|
||||
const theWebSocket = createWebSocketServer()
|
||||
function setClientKillTimerIfNotAuthenticatedInTime(client){
|
||||
client.killTimer = setTimeout(function(){
|
||||
client.terminate()
|
||||
},10000)
|
||||
}
|
||||
function clearKillTimer(client){
|
||||
clearTimeout(client.killTimer)
|
||||
}
|
||||
theWebSocket.on('connection',(client) => {
|
||||
// client.send(someDataToSendAsStringOrBinary)
|
||||
setClientKillTimerIfNotAuthenticatedInTime(client)
|
||||
function onAuthenticate(data){
|
||||
clearKillTimer(client)
|
||||
if(data in s.dataPortTokens){
|
||||
client.removeListener('message', onAuthenticate);
|
||||
client.on('message', onAuthenticatedData)
|
||||
delete(s.dataPortTokens[data]);
|
||||
}else{
|
||||
client.terminate()
|
||||
}
|
||||
}
|
||||
function onAuthenticatedData(jsonData){
|
||||
const data = JSON.parse(jsonData);
|
||||
switch(data.f){
|
||||
case'trigger':
|
||||
triggerEvent(data)
|
||||
break;
|
||||
case's.tx':
|
||||
s.tx(data.data,data.to)
|
||||
break;
|
||||
case'debugLog':
|
||||
s.debugLog(data.data)
|
||||
break;
|
||||
default:
|
||||
console.log(data)
|
||||
break;
|
||||
}
|
||||
s.onDataPortMessageExtensions.forEach(function(extender){
|
||||
extender(data)
|
||||
})
|
||||
}
|
||||
client.on('message', onAuthenticate)
|
||||
client.on('close', () => {
|
||||
clearTimeout(client.killTimer)
|
||||
})
|
||||
client.on('disconnect', () => {
|
||||
clearTimeout(client.killTimer)
|
||||
})
|
||||
})
|
||||
s.onHttpRequestUpgrade('/dataPort',(request, socket, head) => {
|
||||
theWebSocket.handleUpgrade(request, socket, head, function done(ws) {
|
||||
theWebSocket.emit('connection', ws, request)
|
||||
})
|
||||
})
|
||||
}
|
|
@ -140,7 +140,7 @@ module.exports = function(s,config){
|
|||
if(options.groupBy){
|
||||
dbQuery.groupBy(options.groupBy)
|
||||
}
|
||||
if(options.limit){
|
||||
if(options.limit && options.limit !== '0'){
|
||||
if(`${options.limit}`.indexOf(',') === -1){
|
||||
dbQuery.limit(options.limit)
|
||||
}else{
|
||||
|
@ -177,6 +177,7 @@ module.exports = function(s,config){
|
|||
]
|
||||
const monitorRestrictions = options.monitorRestrictions
|
||||
var frameLimit = options.limit
|
||||
const noLimit = options.noLimit === '1'
|
||||
const endIsStartTo = options.endIsStartTo
|
||||
const chosenDate = options.date
|
||||
const startDate = options.startDate ? stringToSqlTime(options.startDate) : null
|
||||
|
@ -217,6 +218,7 @@ module.exports = function(s,config){
|
|||
whereQuery.push(['filename','=',options.filename])
|
||||
frameLimit = "1";
|
||||
}
|
||||
if(noLimit)frameLimit = '0';
|
||||
options.orderBy = options.orderBy ? options.orderBy : ['time','desc']
|
||||
if(options.count)options.groupBy = options.groupBy ? options.groupBy : options.orderBy[0]
|
||||
knexQuery({
|
||||
|
@ -337,7 +339,7 @@ module.exports = function(s,config){
|
|||
endDate: endTime,
|
||||
startOperator: startTimeOperator,
|
||||
endOperator: endTimeOperator,
|
||||
limit: options.limit,
|
||||
limit: options.noLimit === '1' ? '0' : options.limit,
|
||||
archived: archived,
|
||||
rowType: rowName,
|
||||
endIsStartTo: endIsStartTo
|
||||
|
|
|
@ -1,25 +1,19 @@
|
|||
var fs = require('fs')
|
||||
var express = require('express')
|
||||
const {
|
||||
mergeDeep
|
||||
} = require('./common.js')
|
||||
const frameworkBase = require(`../definitions/base.js`)
|
||||
module.exports = function(s,config,lang,app,io){
|
||||
s.location.definitions = s.mainDirectory+'/definitions'
|
||||
try{
|
||||
var definitions = require(s.location.definitions+'/'+config.language+'.js')(s,config,lang)
|
||||
}catch(er){
|
||||
console.error(er)
|
||||
console.log('There was an error loading your definition file.')
|
||||
try{
|
||||
var definitions = require(s.location.definitions+'/en_CA.js')(s,config,lang)
|
||||
}catch(er){
|
||||
console.error(er)
|
||||
console.log('There was an error loading your definition file.')
|
||||
var definitions = require(s.location.definitions+'/en_CA.json');
|
||||
}
|
||||
function getFramework(languageFile){
|
||||
return frameworkBase(s,config,languageFile)
|
||||
}
|
||||
const defaultFramework = getFramework(lang)
|
||||
//load defintions dynamically
|
||||
s.definitions = definitions
|
||||
s.definitions = defaultFramework
|
||||
s.copySystemDefaultDefinitions = function(){
|
||||
//en_CA
|
||||
return Object.assign(s.definitions,{})
|
||||
return Object.assign({},defaultFramework)
|
||||
}
|
||||
s.loadedDefinitons={}
|
||||
s.loadedDefinitons[config.language] = s.copySystemDefaultDefinitions()
|
||||
|
@ -28,10 +22,15 @@ module.exports = function(s,config,lang,app,io){
|
|||
var file = s.loadedDefinitons[rule]
|
||||
if(!file){
|
||||
try{
|
||||
s.loadedDefinitons[rule] = require(s.location.definitions+'/'+rule+'.js')(s,config,lang)
|
||||
s.loadedDefinitons[rule] = Object.assign(s.copySystemDefaultDefinitions(),s.loadedDefinitons[rule])
|
||||
// console.log(getFramework(lang))
|
||||
s.loadedDefinitons[rule] = Object.assign(
|
||||
{},
|
||||
s.copySystemDefaultDefinitions(),
|
||||
getFramework(s.getLanguageFile(rule))
|
||||
);
|
||||
file = s.loadedDefinitons[rule]
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
file = s.copySystemDefaultDefinitions()
|
||||
}
|
||||
}
|
||||
|
@ -40,5 +39,5 @@ module.exports = function(s,config,lang,app,io){
|
|||
}
|
||||
return file
|
||||
}
|
||||
return definitions
|
||||
return defaultFramework
|
||||
}
|
||||
|
|
|
@ -188,14 +188,21 @@ module.exports = function(s,config,lang,app,io){
|
|||
createDropInEventsDirectory()
|
||||
if(!config.ftpServerPort)config.ftpServerPort = 21
|
||||
if(!config.ftpServerUrl)config.ftpServerUrl = `ftp://0.0.0.0:${config.ftpServerPort}`
|
||||
if(!config.ftpServerPasvUrl)config.ftpServerPasvUrl = config.ftpServerUrl.replace(/.*:\/\//, '').replace(/:.*/, '');
|
||||
if(!config.ftpServerPasvMinPort)config.ftpServerPasvMinPort = 10050;
|
||||
if(!config.ftpServerPasvMaxPort)config.ftpServerPasvMaxPort = 10100;
|
||||
config.ftpServerUrl = config.ftpServerUrl.replace('{{PORT}}',config.ftpServerPort)
|
||||
const FtpSrv = require('ftp-srv')
|
||||
|
||||
const ftpServer = new FtpSrv({
|
||||
url: config.ftpServerUrl,
|
||||
// pasv_url must be set to enable PASV; ftp-srv uses its known IP if given 127.0.0.1,
|
||||
// and smart clients will ignore the IP anyway. Some Dahua IP cams require PASV mode.
|
||||
// ftp-srv just wants an IP only (no protocol or port)
|
||||
pasv_url: config.ftpServerUrl.replace(/.*:\/\//, '').replace(/:.*/, ''),
|
||||
pasv_url: config.ftpServerPasvUrl,
|
||||
pasv_min: config.ftpServerPasvMinPort,
|
||||
pasv_max: config.ftpServerPasvMaxPort,
|
||||
greeting: "Shinobi FTP dropInEvent Server says hello!",
|
||||
log: require('bunyan').createLogger({
|
||||
name: 'ftp-srv',
|
||||
level: 100
|
||||
|
@ -331,4 +338,5 @@ module.exports = function(s,config,lang,app,io){
|
|||
s.systemLog(`SMTP Server running on port ${config.smtpServerPort}...`)
|
||||
})
|
||||
}
|
||||
require('./dropInEvents/mqtt.js')(s,config,lang,app,io)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,230 @@
|
|||
module.exports = (s,config,lang,app,io) => {
|
||||
if(config.mqttClient === true){
|
||||
console.log('Loading MQTT Inbound Connectivity...')
|
||||
const mqtt = require('mqtt')
|
||||
const {
|
||||
triggerEvent,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
function sendPlainEvent(options){
|
||||
const groupKey = options.ke
|
||||
const monitorId = options.mid || options.id
|
||||
const subKey = options.subKey
|
||||
const endpoint = options.host
|
||||
triggerEvent({
|
||||
id: monitorId,
|
||||
ke: groupKey,
|
||||
details: {
|
||||
confidence: 100,
|
||||
name: 'mqtt',
|
||||
plug: endpoint,
|
||||
reason: subKey
|
||||
},
|
||||
},config.mqttEventForceSaveEvent)
|
||||
}
|
||||
function sendFrigateEvent(data,options){
|
||||
const groupKey = options.ke
|
||||
const monitorId = options.mid || options.id
|
||||
const subKey = options.subKey
|
||||
const endpoint = options.host
|
||||
const frigateMatrix = data.after || data.before
|
||||
const confidenceScore = frigateMatrix.top_score * 100
|
||||
const activeZones = frigateMatrix.entered_zones.join(', ')
|
||||
const shinobiMatrix = {
|
||||
x: frigateMatrix.box[0],
|
||||
y: frigateMatrix.box[1],
|
||||
width: frigateMatrix.box[2],
|
||||
height: frigateMatrix.box[3],
|
||||
tag: frigateMatrix.label,
|
||||
confidence: confidenceScore,
|
||||
}
|
||||
triggerEvent({
|
||||
id: monitorId,
|
||||
ke: groupKey,
|
||||
details: {
|
||||
confidence: confidenceScore,
|
||||
name: 'mqtt-'+endpoint,
|
||||
plug: subKey,
|
||||
reason: activeZones,
|
||||
matrices: [shinobiMatrix]
|
||||
},
|
||||
},config.mqttEventForceSaveEvent)
|
||||
}
|
||||
function createMqttSubscription(options){
|
||||
const mqttEndpoint = options.host
|
||||
const subKey = options.subKey
|
||||
const groupKey = options.ke
|
||||
const onData = options.onData || function(){}
|
||||
s.debugLog('Connecting.... mqtt://' + mqttEndpoint)
|
||||
const client = mqtt.connect('mqtt://' + mqttEndpoint)
|
||||
client.on('connect', function () {
|
||||
s.debugLog('Connected! mqtt://' + mqttEndpoint)
|
||||
client.subscribe(subKey, function (err) {
|
||||
if (err) {
|
||||
s.debugLog(err)
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
mid: '$USER'
|
||||
},{
|
||||
type: lang['MQTT Error'],
|
||||
msg: err
|
||||
})
|
||||
}else{
|
||||
client.on('message', function (topic, message) {
|
||||
const data = s.parseJSON(message.toString())
|
||||
onData(data)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
return client
|
||||
}
|
||||
// const onEventTrigger = async () => {}
|
||||
// const onMonitorUnexpectedExit = (monitorConfig) => {}
|
||||
const loadMqttListBotForUser = function(user){
|
||||
const groupKey = user.ke
|
||||
const userDetails = s.parseJSON(user.details);
|
||||
if(userDetails.mqttclient === '1'){
|
||||
const mqttClientList = userDetails.mqttclient_list || []
|
||||
if(!s.group[groupKey].mqttSubscriptions)s.group[groupKey].mqttSubscriptions = {};
|
||||
const mqttSubs = s.group[groupKey].mqttSubscriptions
|
||||
mqttClientList.forEach(function(row,n){
|
||||
try{
|
||||
const mqttSubId = `${row.host} ${row.subKey}`
|
||||
const messageConversionTypes = row.type || []
|
||||
const monitorsToTrigger = row.monitors || []
|
||||
const triggerAllMonitors = monitorsToTrigger.indexOf('$all') > -1
|
||||
const doActions = []
|
||||
const onData = (data) => {
|
||||
doActions.forEach(function(theAction){
|
||||
theAction(data)
|
||||
})
|
||||
s.debugLog('MQTT Data',row,data)
|
||||
}
|
||||
if(mqttSubs[mqttSubId]){
|
||||
mqttSubs[mqttSubId].end()
|
||||
delete(mqttSubs[mqttSubId])
|
||||
}
|
||||
messageConversionTypes.forEach(function(type){
|
||||
switch(type){
|
||||
case'plain':
|
||||
doActions.push(function(data){
|
||||
// data is unused for plain event.
|
||||
let listOfMonitors = monitorsToTrigger
|
||||
if(triggerAllMonitors){
|
||||
const activeMonitors = Object.keys(s.group[groupKey].activeMonitors)
|
||||
listOfMonitors = activeMonitors
|
||||
}
|
||||
listOfMonitors.forEach(function(monitorId){
|
||||
sendPlainEvent({
|
||||
host: row.host,
|
||||
subKey: row.subKey,
|
||||
ke: groupKey,
|
||||
mid: monitorId
|
||||
})
|
||||
})
|
||||
})
|
||||
break;
|
||||
case'frigate':
|
||||
// https://docs.frigate.video/integrations/mqtt/#frigateevents
|
||||
doActions.push(function(data){
|
||||
// this handler requires using frigate/events
|
||||
// only "new" events will be captured.
|
||||
if(data.type === 'new'){
|
||||
let listOfMonitors = monitorsToTrigger
|
||||
if(triggerAllMonitors){
|
||||
const activeMonitors = Object.keys(s.group[groupKey].activeMonitors)
|
||||
listOfMonitors = activeMonitors
|
||||
}
|
||||
listOfMonitors.forEach(function(monitorId){
|
||||
sendFrigateEvent(data,{
|
||||
host: row.host,
|
||||
subKey: row.subKey,
|
||||
ke: groupKey,
|
||||
mid: monitorId
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
})
|
||||
mqttSubs[mqttSubId] = createMqttSubscription({
|
||||
host: row.host,
|
||||
subKey: row.subKey,
|
||||
ke: groupKey,
|
||||
onData: onData,
|
||||
})
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
// s.systemLog(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const unloadMqttListBotForUser = function(user){
|
||||
const groupKey = user.ke
|
||||
const mqttSubs = s.group[groupKey].mqttSubscriptions || {}
|
||||
Object.keys(mqttSubs).forEach(function(mqttSubId){
|
||||
try{
|
||||
mqttSubs[mqttSubId].end()
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
// s.userLog({
|
||||
// ke: groupKey,
|
||||
// mid: '$USER'
|
||||
// },{
|
||||
// type: lang['MQTT Error'],
|
||||
// msg: err
|
||||
// })
|
||||
}
|
||||
delete(mqttSubs[mqttSubId])
|
||||
})
|
||||
}
|
||||
const onBeforeAccountSave = function(data){
|
||||
data.d.mqttclient_list = []
|
||||
}
|
||||
s.loadGroupAppExtender(loadMqttListBotForUser)
|
||||
s.unloadGroupAppExtender(unloadMqttListBotForUser)
|
||||
s.beforeAccountSave(onBeforeAccountSave)
|
||||
// s.onEventTrigger(onEventTrigger)
|
||||
// s.onMonitorUnexpectedExit(onMonitorUnexpectedExit)
|
||||
s.definitions["Account Settings"].blocks["MQTT Inbound"] = {
|
||||
"evaluation": "$user.details.use_mqttclient !== '0'",
|
||||
"name": lang['MQTT Inbound'],
|
||||
"color": "green",
|
||||
"info": [
|
||||
{
|
||||
"name": "detail=mqttclient",
|
||||
"selector":"u_mqttclient",
|
||||
"field": lang.Enabled,
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fieldType": "btn",
|
||||
"class": `btn-success mqtt-add-row`,
|
||||
"btnContent": `<i class="fa fa-plus"></i> ${lang['Add']}`,
|
||||
},
|
||||
{
|
||||
"id": "mqttclient_list",
|
||||
"fieldType": "div",
|
||||
},
|
||||
{
|
||||
"fieldType": "script",
|
||||
"src": "assets/js/bs5.mqtt.js",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,6 @@ const moment = require('moment');
|
|||
const execSync = require('child_process').execSync;
|
||||
const exec = require('child_process').exec;
|
||||
const spawn = require('child_process').spawn;
|
||||
const request = require('request');
|
||||
const imageSaveEventLock = {};
|
||||
// Matrix In Region Libs >
|
||||
const SAT = require('sat')
|
||||
|
@ -12,6 +11,9 @@ const P = SAT.Polygon;
|
|||
const B = SAT.Box;
|
||||
// Matrix In Region Libs />
|
||||
module.exports = (s,config,lang,app,io) => {
|
||||
// Event Filters >
|
||||
const acceptableOperators = ['indexOf','!indexOf','===','!==','>=','>','<','<=']
|
||||
// Event Filters />
|
||||
const {
|
||||
splitForFFPMEG
|
||||
} = require('../ffmpeg/utils.js')(s,config,lang)
|
||||
|
@ -21,6 +23,10 @@ module.exports = (s,config,lang,app,io) => {
|
|||
const {
|
||||
cutVideoLength
|
||||
} = require('../video/utils.js')(s,config,lang)
|
||||
const {
|
||||
isEven,
|
||||
fetchTimeout,
|
||||
} = require('../basic/utils.js')(process.cwd(),config)
|
||||
async function saveImageFromEvent(options,frameBuffer){
|
||||
const monitorId = options.mid || options.id
|
||||
const groupKey = options.ke
|
||||
|
@ -187,8 +193,20 @@ module.exports = (s,config,lang,app,io) => {
|
|||
Object.keys(filters).forEach(function(key){
|
||||
var conditionChain = {}
|
||||
var dFilter = filters[key]
|
||||
if(dFilter.enabled === '0')return;
|
||||
var numberOfOpenAndCloseBrackets = 0
|
||||
dFilter.where.forEach(function(condition,place){
|
||||
conditionChain[place] = {ok:false,next:condition.p4,matrixCount:0}
|
||||
const hasOpenBracket = condition.openBracket === '1';
|
||||
const hasCloseBracket = condition.closeBracket === '1';
|
||||
conditionChain[place] = {
|
||||
ok: false,
|
||||
next: condition.p4,
|
||||
matrixCount: 0,
|
||||
openBracket: hasOpenBracket,
|
||||
closeBracket: hasCloseBracket,
|
||||
}
|
||||
if(hasOpenBracket)++numberOfOpenAndCloseBrackets;
|
||||
if(hasCloseBracket)++numberOfOpenAndCloseBrackets;
|
||||
if(d.details.matrices)conditionChain[place].matrixCount = d.details.matrices.length
|
||||
var modifyFilters = function(toCheck,matrixPosition){
|
||||
var param = toCheck[condition.p1]
|
||||
|
@ -210,7 +228,12 @@ module.exports = (s,config,lang,app,io) => {
|
|||
pass()
|
||||
}
|
||||
break;
|
||||
default:
|
||||
case'===':
|
||||
case'!==':
|
||||
case'>=':
|
||||
case'>':
|
||||
case'<':
|
||||
case'<=':
|
||||
if(eval('param '+condition.p2+' "'+condition.p3.replace(/"/g,'\\"')+'"')){
|
||||
pass()
|
||||
}
|
||||
|
@ -243,7 +266,7 @@ module.exports = (s,config,lang,app,io) => {
|
|||
var atSecond = parseInt(doAtTime[2]) - 1 || timeNow.getSeconds()
|
||||
var nowAddedInSeconds = atHourNow * 60 * 60 + atMinuteNow * 60 + atSecondNow
|
||||
var conditionAddedInSeconds = atHour * 60 * 60 + atMinute * 60 + atSecond
|
||||
if(eval('nowAddedInSeconds '+condition.p2+' conditionAddedInSeconds')){
|
||||
if(acceptableOperators.indexOf(condition.p2) > -1 && eval('nowAddedInSeconds '+condition.p2+' conditionAddedInSeconds')){
|
||||
conditionChain[place].ok = true
|
||||
}
|
||||
}
|
||||
|
@ -254,20 +277,29 @@ module.exports = (s,config,lang,app,io) => {
|
|||
}
|
||||
})
|
||||
var conditionArray = Object.values(conditionChain)
|
||||
var validationString = ''
|
||||
var validationString = []
|
||||
var allowBrackets = false;
|
||||
if (numberOfOpenAndCloseBrackets === 0 || isEven(numberOfOpenAndCloseBrackets)){
|
||||
allowBrackets = true;
|
||||
}else{
|
||||
s.userLog(d,{type:lang["Event Filter Error"],msg:lang.eventFilterErrorBrackets})
|
||||
}
|
||||
conditionArray.forEach(function(condition,number){
|
||||
validationString += condition.ok+' '
|
||||
validationString.push(`${allowBrackets && condition.openBracket ? '(' : ''}${condition.ok}${allowBrackets && condition.closeBracket ? ')' : ''}`);
|
||||
if(conditionArray.length-1 !== number){
|
||||
validationString += condition.next+' '
|
||||
validationString.push(condition.next)
|
||||
}
|
||||
})
|
||||
if(eval(validationString)){
|
||||
if(eval(validationString.join(' '))){
|
||||
if(dFilter.actions.halt !== '1'){
|
||||
delete(dFilter.actions.halt)
|
||||
Object.keys(dFilter.actions).forEach(function(key){
|
||||
var value = dFilter.actions[key]
|
||||
filter[key] = parseValue(key,value)
|
||||
})
|
||||
if(dFilter.actions.record === '1'){
|
||||
filter.forceRecord = true
|
||||
}
|
||||
}else{
|
||||
filter.halt = true
|
||||
}
|
||||
|
@ -362,7 +394,7 @@ module.exports = (s,config,lang,app,io) => {
|
|||
matrices: eventDetails.matrices || [],
|
||||
},d.frame)
|
||||
}
|
||||
if(forceSave || (filter.save && monitorDetails.detector_save === '1')){
|
||||
if(forceSave || (filter.save || monitorDetails.detector_save === '1')){
|
||||
s.knexQuery({
|
||||
action: "insert",
|
||||
table: "Events",
|
||||
|
@ -370,7 +402,7 @@ module.exports = (s,config,lang,app,io) => {
|
|||
ke: d.ke,
|
||||
mid: d.id,
|
||||
details: detailString,
|
||||
time: eventTime,
|
||||
time: s.formattedTime(eventTime),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -384,9 +416,8 @@ module.exports = (s,config,lang,app,io) => {
|
|||
detector_timeout = parseFloat(monitorDetails.detector_timeout)
|
||||
}
|
||||
if(
|
||||
filter.record &&
|
||||
(filter.forceRecord || (filter.record && monitorDetails.detector_trigger === '1')) &&
|
||||
monitorConfig.mode === 'start' &&
|
||||
monitorDetails.detector_trigger === '1' &&
|
||||
(monitorDetails.detector_record_method === 'sip' || monitorDetails.detector_record_method === 'hot')
|
||||
){
|
||||
createEventBasedRecording(d,moment(eventTime).subtract(5,'seconds').format('YYYY-MM-DDTHH-mm-ss'))
|
||||
|
@ -401,14 +432,17 @@ module.exports = (s,config,lang,app,io) => {
|
|||
var detector_webhook_url = addEventDetailsToString(d,monitorDetails.detector_webhook_url)
|
||||
var webhookMethod = monitorDetails.detector_webhook_method
|
||||
if(!webhookMethod || webhookMethod === '')webhookMethod = 'GET'
|
||||
request(detector_webhook_url,{method: webhookMethod,encoding:null},function(err,data){
|
||||
if(err){
|
||||
s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}})
|
||||
}
|
||||
fetchTimeout(detector_webhook_url,10000,{
|
||||
method: webhookMethod
|
||||
}).catch((err) => {
|
||||
s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}})
|
||||
})
|
||||
}
|
||||
|
||||
if(filter.command && monitorDetails.detector_command_enable === '1' && !s.group[d.ke].activeMonitors[d.id].detector_command){
|
||||
if(
|
||||
filter.command ||
|
||||
(monitorDetails.detector_command_enable === '1' && !s.group[d.ke].activeMonitors[d.id].detector_command)
|
||||
){
|
||||
s.group[d.ke].activeMonitors[d.id].detector_command = s.createTimeout('detector_command',s.group[d.ke].activeMonitors[d.id],monitorDetails.detector_command_timeout,10)
|
||||
var detector_command = addEventDetailsToString(d,monitorDetails.detector_command)
|
||||
if(detector_command === '')return
|
||||
|
@ -431,23 +465,27 @@ module.exports = (s,config,lang,app,io) => {
|
|||
if(activeMonitor && activeMonitor.eventBasedRecording && activeMonitor.eventBasedRecording.process){
|
||||
const eventBasedRecording = activeMonitor.eventBasedRecording
|
||||
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
|
||||
const videoLength = monitorConfig.details.detector_send_video_length
|
||||
const videoLength = parseInt(monitorConfig.details.detector_send_video_length) || 10
|
||||
const recordingDirectory = s.getVideoDirectory(monitorConfig)
|
||||
const fileTime = eventBasedRecording.lastFileTime
|
||||
const filename = `${fileTime}.mp4`
|
||||
response.filename = `${filename}`
|
||||
response.filePath = `${recordingDirectory}${filename}`
|
||||
eventBasedRecording.process.on('close',function(){
|
||||
eventBasedRecording.process.on('exit',function(){
|
||||
setTimeout(async () => {
|
||||
if(!isNaN(videoLength)){
|
||||
const cutResponse = await cutVideoLength({
|
||||
ke: groupKey,
|
||||
mid: monitorId,
|
||||
filePath: response.filePath,
|
||||
cutLength: parseInt(videoLength),
|
||||
cutLength: videoLength,
|
||||
})
|
||||
response.filename = cutResponse.filename
|
||||
response.filePath = cutResponse.filePath
|
||||
if(cutResponse.ok){
|
||||
response.filename = cutResponse.filename
|
||||
response.filePath = cutResponse.filePath
|
||||
}else{
|
||||
s.debugLog('cutResponse',cutResponse)
|
||||
}
|
||||
}
|
||||
resolve(response)
|
||||
},1000)
|
||||
|
@ -594,12 +632,13 @@ module.exports = (s,config,lang,app,io) => {
|
|||
halt : false,
|
||||
addToMotionCounter : true,
|
||||
useLock : true,
|
||||
save : true,
|
||||
webhook : true,
|
||||
command : true,
|
||||
save : false,
|
||||
webhook : false,
|
||||
command : false,
|
||||
record : true,
|
||||
forceRecord : false,
|
||||
indifference : false,
|
||||
countObjects : true
|
||||
countObjects : false
|
||||
}
|
||||
if(!s.group[d.ke] || !s.group[d.ke].activeMonitors[d.id]){
|
||||
return s.systemLog(lang['No Monitor Found, Ignoring Request'])
|
||||
|
@ -614,8 +653,7 @@ module.exports = (s,config,lang,app,io) => {
|
|||
})
|
||||
const eventDetails = d.details
|
||||
const passedEventFilters = checkEventFilters(d,monitorDetails,filter)
|
||||
if(!passedEventFilters)return
|
||||
const detailString = JSON.stringify(eventDetails)
|
||||
if(!passedEventFilters)return;
|
||||
const eventTime = new Date()
|
||||
if(
|
||||
filter.addToMotionCounter &&
|
||||
|
@ -638,8 +676,7 @@ module.exports = (s,config,lang,app,io) => {
|
|||
addToEventCounter(d)
|
||||
}
|
||||
if(
|
||||
filter.countObjects &&
|
||||
monitorDetails.detector_obj_count === '1' &&
|
||||
(filter.countObjects || monitorDetails.detector_obj_count === '1') &&
|
||||
monitorDetails.detector_obj_count_in_region !== '1'
|
||||
){
|
||||
didCountingAlready = true
|
||||
|
|
|
@ -103,6 +103,16 @@ module.exports = function(s,config){
|
|||
s.onFfmpegCameraStringCreationExtensions.push(callback)
|
||||
}
|
||||
//
|
||||
s.onFfmpegBuildMainStreamExtensions = []
|
||||
s.onFfmpegBuildMainStream = function(callback){
|
||||
s.onFfmpegBuildMainStreamExtensions.push(callback)
|
||||
}
|
||||
//
|
||||
s.onFfmpegBuildStreamChannelExtensions = []
|
||||
s.onFfmpegBuildStreamChannel = function(callback){
|
||||
s.onFfmpegBuildStreamChannelExtensions.push(callback)
|
||||
}
|
||||
//
|
||||
s.onMonitorPingFailedExtensions = []
|
||||
s.onMonitorPingFailed = function(callback){
|
||||
s.onMonitorPingFailedExtensions.push(callback)
|
||||
|
@ -112,6 +122,11 @@ module.exports = function(s,config){
|
|||
s.onMonitorDied = function(callback){
|
||||
s.onMonitorDiedExtensions.push(callback)
|
||||
}
|
||||
//
|
||||
s.onMonitorCreateStreamPipeExtensions = []
|
||||
s.onMonitorCreateStreamPipe = function(callback){
|
||||
s.onMonitorCreateStreamPipeExtensions.push(callback)
|
||||
}
|
||||
|
||||
///////// SYSTEM ////////
|
||||
s.onProcessReadyExtensions = []
|
||||
|
@ -169,6 +184,16 @@ module.exports = function(s,config){
|
|||
s.onSubscriptionCheckExtensions.push(callback)
|
||||
}
|
||||
//
|
||||
s.onDataPortMessageExtensions = []
|
||||
s.onDataPortMessage = function(callback){
|
||||
s.onDataPortMessageExtensions.push(callback)
|
||||
}
|
||||
//
|
||||
s.onHttpRequestUpgradeExtensions = {}
|
||||
s.onHttpRequestUpgrade = function(nameOfCallback,callback){
|
||||
s.onHttpRequestUpgradeExtensions[nameOfCallback] = callback
|
||||
}
|
||||
//
|
||||
/////// VIDEOS ////////
|
||||
s.insertCompletedVideoExtensions = []
|
||||
s.insertCompletedVideoExtender = function(callback){
|
||||
|
|
|
@ -26,9 +26,15 @@ module.exports = async (s,config,lang,onFinish) => {
|
|||
|
||||
s.ffmpeg = function(e){
|
||||
try{
|
||||
const activeMonitor = s.group[e.ke].activeMonitors[e.mid];
|
||||
const dataPortToken = s.gid(10);
|
||||
s.dataPortTokens[dataPortToken] = {
|
||||
type: 'cameraThread',
|
||||
ke: e.ke,
|
||||
mid: e.mid,
|
||||
}
|
||||
const ffmpegCommand = [`-progress pipe:5`];
|
||||
([
|
||||
buildMainInput(e),
|
||||
const allOutputs = [
|
||||
buildMainStream(e),
|
||||
buildJpegApiOutput(e),
|
||||
buildMainRecording(e),
|
||||
|
@ -36,45 +42,53 @@ module.exports = async (s,config,lang,onFinish) => {
|
|||
buildMainDetector(e),
|
||||
buildEventRecordingOutput(e),
|
||||
buildTimelapseOutput(e),
|
||||
]).forEach(function(commandStringPart){
|
||||
ffmpegCommand.push(commandStringPart)
|
||||
})
|
||||
s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){
|
||||
extender(e,ffmpegCommand)
|
||||
})
|
||||
const stdioPipes = createPipeArray(e)
|
||||
const ffmpegCommandString = ffmpegCommand.join(' ')
|
||||
//hold ffmpeg command for log stream
|
||||
s.group[e.ke].activeMonitors[e.mid].ffmpeg = sanitizedFfmpegCommand(e,ffmpegCommandString)
|
||||
//clean the string of spatial impurities and split for spawn()
|
||||
const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString)
|
||||
try{
|
||||
fs.unlinkSync(e.sdir + 'cmd.txt')
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({
|
||||
cmd: ffmpegCommandParsed,
|
||||
pipes: stdioPipes.length,
|
||||
rawMonitorConfig: s.group[e.ke].rawMonitorConfigurations[e.id],
|
||||
globalInfo: {
|
||||
config: config,
|
||||
isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected
|
||||
}
|
||||
},null,3),'utf8')
|
||||
var cameraCommandParams = [
|
||||
config.monitorDaemonPath ? config.monitorDaemonPath : __dirname + '/cameraThread/singleCamera.js',
|
||||
config.ffmpegDir,
|
||||
e.sdir + 'cmd.txt'
|
||||
]
|
||||
const cameraProcess = spawn('node',cameraCommandParams,{detached: true,stdio: stdioPipes})
|
||||
if(config.debugLog === true){
|
||||
cameraProcess.stderr.on('data',(data) => {
|
||||
console.log(`${e.ke} ${e.mid}`)
|
||||
console.log(data.toString())
|
||||
];
|
||||
if(allOutputs.filter(output => !!output).length > 0){
|
||||
([
|
||||
buildMainInput(e),
|
||||
]).concat(allOutputs).forEach(function(commandStringPart){
|
||||
ffmpegCommand.push(commandStringPart)
|
||||
})
|
||||
s.onFfmpegCameraStringCreationExtensions.forEach(function(extender){
|
||||
extender(e,ffmpegCommand)
|
||||
})
|
||||
const stdioPipes = createPipeArray(e)
|
||||
const ffmpegCommandString = ffmpegCommand.join(' ')
|
||||
//hold ffmpeg command for log stream
|
||||
activeMonitor.ffmpeg = sanitizedFfmpegCommand(e,ffmpegCommandString)
|
||||
//clean the string of spatial impurities and split for spawn()
|
||||
const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString)
|
||||
try{
|
||||
fs.unlinkSync(e.sdir + 'cmd.txt')
|
||||
}catch(err){
|
||||
|
||||
}
|
||||
fs.writeFileSync(e.sdir + 'cmd.txt',JSON.stringify({
|
||||
dataPortToken: dataPortToken,
|
||||
cmd: ffmpegCommandParsed,
|
||||
pipes: stdioPipes.length,
|
||||
rawMonitorConfig: s.group[e.ke].rawMonitorConfigurations[e.id],
|
||||
globalInfo: {
|
||||
config: config,
|
||||
isAtleatOneDetectorPluginConnected: s.isAtleatOneDetectorPluginConnected
|
||||
}
|
||||
},null,3),'utf8')
|
||||
var cameraCommandParams = [
|
||||
config.monitorDaemonPath ? config.monitorDaemonPath : __dirname + '/cameraThread/singleCamera.js',
|
||||
config.ffmpegDir,
|
||||
e.sdir + 'cmd.txt'
|
||||
]
|
||||
const cameraProcess = spawn('node',cameraCommandParams,{detached: true,stdio: stdioPipes})
|
||||
if(config.debugLog === true && config.debugLogMonitors === true){
|
||||
cameraProcess.stderr.on('data',(data) => {
|
||||
console.log(`${e.ke} ${e.mid}`)
|
||||
console.log(data.toString())
|
||||
})
|
||||
}
|
||||
return cameraProcess
|
||||
}else{
|
||||
return null
|
||||
}
|
||||
return cameraProcess
|
||||
}catch(err){
|
||||
s.systemLog(err)
|
||||
return null
|
||||
|
|
|
@ -9,6 +9,8 @@ module.exports = (s,config,lang) => {
|
|||
const {
|
||||
validateDimensions,
|
||||
} = require('./utils.js')(s,config,lang)
|
||||
if(!config.outputsWithAudio)config.outputsWithAudio = ['hls','flv','mp4','rtmp'];
|
||||
if(!config.outputsNotCapableOfPresets)config.outputsNotCapableOfPresets = [];
|
||||
const hasCudaEnabled = (monitor) => {
|
||||
return monitor.details.accelerator === '1' && monitor.details.hwaccel === 'cuvid' && monitor.details.hwaccel_vcodec === ('h264_cuvid' || 'hevc_cuvid' || 'mjpeg_cuvid' || 'mpeg4_cuvid')
|
||||
}
|
||||
|
@ -183,7 +185,7 @@ module.exports = (s,config,lang) => {
|
|||
const createStreamChannel = function(e,number,channel){
|
||||
//`e` is the monitor object
|
||||
//`x` is an object used to contain temporary values.
|
||||
const channelStreamDirectory = !isNaN(parseInt(number)) ? `${e.sdir}channel${number}/` : e.sdir
|
||||
const channelStreamDirectory = !isNaN(parseInt(number)) ? `${e.sdir || s.getStreamsDirectory(e)}channel${number}/` : e.sdir
|
||||
if(channelStreamDirectory !== e.sdir && !fs.existsSync(channelStreamDirectory)){
|
||||
try{
|
||||
fs.mkdirSync(channelStreamDirectory)
|
||||
|
@ -203,7 +205,7 @@ module.exports = (s,config,lang) => {
|
|||
const streamType = channel.stream_type ? channel.stream_type : 'hls'
|
||||
const videoFps = !isNaN(parseFloat(channel.stream_fps)) && channel.stream_fps !== '0' ? parseFloat(channel.stream_fps) : streamType === 'rtmp' ? '30' : null
|
||||
const inputMap = buildInputMap(e,e.details.input_map_choices[`stream_channel-${channelNumber}`])
|
||||
const outputCanHaveAudio = (streamType === 'hls' || streamType === 'mp4' || streamType === 'flv' || streamType === 'h265' || streamType === 'rtmp')
|
||||
const outputCanHaveAudio = config.outputsWithAudio.indexOf(streamType) > -1;
|
||||
const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64'
|
||||
const outputIsPresetCapable = outputCanHaveAudio
|
||||
const { videoWidth, videoHeight } = validateDimensions(channel.stream_scale_x,channel.stream_scale_y)
|
||||
|
@ -246,7 +248,7 @@ module.exports = (s,config,lang) => {
|
|||
streamFilters.push(channel.stream_vf)
|
||||
}
|
||||
if(outputIsPresetCapable){
|
||||
const streamPreset = streamType !== 'h265' && channel.preset_stream ? channel.preset_stream : null
|
||||
const streamPreset = config.outputsNotCapableOfPresets.indexOf(streamType) === -1 && channel.preset_stream ? channel.preset_stream : null
|
||||
if(streamPreset){
|
||||
streamFlags.push(`-preset ${streamPreset}`)
|
||||
}
|
||||
|
@ -287,18 +289,18 @@ module.exports = (s,config,lang) => {
|
|||
streamFlags.push(`-g 1`)
|
||||
}
|
||||
}
|
||||
streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${channelStreamDirectory}s.m3u8"`)
|
||||
streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist+discont_start "${channelStreamDirectory}s.m3u8"`)
|
||||
break;
|
||||
case'mjpeg':
|
||||
streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:${number}`)
|
||||
break;
|
||||
case'h265':
|
||||
streamFlags.push(`-movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1 -f hevc pipe:${number}`)
|
||||
break;
|
||||
case'b64':case'':case undefined:case null://base64
|
||||
streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:${number}`)
|
||||
break;
|
||||
}
|
||||
s.onFfmpegBuildStreamChannelExtensions.forEach(function(extender){
|
||||
extender(streamType,streamFlags,number,e)
|
||||
});
|
||||
return ' ' + streamFlags.join(' ')
|
||||
}
|
||||
const buildMainInput = function(e){
|
||||
|
@ -375,7 +377,7 @@ module.exports = (s,config,lang) => {
|
|||
//x = temporary values
|
||||
const streamFlags = []
|
||||
const streamType = e.details.stream_type ? e.details.stream_type : 'hls'
|
||||
if(streamType !== 'jpeg'){
|
||||
if(streamType !== 'jpeg' && streamType !== 'useSubstream'){
|
||||
const isCudaEnabled = hasCudaEnabled(e)
|
||||
const streamFilters = []
|
||||
const videoCodecisCopy = e.details.stream_vcodec === 'copy'
|
||||
|
@ -384,7 +386,7 @@ module.exports = (s,config,lang) => {
|
|||
const videoQuality = e.details.stream_quality ? e.details.stream_quality : '1'
|
||||
const videoFps = !isNaN(parseFloat(e.details.stream_fps)) && e.details.stream_fps !== '0' ? parseFloat(e.details.stream_fps) : null
|
||||
const inputMap = buildInputMap(e,e.details.input_map_choices.stream)
|
||||
const outputCanHaveAudio = (streamType === 'hls' || streamType === 'mp4' || streamType === 'flv' || streamType === 'h265')
|
||||
const outputCanHaveAudio = config.outputsWithAudio.indexOf(streamType) > -1;
|
||||
const outputRequiresEncoding = streamType === 'mjpeg' || streamType === 'b64'
|
||||
const outputIsPresetCapable = outputCanHaveAudio
|
||||
const { videoWidth, videoHeight } = validateDimensions(e.details.stream_scale_x,e.details.stream_scale_y)
|
||||
|
@ -429,7 +431,7 @@ module.exports = (s,config,lang) => {
|
|||
streamFilters.push(e.details.stream_vf)
|
||||
}
|
||||
if(outputIsPresetCapable){
|
||||
const streamPreset = streamType !== 'h265' && e.details.preset_stream ? e.details.preset_stream : null
|
||||
const streamPreset = config.outputsNotCapableOfPresets.indexOf(streamType) === -1 && e.details.preset_stream ? e.details.preset_stream : null
|
||||
if(streamPreset){
|
||||
streamFlags.push(`-preset ${streamPreset}`)
|
||||
}
|
||||
|
@ -460,18 +462,18 @@ module.exports = (s,config,lang) => {
|
|||
streamFlags.push(`-g 1`)
|
||||
}
|
||||
}
|
||||
streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${e.sdir}s.m3u8"`)
|
||||
streamFlags.push(`-f hls -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist+discont_start "${e.sdir}s.m3u8"`)
|
||||
break;
|
||||
case'mjpeg':
|
||||
streamFlags.push(`-an -c:v mjpeg -f mpjpeg -boundary_tag shinobi pipe:1`)
|
||||
break;
|
||||
case'h265':
|
||||
streamFlags.push(`-movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1 -f hevc pipe:1`)
|
||||
break;
|
||||
case'b64':case'':case undefined:case null://base64
|
||||
streamFlags.push(`-an -c:v mjpeg -f image2pipe pipe:1`)
|
||||
break;
|
||||
}
|
||||
s.onFfmpegBuildMainStreamExtensions.forEach(function(extender){
|
||||
extender(streamType,streamFlags,e)
|
||||
});
|
||||
if(e.details.custom_output){
|
||||
streamFlags.push(e.details.custom_output)
|
||||
}
|
||||
|
@ -652,20 +654,20 @@ module.exports = (s,config,lang) => {
|
|||
if(objectDetectorOutputIsEnabled){
|
||||
addObjectDetectorInputMap()
|
||||
addObjectDetectValues()
|
||||
detectorFlags.push('-an -f singlejpeg pipe:4')
|
||||
detectorFlags.push('-an -f mjpeg pipe:4')
|
||||
}
|
||||
}else if(sendFramesToObjectDetector){
|
||||
addObjectDetectorInputMap()
|
||||
addObjectDetectValues()
|
||||
detectorFlags.push('-an -f singlejpeg pipe:4')
|
||||
detectorFlags.push('-an -f mjpeg pipe:4')
|
||||
}else{
|
||||
addInputMap()
|
||||
detectorFlags.push('-an -f singlejpeg pipe:4')
|
||||
detectorFlags.push('-an -f mjpeg pipe:4')
|
||||
}
|
||||
}else if(sendFramesToObjectDetector){
|
||||
addObjectDetectorInputMap()
|
||||
addObjectDetectValues()
|
||||
detectorFlags.push('-an -f singlejpeg pipe:4')
|
||||
detectorFlags.push('-an -f mjpeg pipe:4')
|
||||
}
|
||||
return detectorFlags.join(' ')
|
||||
}
|
||||
|
@ -737,7 +739,7 @@ module.exports = (s,config,lang) => {
|
|||
outputFlags.push(`-g 1`)
|
||||
}
|
||||
}
|
||||
outputFlags.push(`-f hls -live_start_index -3 -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "${e.sdir}detectorStream.m3u8"`)
|
||||
outputFlags.push(`-f hls -live_start_index -3 -hls_time ${hlsTime} -hls_list_size ${hlsListSize} -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist+discont_start "${e.sdir}detectorStream.m3u8"`)
|
||||
}
|
||||
return outputFlags.join(' ')
|
||||
}
|
||||
|
@ -757,11 +759,63 @@ module.exports = (s,config,lang) => {
|
|||
if(videoFilters.length > 0){
|
||||
videoFlags.push(`-vf "${videoFilters.join(',')}"`)
|
||||
}
|
||||
videoFlags.push(`-f singlejpeg -an -q:v 1 pipe:7`)
|
||||
videoFlags.push(`-f mjpeg -an -q:v 1 pipe:7`)
|
||||
return videoFlags.join(' ')
|
||||
}
|
||||
return ``
|
||||
}
|
||||
const getDefaultSubstreamFields = function(monitor){
|
||||
const subStreamFields = s.parseJSON(monitor.details.substream || {input:{},output:{}})
|
||||
const inputAndConnectionFields = Object.assign({
|
||||
"type":"h264",
|
||||
"fulladdress":"",
|
||||
"sfps":"",
|
||||
"aduration":"",
|
||||
"probesize":"",
|
||||
"stream_loop":"0",
|
||||
"rtsp_transport":"",
|
||||
"accelerator":"0",
|
||||
"hwaccel":"",
|
||||
"hwaccel_vcodec":"auto",
|
||||
"hwaccel_device":"",
|
||||
"cust_input":""
|
||||
},subStreamFields.input);
|
||||
const outputFields = Object.assign({
|
||||
"stream_type":"hls",
|
||||
"rtmp_server_url":"",
|
||||
"rtmp_stream_key":"",
|
||||
"stream_mjpeg_clients":"",
|
||||
"stream_vcodec":"copy",
|
||||
"stream_acodec":"no",
|
||||
"stream_fps":"",
|
||||
"hls_time":"",
|
||||
"preset_stream":"",
|
||||
"hls_list_size":"",
|
||||
"stream_quality":"",
|
||||
"stream_v_br":"",
|
||||
"stream_a_br":"",
|
||||
"stream_scale_x":"",
|
||||
"stream_scale_y":"",
|
||||
"rotate_stream":"no",
|
||||
"svf":"",
|
||||
"cust_stream":""
|
||||
},subStreamFields.output);
|
||||
return {
|
||||
inputAndConnectionFields,
|
||||
outputFields,
|
||||
}
|
||||
}
|
||||
const buildSubstreamString = function(channelNumber,monitor){
|
||||
let ffmpegParts = []
|
||||
const {
|
||||
inputAndConnectionFields,
|
||||
outputFields,
|
||||
} = getDefaultSubstreamFields(monitor)
|
||||
ffmpegParts.push(`-loglevel ${monitor.details.loglevel || 'warning'}`)
|
||||
ffmpegParts.push(createInputMap(monitor,channelNumber,inputAndConnectionFields))
|
||||
ffmpegParts.push(createStreamChannel(monitor,channelNumber,outputFields))
|
||||
return ffmpegParts.join(' ')
|
||||
}
|
||||
return {
|
||||
createStreamChannel: createStreamChannel,
|
||||
buildMainInput: buildMainInput,
|
||||
|
@ -772,5 +826,7 @@ module.exports = (s,config,lang) => {
|
|||
buildMainDetector: buildMainDetector,
|
||||
buildEventRecordingOutput: buildEventRecordingOutput,
|
||||
buildTimelapseOutput: buildTimelapseOutput,
|
||||
getDefaultSubstreamFields: getDefaultSubstreamFields,
|
||||
buildSubstreamString: buildSubstreamString,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
function createStringForSubstreamProcess(options){
|
||||
let options = {
|
||||
"type":"h264",
|
||||
"fulladdress":"",
|
||||
"sfps":"",
|
||||
"aduration":"",
|
||||
"probesize":"",
|
||||
"stream_loop":"0",
|
||||
"rtsp_transport":"",
|
||||
"accelerator":"0",
|
||||
"hwaccel":"auto",
|
||||
"hwaccel_vcodec":"auto",
|
||||
"hwaccel_device":"",
|
||||
"cust_input":""
|
||||
}
|
||||
}
|
|
@ -155,11 +155,11 @@ module.exports = (s,config,lang) => {
|
|||
}
|
||||
return sanitizedCmd
|
||||
}
|
||||
const createPipeArray = function(e){
|
||||
const createPipeArray = function(e, amountToAdd){
|
||||
const stdioPipes = [];
|
||||
var times = config.pipeAddition;
|
||||
if(e.details.stream_channels){
|
||||
times+=e.details.stream_channels.length
|
||||
var times = amountToAdd ? amountToAdd + config.pipeAddition : config.pipeAddition;
|
||||
if(e.details && e.details.stream_channels){
|
||||
times += e.details.stream_channels.length
|
||||
}
|
||||
for(var i=0; i < times; i++){
|
||||
stdioPipes.push('pipe')
|
||||
|
|
|
@ -16,14 +16,11 @@ const currentCPUInfo = {
|
|||
total: 0,
|
||||
active: 0
|
||||
}
|
||||
const lastCPUInfo = {
|
||||
let lastCPUInfo = {
|
||||
total: 0,
|
||||
active: 0
|
||||
}
|
||||
exports.getCpuUsageOnLinux = () => {
|
||||
lastCPUInfo.active = currentCPUInfo.active;
|
||||
lastCPUInfo.idle = currentCPUInfo.idle;
|
||||
lastCPUInfo.total = currentCPUInfo.total;
|
||||
return new Promise((resolve,reject) => {
|
||||
const getUsage = function(callback){
|
||||
fs.readFile("/proc/stat" ,'utf8', function(err, data){
|
||||
|
@ -36,6 +33,7 @@ exports.getCpuUsageOnLinux = () => {
|
|||
}
|
||||
currentCPUInfo.active = currentCPUInfo.total - currentCPUInfo.idle
|
||||
currentCPUInfo.percentUsed = calculateCPUPercentage(lastCPUInfo, currentCPUInfo);
|
||||
lastCPUInfo = Object.assign({},currentCPUInfo)
|
||||
callback(currentCPUInfo.percentUsed)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,16 +4,17 @@ module.exports = function(s,config){
|
|||
config.language='en_CA'
|
||||
}
|
||||
try{
|
||||
var lang = require(s.location.languages+'/'+config.language+'.json');
|
||||
var lang = {};
|
||||
eval(`lang = ${fs.readFileSync(s.location.languages+'/'+config.language+'.json','utf8')}`)
|
||||
}catch(er){
|
||||
console.error(er)
|
||||
console.log('There was an error loading your language file.')
|
||||
var lang = require(s.location.languages+'/en_CA.json');
|
||||
eval(`lang = ${fs.readFileSync(s.location.languages+'/en_CA.json','utf8')}`)
|
||||
}
|
||||
//load languages dynamically
|
||||
s.copySystemDefaultLanguage = function(){
|
||||
//en_CA
|
||||
return Object.assign(lang,{})
|
||||
return Object.assign({},lang)
|
||||
}
|
||||
s.listOfPossibleLanguages = []
|
||||
fs.readdirSync(s.mainDirectory + '/languages').forEach(function(filename){
|
||||
|
@ -28,12 +29,15 @@ module.exports = function(s,config){
|
|||
s.getLanguageFile = function(rule){
|
||||
if(rule && rule !== ''){
|
||||
var file = s.loadedLanguages[file]
|
||||
s.debugLog(file)
|
||||
if(!file){
|
||||
try{
|
||||
s.loadedLanguages[rule] = require(s.location.languages+'/'+rule+'.json')
|
||||
s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),s.loadedLanguages[rule])
|
||||
let newLang = {}
|
||||
eval(`newLang = ${fs.readFileSync(s.location.languages+'/'+rule+'.json','utf8')}`)
|
||||
s.loadedLanguages[rule] = Object.assign(s.copySystemDefaultLanguage(),newLang)
|
||||
file = s.loadedLanguages[rule]
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
file = s.copySystemDefaultLanguage()
|
||||
}
|
||||
}
|
||||
|
|
363
libs/monitor.js
363
libs/monitor.js
|
@ -5,7 +5,6 @@ const exec = require('child_process').exec;
|
|||
const Mp4Frag = require('mp4frag');
|
||||
const onvif = require("shinobi-onvif");
|
||||
const treekill = require('tree-kill');
|
||||
const request = require('request');
|
||||
const connectionTester = require('connection-tester')
|
||||
const SoundDetection = require('shinobi-sound-detection')
|
||||
const async = require("async");
|
||||
|
@ -15,6 +14,14 @@ const {
|
|||
} = require('worker_threads');
|
||||
const { copyObject, createQueue, queryStringToObject, createQueryStringFromObject } = require('./common.js')
|
||||
module.exports = function(s,config,lang){
|
||||
const { fetchTimeout } = require('./basic/utils.js')(process.cwd(),config)
|
||||
const isMasterNode = (
|
||||
(
|
||||
config.childNodes.enabled === true &&
|
||||
config.childNodes.mode === 'master'
|
||||
) ||
|
||||
config.childNodes.enabled === false
|
||||
);
|
||||
const {
|
||||
probeMonitor,
|
||||
getStreamInfoFromProbe,
|
||||
|
@ -27,6 +34,11 @@ module.exports = function(s,config,lang){
|
|||
processKill,
|
||||
cameraDestroy,
|
||||
monitorConfigurationMigrator,
|
||||
attachStreamChannelHandlers,
|
||||
setActiveViewer,
|
||||
getActiveViewerCount,
|
||||
destroySubstreamProcess,
|
||||
attachMainProcessHandlers,
|
||||
} = require('./monitor/utils.js')(s,config,lang)
|
||||
const {
|
||||
addEventDetailsToString,
|
||||
|
@ -39,13 +51,18 @@ module.exports = function(s,config,lang){
|
|||
const {
|
||||
scanForOrphanedVideos
|
||||
} = require('./video/utils.js')(s,config,lang)
|
||||
const {
|
||||
selectNodeForOperation,
|
||||
bindMonitorToChildNode
|
||||
} = require('./childNode/utils.js')(s,config,lang)
|
||||
const startMonitorInQueue = createQueue(1, 3)
|
||||
s.initiateMonitorObject = function(e){
|
||||
if(!s.group[e.ke]){s.group[e.ke]={}};
|
||||
if(!s.group[e.ke].activeMonitors){s.group[e.ke].activeMonitors={}}
|
||||
if(!s.group[e.ke].activeMonitors[e.mid]){s.group[e.ke].activeMonitors[e.mid]={}}
|
||||
const activeMonitor = s.group[e.ke].activeMonitors[e.mid]
|
||||
|
||||
activeMonitor.ke = e.ke
|
||||
activeMonitor.mid = e.mid
|
||||
if(!activeMonitor.streamIn){activeMonitor.streamIn={}};
|
||||
if(!activeMonitor.emitterChannel){activeMonitor.emitterChannel={}};
|
||||
if(!activeMonitor.mp4frag){activeMonitor.mp4frag={}};
|
||||
|
@ -53,7 +70,7 @@ module.exports = function(s,config,lang){
|
|||
if(!activeMonitor.contentWriter){activeMonitor.contentWriter={}};
|
||||
if(!activeMonitor.childNodeStreamWriters){activeMonitor.childNodeStreamWriters={}};
|
||||
if(!activeMonitor.eventBasedRecording){activeMonitor.eventBasedRecording={}};
|
||||
if(!activeMonitor.watch){activeMonitor.watch={}};
|
||||
if(!activeMonitor.watch){activeMonitor.watch = []};
|
||||
if(!activeMonitor.fixingVideos){activeMonitor.fixingVideos={}};
|
||||
// if(!activeMonitor.viewerConnection){activeMonitor.viewerConnection={}};
|
||||
// if(!activeMonitor.viewerConnectionCount){activeMonitor.viewerConnectionCount=0};
|
||||
|
@ -138,7 +155,7 @@ module.exports = function(s,config,lang){
|
|||
return x.ar;
|
||||
}
|
||||
s.getStreamsDirectory = (monitor) => {
|
||||
return s.dir.streams + monitor.ke + '/' + monitor.mid + '/'
|
||||
return s.dir.streams + monitor.ke + '/' + (monitor.mid || monitor.id) + '/'
|
||||
}
|
||||
s.getRawSnapshotFromMonitor = function(monitor,options){
|
||||
return new Promise((resolve,reject) => {
|
||||
|
@ -185,7 +202,7 @@ module.exports = function(s,config,lang){
|
|||
var snapBuffer = []
|
||||
var temporaryImageFile = streamDir + s.gid(5) + '.jpg'
|
||||
var iconImageFile = streamDir + 'icon.jpg'
|
||||
var ffmpegCmd = splitForFFPMEG(`-loglevel warning -re -probesize 100000 -analyzeduration 100000 ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -vf "fps=1" -vframes 1 "${temporaryImageFile}"`)
|
||||
var ffmpegCmd = splitForFFPMEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
|
||||
checkExists(streamDir, function(success) {
|
||||
if (success === false) {
|
||||
fs.mkdirSync(streamDir, {recursive: true}, (err) => {s.debugLog(err)})
|
||||
|
@ -500,20 +517,7 @@ module.exports = function(s,config,lang){
|
|||
s.checkDetails(e)
|
||||
if(e.ke && config.doSnapshot === true){
|
||||
if(s.group[e.ke] && s.group[e.ke].rawMonitorConfigurations && s.group[e.ke].rawMonitorConfigurations[e.mid] && s.group[e.ke].rawMonitorConfigurations[e.mid].mode !== 'stop'){
|
||||
if(s.group[e.ke].activeMonitors[e.mid].onvifConnection){
|
||||
const screenShot = await s.getSnapshotFromOnvif({
|
||||
username: onvifUsername,
|
||||
password: onvifPassword,
|
||||
uri: cameraResponse.uri,
|
||||
});
|
||||
s.tx({
|
||||
f: 'monitor_snapshot',
|
||||
snapshot: screenShot.toString('base64'),
|
||||
snapshot_format: 'b64',
|
||||
mid: e.mid,
|
||||
ke: e.ke
|
||||
},'GRP_'+e.ke)
|
||||
}else{
|
||||
async function getRaw(){
|
||||
var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/'
|
||||
const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(s.group[e.ke].rawMonitorConfigurations[e.mid],options)
|
||||
if(screenShot){
|
||||
|
@ -527,7 +531,27 @@ module.exports = function(s,config,lang){
|
|||
}else{
|
||||
s.debugLog('Damaged Snapshot Data')
|
||||
s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke)
|
||||
}
|
||||
}
|
||||
}
|
||||
if(s.group[e.ke].activeMonitors[e.mid].onvifConnection){
|
||||
try{
|
||||
const screenShot = await s.getSnapshotFromOnvif({
|
||||
ke: e.ke,
|
||||
mid: e.mid,
|
||||
});
|
||||
s.tx({
|
||||
f: 'monitor_snapshot',
|
||||
snapshot: screenShot.toString('base64'),
|
||||
snapshot_format: 'b64',
|
||||
mid: e.mid,
|
||||
ke: e.ke
|
||||
},'GRP_'+e.ke)
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
await getRaw()
|
||||
}
|
||||
}else{
|
||||
await getRaw()
|
||||
}
|
||||
}else{
|
||||
s.tx({f:'monitor_snapshot',snapshot:'Disabled',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke)
|
||||
|
@ -587,7 +611,7 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
const createTimelapseDirectory = function(e,callback){
|
||||
var directory = s.getTimelapseFrameDirectory(e)
|
||||
fs.mkdir(directory,function(err){
|
||||
fs.mkdir(directory,{ recursive: true },function(err){
|
||||
s.handleFolderError(err)
|
||||
callback(err,directory)
|
||||
})
|
||||
|
@ -752,55 +776,8 @@ module.exports = function(s,config,lang){
|
|||
code: e.wantedStatusCode
|
||||
});
|
||||
//on unexpected exit restart
|
||||
s.group[e.ke].activeMonitors[e.id].spawn_exit = function(){
|
||||
if(s.group[e.ke].activeMonitors[e.id].isStarted === true){
|
||||
if(e.details.loglevel!=='quiet'){
|
||||
s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang.unexpectedExitText,cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}});
|
||||
}
|
||||
fatalError(e,'Process Unexpected Exit');
|
||||
scanForOrphanedVideos(e,{
|
||||
forceCheck: true,
|
||||
checkMax: 2
|
||||
})
|
||||
s.onMonitorUnexpectedExitExtensions.forEach(function(extender){
|
||||
extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e)
|
||||
})
|
||||
}
|
||||
}
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.on('end',s.group[e.ke].activeMonitors[e.id].spawn_exit)
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.on('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit)
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.on('error',function(er){
|
||||
s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error')
|
||||
})
|
||||
s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}})
|
||||
if(s.isWin === false){
|
||||
var strippedHost = s.stripAuthFromHost(e)
|
||||
var sendProcessCpuUsage = function(){
|
||||
s.getMonitorCpuUsage(e,function(percent){
|
||||
s.group[e.ke].activeMonitors[e.id].currentCpuUsage = percent
|
||||
s.tx({
|
||||
f: 'camera_cpu_usage',
|
||||
ke: e.ke,
|
||||
id: e.id,
|
||||
percent: percent
|
||||
},'MON_STREAM_'+e.ke+e.id)
|
||||
})
|
||||
}
|
||||
clearInterval(s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage)
|
||||
s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage = setInterval(function(){
|
||||
if(e.details.skip_ping !== '1'){
|
||||
connectionTester.test(strippedHost,e.port,2000,function(err,response){
|
||||
if(response.success){
|
||||
sendProcessCpuUsage()
|
||||
}else{
|
||||
launchMonitorProcesses(e)
|
||||
}
|
||||
})
|
||||
}else{
|
||||
sendProcessCpuUsage()
|
||||
}
|
||||
},1000 * 60)
|
||||
}
|
||||
if(s.group[e.ke].activeMonitors[e.id].spawn)attachMainProcessHandlers(e,fatalError)
|
||||
return s.group[e.ke].activeMonitors[e.id].spawn
|
||||
}
|
||||
const createEventCounter = function(monitor){
|
||||
if(monitor.details.detector_obj_count === '1'){
|
||||
|
@ -923,42 +900,16 @@ module.exports = function(s,config,lang){
|
|||
//frames from motion detect
|
||||
if(e.details.detector_pam === '1'){
|
||||
// s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].pipe(s.group[e.ke].activeMonitors[e.id].p2p).pipe(s.group[e.ke].activeMonitors[e.id].pamDiff)
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.stdio[3].on('data',function(buf){
|
||||
let theJson
|
||||
try{
|
||||
buf.toString().split('}{').forEach((object,n)=>{
|
||||
theJson = object
|
||||
if(object.substr(object.length - 1) !== '}')theJson += '}'
|
||||
if(object.substr(0,1) !== '{')theJson = '{' + theJson
|
||||
try{
|
||||
var data = JSON.parse(theJson)
|
||||
}catch(err){
|
||||
var data = JSON.parse(theJson + '}')
|
||||
}
|
||||
switch(data.f){
|
||||
case'trigger':
|
||||
triggerEvent(data)
|
||||
break;
|
||||
case's.tx':
|
||||
s.tx(data.data,data.to)
|
||||
break;
|
||||
}
|
||||
})
|
||||
}catch(err){
|
||||
console.log(theJson)
|
||||
console.log('There was an error parsing a detector event')
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
// spawn.stdio[3] is deprecated and now motion events are handled by dataPort
|
||||
if(e.details.detector_use_detect_object === '1' && e.details.detector_use_motion === '1' ){
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){
|
||||
onDetectorJpegOutputSecondary(e,data)
|
||||
})
|
||||
}else{
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){
|
||||
onDetectorJpegOutputAlone(e,data)
|
||||
})
|
||||
}
|
||||
}
|
||||
}else if(e.details.detector_use_detect_object === '1' && e.details.detector_send_frames !== '1'){
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.stdio[4].on('data',function(data){
|
||||
onDetectorJpegOutputSecondary(e,data)
|
||||
|
@ -970,8 +921,9 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
}
|
||||
//frames to stream
|
||||
var frameToStreamPrimary
|
||||
switch(e.details.stream_type){
|
||||
var frameToStreamPrimary;
|
||||
const streamType = e.details.stream_type;
|
||||
switch(streamType){
|
||||
case'mp4':
|
||||
delete(s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'])
|
||||
if(!s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'])s.group[e.ke].activeMonitors[e.id].mp4frag['MAIN'] = new Mp4Frag()
|
||||
|
@ -996,12 +948,6 @@ module.exports = function(s,config,lang){
|
|||
s.group[e.ke].activeMonitors[e.id].emitter.emit('data',d)
|
||||
}
|
||||
break;
|
||||
case'h265':
|
||||
frameToStreamPrimary = function(d){
|
||||
resetStreamCheck(e)
|
||||
s.group[e.ke].activeMonitors[e.id].emitter.emit('data',d)
|
||||
}
|
||||
break;
|
||||
case'b64':case undefined:case null:case'':
|
||||
var buffer
|
||||
frameToStreamPrimary = function(d){
|
||||
|
@ -1018,47 +964,22 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
break;
|
||||
}
|
||||
s.onMonitorCreateStreamPipeExtensions.forEach(function(extender){
|
||||
if(!frameToStreamPrimary)frameToStreamPrimary = extender(streamType,e,resetStreamCheck)
|
||||
});
|
||||
if(frameToStreamPrimary){
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.stdout.on('data',frameToStreamPrimary)
|
||||
}
|
||||
if(e.details.stream_channels && e.details.stream_channels !== ''){
|
||||
var createStreamEmitter = function(channel,number){
|
||||
var pipeNumber = number+config.pipeAddition;
|
||||
if(!s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber]){
|
||||
s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber] = new events.EventEmitter().setMaxListeners(0);
|
||||
}
|
||||
var frameToStreamAdded
|
||||
switch(channel.stream_type){
|
||||
case'mp4':
|
||||
delete(s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber])
|
||||
if(!s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber])s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber] = new Mp4Frag();
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.stdio[pipeNumber].pipe(s.group[e.ke].activeMonitors[e.id].mp4frag[pipeNumber],{ end: false })
|
||||
break;
|
||||
case'mjpeg':
|
||||
frameToStreamAdded = function(d){
|
||||
s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d)
|
||||
}
|
||||
break;
|
||||
case'flv':
|
||||
frameToStreamAdded = function(d){
|
||||
if(!s.group[e.ke].activeMonitors[e.id].firstStreamChunk[pipeNumber])s.group[e.ke].activeMonitors[e.id].firstStreamChunk[pipeNumber] = d;
|
||||
frameToStreamAdded = function(d){
|
||||
s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d)
|
||||
}
|
||||
frameToStreamAdded(d)
|
||||
}
|
||||
break;
|
||||
case'h264':
|
||||
frameToStreamAdded = function(d){
|
||||
s.group[e.ke].activeMonitors[e.id].emitterChannel[pipeNumber].emit('data',d)
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(frameToStreamAdded){
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.stdio[pipeNumber].on('data',frameToStreamAdded)
|
||||
}
|
||||
}
|
||||
e.details.stream_channels.forEach(createStreamEmitter)
|
||||
e.details.stream_channels.forEach((fields,number) => {
|
||||
attachStreamChannelHandlers({
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
fields: fields,
|
||||
number: number,
|
||||
ffmpegProcess: s.group[e.ke].activeMonitors[e.id].spawn,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
const catchNewSegmentNames = function(e){
|
||||
|
@ -1158,11 +1079,11 @@ module.exports = function(s,config,lang){
|
|||
s.group[e.ke].activeMonitors[monitorId].detector_notrigger_webhook = s.createTimeout('detector_notrigger_webhook',s.group[e.ke].activeMonitors[monitorId],currentConfig.detector_notrigger_webhook_timeout,10)
|
||||
var detector_notrigger_webhook_url = addEventDetailsToString(e,currentConfig.detector_notrigger_webhook_url)
|
||||
var webhookMethod = currentConfig.detector_notrigger_webhook_method
|
||||
if(!webhookMethod || webhookMethod === '')webhookMethod = 'GET'
|
||||
request(detector_notrigger_webhook_url,{method: webhookMethod,encoding:null},function(err,data){
|
||||
if(err){
|
||||
s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}})
|
||||
}
|
||||
if(!webhookMethod || webhookMethod === '')webhookMethod = 'GET';
|
||||
fetchTimeout(detector_notrigger_webhook_url,10000,{
|
||||
method: webhookMethod
|
||||
}).catch((err) => {
|
||||
s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}})
|
||||
})
|
||||
}
|
||||
if(currentConfig.detector_notrigger_command_enable === '1' && !s.group[e.ke].activeMonitors[monitorId].detector_notrigger_command){
|
||||
|
@ -1278,27 +1199,29 @@ module.exports = function(s,config,lang){
|
|||
if(pingResponse.success === true){
|
||||
activeMonitor.isRecording = true
|
||||
try{
|
||||
createCameraFfmpegProcess(e)
|
||||
createCameraStreamHandlers(e)
|
||||
var mainProcess = createCameraFfmpegProcess(e)
|
||||
createEventCounter(e)
|
||||
if(e.type === 'dashcam' || e.type === 'socket'){
|
||||
setTimeout(function(){
|
||||
activeMonitor.allowStdinWrite = true
|
||||
s.txToDashcamUsers({
|
||||
f : 'enable_stream',
|
||||
ke : e.ke,
|
||||
mid : e.id
|
||||
},e.ke)
|
||||
},30000)
|
||||
}
|
||||
if(
|
||||
e.functionMode === 'record' ||
|
||||
e.type === 'mjpeg' ||
|
||||
e.type === 'h264' ||
|
||||
e.type === 'local'
|
||||
){
|
||||
catchNewSegmentNames(e)
|
||||
cameraFilterFfmpegLog(e)
|
||||
if(mainProcess){
|
||||
createCameraStreamHandlers(e)
|
||||
if(e.type === 'dashcam' || e.type === 'socket'){
|
||||
setTimeout(function(){
|
||||
activeMonitor.allowStdinWrite = true
|
||||
s.txToDashcamUsers({
|
||||
f : 'enable_stream',
|
||||
ke : e.ke,
|
||||
mid : e.id
|
||||
},e.ke)
|
||||
},30000)
|
||||
}
|
||||
if(
|
||||
e.functionMode === 'record' ||
|
||||
e.type === 'mjpeg' ||
|
||||
e.type === 'h264' ||
|
||||
e.type === 'local'
|
||||
){
|
||||
catchNewSegmentNames(e)
|
||||
cameraFilterFfmpegLog(e)
|
||||
}
|
||||
}
|
||||
clearTimeout(activeMonitor.onMonitorStartTimer)
|
||||
activeMonitor.onMonitorStartTimer = setTimeout(() => {
|
||||
|
@ -1349,6 +1272,8 @@ module.exports = function(s,config,lang){
|
|||
//data, options
|
||||
d : s.group[e.ke].rawMonitorConfigurations[e.id]
|
||||
},activeMonitor.childNodeId)
|
||||
clearTimeout(activeMonitor.recordingChecker);
|
||||
clearTimeout(activeMonitor.streamChecker);
|
||||
}
|
||||
if(
|
||||
e.type !== 'socket' &&
|
||||
|
@ -1364,37 +1289,21 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
try{
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){
|
||||
var copiedMonitorObject = s.cleanMonitorObject(s.group[e.ke].rawMonitorConfigurations[e.id])
|
||||
var childNodeList = Object.keys(s.childNodes)
|
||||
if(childNodeList.length > 0){
|
||||
e.childNodeFound = false
|
||||
var selectNode = function(ip){
|
||||
e.childNodeFound = true
|
||||
e.childNodeSelected = ip
|
||||
}
|
||||
var nodeWithLowestActiveCamerasCount = 65535
|
||||
var nodeWithLowestActiveCameras = null
|
||||
childNodeList.forEach(function(ip){
|
||||
delete(s.childNodes[ip].activeCameras[e.ke+e.id])
|
||||
var nodeCameraCount = Object.keys(s.childNodes[ip].activeCameras).length
|
||||
if(!s.childNodes[ip].dead && nodeCameraCount < nodeWithLowestActiveCamerasCount && s.childNodes[ip].cpu < 75){
|
||||
nodeWithLowestActiveCamerasCount = nodeCameraCount
|
||||
nodeWithLowestActiveCameras = ip
|
||||
}
|
||||
})
|
||||
if(nodeWithLowestActiveCameras)selectNode(nodeWithLowestActiveCameras)
|
||||
if(e.childNodeFound === true){
|
||||
s.childNodes[e.childNodeSelected].activeCameras[e.ke+e.id] = copiedMonitorObject
|
||||
activeMonitor.childNode = e.childNodeSelected
|
||||
activeMonitor.childNodeId = s.childNodes[e.childNodeSelected].cnid;
|
||||
s.cx({f:'sync',sync:s.group[e.ke].rawMonitorConfigurations[e.id],ke:e.ke,mid:e.id},activeMonitor.childNodeId);
|
||||
selectNodeForOperation({
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
}).then((selectedNode) => {
|
||||
if(selectedNode){
|
||||
bindMonitorToChildNode({
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
childNodeId: selectedNode,
|
||||
})
|
||||
doOnChildMachine()
|
||||
}else{
|
||||
startMonitorInQueue.push(doOnThisMachine,function(){})
|
||||
}
|
||||
}else{
|
||||
startMonitorInQueue.push(doOnThisMachine,function(){})
|
||||
}
|
||||
});
|
||||
}else{
|
||||
startMonitorInQueue.push(doOnThisMachine,function(){})
|
||||
}
|
||||
|
@ -1568,26 +1477,23 @@ module.exports = function(s,config,lang){
|
|||
s.initiateMonitorObject({ke:e.ke,mid:e.id})
|
||||
switch(e.functionMode){
|
||||
case'watch_on'://live streamers - join
|
||||
if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}}
|
||||
if(!cn.monitorsCurrentlyWatching[e.id]){cn.monitorsCurrentlyWatching[e.id]={ke:e.ke}}
|
||||
s.group[e.ke].activeMonitors[e.id].watch[cn.id]={};
|
||||
var numberOfViewers = Object.keys(s.group[e.ke].activeMonitors[e.id].watch).length
|
||||
s.tx({
|
||||
viewers: numberOfViewers,
|
||||
ke: e.ke,
|
||||
id: e.id
|
||||
},'MON_'+e.ke+e.id)
|
||||
if(!cn.monitorsCurrentlyWatching){cn.monitorsCurrentlyWatching = {}}
|
||||
if(!cn.monitorsCurrentlyWatching[e.id]){cn.monitorsCurrentlyWatching[e.id]={ke:e.ke}}
|
||||
setActiveViewer(e.ke,e.id,cn.id,true)
|
||||
s.group[e.ke].activeMonitors[e.id].allowDestroySubstream = false
|
||||
clearTimeout(s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream)
|
||||
break;
|
||||
case'watch_off'://live streamers - leave
|
||||
if(cn.monitorsCurrentlyWatching){delete(cn.monitorsCurrentlyWatching[e.id])}
|
||||
var numberOfViewers = 0
|
||||
delete(s.group[e.ke].activeMonitors[e.id].watch[cn.id]);
|
||||
numberOfViewers = Object.keys(s.group[e.ke].activeMonitors[e.id].watch).length
|
||||
s.tx({
|
||||
viewers: numberOfViewers,
|
||||
ke: e.ke,
|
||||
id: e.id
|
||||
},'MON_'+e.ke+e.id)
|
||||
setActiveViewer(e.ke,e.id,cn.id,false)
|
||||
clearTimeout(s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream)
|
||||
s.group[e.ke].activeMonitors[e.id].noViewerCountDisableSubstream = setTimeout(async () => {
|
||||
let currentCount = getActiveViewerCount(e.ke,e.id)
|
||||
if(currentCount === 0 && s.group[e.ke].activeMonitors[e.id].subStreamProcess){
|
||||
s.group[e.ke].activeMonitors[e.id].allowDestroySubstream = true
|
||||
await destroySubstreamProcess(s.group[e.ke].activeMonitors[e.id])
|
||||
}
|
||||
},10000)
|
||||
break;
|
||||
case'restart'://restart monitor
|
||||
s.sendMonitorStatus({
|
||||
|
@ -1605,13 +1511,13 @@ module.exports = function(s,config,lang){
|
|||
if(!s.group[e.ke]||!s.group[e.ke].activeMonitors[e.id]){return}
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'master' && s.group[e.ke].activeMonitors[e.id].childNode && s.childNodes[s.group[e.ke].activeMonitors[e.id].childNode].activeCameras[e.ke+e.id]){
|
||||
s.group[e.ke].activeMonitors[e.id].isStarted = false
|
||||
s.cx({f:'sync',sync:s.group[e.ke].rawMonitorConfigurations[e.id],ke:e.ke,mid:e.id},s.group[e.ke].activeMonitors[e.id].childNodeId);
|
||||
s.cx({
|
||||
//function
|
||||
f : 'cameraStop',
|
||||
//data, options
|
||||
d : s.group[e.ke].rawMonitorConfigurations[e.id]
|
||||
},s.group[e.ke].activeMonitors[e.id].childNodeId)
|
||||
s.cx({f:'sync',sync:s.group[e.ke].rawMonitorConfigurations[e.id],ke:e.ke,mid:e.id},s.group[e.ke].activeMonitors[e.id].childNodeId);
|
||||
}else{
|
||||
closeEventBasedRecording(e)
|
||||
if(s.group[e.ke].activeMonitors[e.id].fswatch){s.group[e.ke].activeMonitors[e.id].fswatch.close();delete(s.group[e.ke].activeMonitors[e.id].fswatch)}
|
||||
|
@ -1653,15 +1559,17 @@ module.exports = function(s,config,lang){
|
|||
status: wantedStatus,
|
||||
code: wantedStatusCode,
|
||||
})
|
||||
setTimeout(() => {
|
||||
scanForOrphanedVideos({
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
},{
|
||||
forceCheck: true,
|
||||
checkMax: 2
|
||||
})
|
||||
},2000)
|
||||
if(isMasterNode){
|
||||
setTimeout(() => {
|
||||
scanForOrphanedVideos({
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
},{
|
||||
forceCheck: true,
|
||||
checkMax: 2
|
||||
})
|
||||
},2000)
|
||||
}
|
||||
clearTimeout(s.group[e.ke].activeMonitors[e.id].onMonitorStartTimer)
|
||||
s.onMonitorStopExtensions.forEach(function(extender){
|
||||
extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e)
|
||||
|
@ -1676,6 +1584,15 @@ module.exports = function(s,config,lang){
|
|||
//stop action, monitor already started or recording
|
||||
return
|
||||
}
|
||||
if(activeMonitor.masterSaysToStop === true){
|
||||
s.sendMonitorStatus({
|
||||
id: e.id,
|
||||
ke: e.ke,
|
||||
status: lang.Stopped,
|
||||
code: 5,
|
||||
})
|
||||
return;
|
||||
}
|
||||
if(config.probeMonitorOnStart === true){
|
||||
const probeResponse = await probeMonitor(s.group[e.ke].rawMonitorConfigurations[e.id],2000,true)
|
||||
const probeStreams = getStreamInfoFromProbe(probeResponse.result)
|
||||
|
|
|
@ -1,14 +1,30 @@
|
|||
const fs = require('fs');
|
||||
const treekill = require('tree-kill');
|
||||
const spawn = require('child_process').spawn;
|
||||
const events = require('events');
|
||||
const Mp4Frag = require('mp4frag');
|
||||
const streamViewerCountTimeouts = {}
|
||||
module.exports = (s,config,lang) => {
|
||||
const {
|
||||
scanForOrphanedVideos
|
||||
} = require('../video/utils.js')(s,config,lang)
|
||||
const {
|
||||
createPipeArray,
|
||||
splitForFFPMEG,
|
||||
sanitizedFfmpegCommand,
|
||||
} = require('../ffmpeg/utils.js')(s,config,lang)
|
||||
const {
|
||||
buildSubstreamString,
|
||||
getDefaultSubstreamFields,
|
||||
} = require('../ffmpeg/builders.js')(s,config,lang)
|
||||
const getUpdateableFields = require('./updatedFields.js')
|
||||
const processKill = (proc) => {
|
||||
const response = {ok: true}
|
||||
return new Promise((resolve,reject) => {
|
||||
if(!proc){
|
||||
resolve(response)
|
||||
return
|
||||
}
|
||||
function sendError(err){
|
||||
response.ok = false
|
||||
response.err = err
|
||||
|
@ -86,13 +102,17 @@ module.exports = (s,config,lang) => {
|
|||
if(activeMonitor.onChildNodeExit){
|
||||
activeMonitor.onChildNodeExit()
|
||||
}
|
||||
activeMonitor.spawn.stdio.forEach(function(stdio){
|
||||
try{
|
||||
stdio.unpipe()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
try{
|
||||
activeMonitor.spawn.stdio.forEach(function(stdio){
|
||||
try{
|
||||
stdio.unpipe()
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}catch(err){
|
||||
// s.debugLog(err)
|
||||
}
|
||||
if(activeMonitor.mp4frag){
|
||||
var mp4FragChannels = Object.keys(activeMonitor.mp4frag)
|
||||
mp4FragChannels.forEach(function(channel){
|
||||
|
@ -108,6 +128,10 @@ module.exports = (s,config,lang) => {
|
|||
}else{
|
||||
processKill(proc).then((response) => {
|
||||
s.debugLog(`cameraDestroy`,response)
|
||||
activeMonitor.allowDestroySubstream = true
|
||||
destroySubstreamProcess(activeMonitor).then((response) => {
|
||||
if(response.hadSubStream)s.debugLog(`cameraDestroy`,response.closeResponse)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -136,7 +160,7 @@ module.exports = (s,config,lang) => {
|
|||
})
|
||||
}
|
||||
const temporaryImageFile = streamDir + s.gid(5) + '.jpg'
|
||||
const ffmpegCmd = splitForFFPMEG(`-loglevel warning -re -stimeout 30000000 -probesize 100000 -analyzeduration 100000 ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -vf "fps=1" -vframes 1 "${temporaryImageFile}"`)
|
||||
const ffmpegCmd = splitForFFPMEG(`-y -loglevel warning -re ${inputOptions.join(' ')} -i "${url}" ${outputOptions.join(' ')} -f image2 -an -frames:v 1 "${temporaryImageFile}"`)
|
||||
const snapProcess = spawn('ffmpeg',ffmpegCmd,{detached: true})
|
||||
snapProcess.stderr.on('data',function(data){
|
||||
// s.debugLog(data.toString())
|
||||
|
@ -195,11 +219,272 @@ module.exports = (s,config,lang) => {
|
|||
}
|
||||
})
|
||||
}
|
||||
const spawnSubstreamProcess = function(e){
|
||||
// e = monitorConfig
|
||||
try{
|
||||
const groupKey = e.ke
|
||||
const monitorId = e.mid
|
||||
const monitorConfig = Object.assign({},s.group[groupKey].rawMonitorConfigurations[monitorId])
|
||||
const monitorDetails = monitorConfig.details
|
||||
const activeMonitor = s.group[e.ke].activeMonitors[e.mid]
|
||||
const channelNumber = 1 + (monitorDetails.stream_channels || []).length
|
||||
const ffmpegCommand = [`-progress pipe:5`];
|
||||
const logLevel = monitorDetails.loglevel ? e.details.loglevel : 'warning'
|
||||
const stdioPipes = createPipeArray({}, 2)
|
||||
const substreamConfig = monitorConfig.details.substream
|
||||
substreamConfig.input.type = !substreamConfig.input.fulladdress ? monitorConfig.type : substreamConfig.input.type || monitorConfig.details.rtsp_transport
|
||||
substreamConfig.input.fulladdress = substreamConfig.input.fulladdress || s.buildMonitorUrl(monitorConfig)
|
||||
substreamConfig.input.rtsp_transport = substreamConfig.input.rtsp_transport || monitorConfig.details.rtsp_transport
|
||||
const {
|
||||
inputAndConnectionFields,
|
||||
outputFields,
|
||||
} = getDefaultSubstreamFields(monitorConfig);
|
||||
([
|
||||
buildSubstreamString(channelNumber + config.pipeAddition,e),
|
||||
]).forEach(function(commandStringPart){
|
||||
ffmpegCommand.push(commandStringPart)
|
||||
});
|
||||
const ffmpegCommandString = ffmpegCommand.join(' ')
|
||||
activeMonitor.ffmpegSubstream = sanitizedFfmpegCommand(e,ffmpegCommandString)
|
||||
const ffmpegCommandParsed = splitForFFPMEG(ffmpegCommandString)
|
||||
activeMonitor.subStreamChannel = channelNumber;
|
||||
s.userLog({
|
||||
ke: e.ke,
|
||||
mid: e.mid,
|
||||
},
|
||||
{
|
||||
type: lang["Substream Process"],
|
||||
msg: {
|
||||
msg: lang["Process Started"],
|
||||
cmd: ffmpegCommandString,
|
||||
},
|
||||
});
|
||||
const subStreamProcess = spawn(config.ffmpegDir,ffmpegCommandParsed,{detached: true,stdio: stdioPipes})
|
||||
attachStreamChannelHandlers({
|
||||
ke: e.ke,
|
||||
mid: e.mid,
|
||||
fields: Object.assign({},inputAndConnectionFields,outputFields),
|
||||
number: activeMonitor.subStreamChannel,
|
||||
ffmpegProcess: subStreamProcess,
|
||||
})
|
||||
if(config.debugLog === true){
|
||||
subStreamProcess.stderr.on('data',(data) => {
|
||||
console.log(`${e.ke} ${e.mid}`)
|
||||
console.log(data.toString())
|
||||
})
|
||||
}
|
||||
if(logLevel !== 'quiet'){
|
||||
subStreamProcess.stderr.on('data',(data) => {
|
||||
s.userLog({
|
||||
ke: e.ke,
|
||||
mid: e.mid,
|
||||
},{
|
||||
type: lang["Substream Process"],
|
||||
msg: data.toString()
|
||||
})
|
||||
})
|
||||
}
|
||||
subStreamProcess.on('close',(data) => {
|
||||
if(!activeMonitor.allowDestroySubstream){
|
||||
subStreamProcess.stderr.on('data',(data) => {
|
||||
s.userLog({
|
||||
ke: e.ke,
|
||||
mid: e.mid,
|
||||
},
|
||||
{
|
||||
type: lang["Substream Process"],
|
||||
msg: lang["Process Crashed for Monitor"],
|
||||
})
|
||||
})
|
||||
setTimeout(() => {
|
||||
spawnSubstreamProcess(e)
|
||||
},2000)
|
||||
}
|
||||
})
|
||||
activeMonitor.subStreamProcess = subStreamProcess
|
||||
s.tx({
|
||||
f: 'substream_start',
|
||||
mid: e.mid,
|
||||
ke: e.ke,
|
||||
channel: activeMonitor.subStreamChannel
|
||||
},'GRP_'+e.ke);
|
||||
return subStreamProcess
|
||||
}catch(err){
|
||||
s.systemLog(err)
|
||||
return null
|
||||
}
|
||||
}
|
||||
const destroySubstreamProcess = async function(activeMonitor){
|
||||
// e = monitorConfig.details.substream
|
||||
const response = {
|
||||
hadSubStream: false,
|
||||
alreadyClosing: false
|
||||
}
|
||||
try{
|
||||
if(activeMonitor.subStreamProcessClosing){
|
||||
response.alreadyClosing = true
|
||||
}else if(activeMonitor.subStreamProcess){
|
||||
activeMonitor.subStreamProcessClosing = true
|
||||
activeMonitor.subStreamChannel = null;
|
||||
const closeResponse = await processKill(activeMonitor.subStreamProcess)
|
||||
response.hadSubStream = true
|
||||
response.closeResponse = closeResponse
|
||||
delete(activeMonitor.subStreamProcess)
|
||||
s.tx({
|
||||
f: 'substream_end',
|
||||
mid: activeMonitor.mid,
|
||||
ke: activeMonitor.ke
|
||||
},'GRP_'+activeMonitor.ke);
|
||||
activeMonitor.subStreamProcessClosing = false
|
||||
}
|
||||
}catch(err){
|
||||
s.debugLog('destroySubstreamProcess',err)
|
||||
}
|
||||
return response
|
||||
}
|
||||
function attachStreamChannelHandlers(options){
|
||||
const fields = options.fields
|
||||
const number = options.number
|
||||
const ffmpegProcess = options.ffmpegProcess
|
||||
const activeMonitor = s.group[options.ke].activeMonitors[options.mid]
|
||||
const pipeNumber = number + config.pipeAddition;
|
||||
if(!activeMonitor.emitterChannel[pipeNumber]){
|
||||
activeMonitor.emitterChannel[pipeNumber] = new events.EventEmitter().setMaxListeners(0);
|
||||
}
|
||||
let frameToStreamAdded
|
||||
switch(fields.stream_type){
|
||||
case'mp4':
|
||||
delete(activeMonitor.mp4frag[pipeNumber])
|
||||
if(!activeMonitor.mp4frag[pipeNumber])activeMonitor.mp4frag[pipeNumber] = new Mp4Frag();
|
||||
ffmpegProcess.stdio[pipeNumber].pipe(activeMonitor.mp4frag[pipeNumber],{ end: false })
|
||||
break;
|
||||
case'mjpeg':
|
||||
frameToStreamAdded = function(d){
|
||||
activeMonitor.emitterChannel[pipeNumber].emit('data',d)
|
||||
}
|
||||
break;
|
||||
case'flv':
|
||||
frameToStreamAdded = function(d){
|
||||
if(!activeMonitor.firstStreamChunk[pipeNumber])activeMonitor.firstStreamChunk[pipeNumber] = d;
|
||||
frameToStreamAdded = function(d){
|
||||
activeMonitor.emitterChannel[pipeNumber].emit('data',d)
|
||||
}
|
||||
frameToStreamAdded(d)
|
||||
}
|
||||
break;
|
||||
case'h264':
|
||||
frameToStreamAdded = function(d){
|
||||
activeMonitor.emitterChannel[pipeNumber].emit('data',d)
|
||||
}
|
||||
break;
|
||||
}
|
||||
if(frameToStreamAdded){
|
||||
ffmpegProcess.stdio[pipeNumber].on('data',frameToStreamAdded)
|
||||
}
|
||||
}
|
||||
function setActiveViewer(groupKey,monitorId,connectionId,isBeingAdded){
|
||||
const viewerList = s.group[groupKey].activeMonitors[monitorId].watch;
|
||||
if(isBeingAdded){
|
||||
if(viewerList.indexOf(connectionId) > -1)viewerList.push(connectionId);
|
||||
}else{
|
||||
viewerList.splice(viewerList.indexOf(connectionId), 1)
|
||||
}
|
||||
const numberOfViewers = viewerList.length
|
||||
s.tx({
|
||||
f: 'viewer_count',
|
||||
viewers: numberOfViewers,
|
||||
ke: groupKey,
|
||||
id: monitorId
|
||||
},'MON_' + groupKey + monitorId)
|
||||
return numberOfViewers;
|
||||
}
|
||||
function getActiveViewerCount(groupKey,monitorId){
|
||||
const viewerList = s.group[groupKey].activeMonitors[monitorId].watch;
|
||||
const numberOfViewers = viewerList.length
|
||||
return numberOfViewers;
|
||||
}
|
||||
function setTimedActiveViewerForHttp(req){
|
||||
const groupKey = req.params.ke
|
||||
const connectionId = req.params.auth
|
||||
const loggedInUser = s.group[groupKey].users[connectionId]
|
||||
if(!loggedInUser){
|
||||
const monitorId = req.params.id
|
||||
const viewerList = s.group[groupKey].activeMonitors[monitorId].watch
|
||||
const theViewer = viewerList[connectionId]
|
||||
if(!theViewer){
|
||||
setActiveViewer(groupKey,monitorId,connectionId,true)
|
||||
}
|
||||
clearTimeout(streamViewerCountTimeouts[req.originalUrl])
|
||||
streamViewerCountTimeouts[req.originalUrl] = setTimeout(() => {
|
||||
setActiveViewer(groupKey,monitorId,connectionId,false)
|
||||
},5000)
|
||||
}else{
|
||||
s.debugLog(`User is Logged in, Don't add to viewer count`);
|
||||
}
|
||||
}
|
||||
function attachMainProcessHandlers(e,fatalError){
|
||||
s.group[e.ke].activeMonitors[e.id].spawn_exit = function(){
|
||||
if(s.group[e.ke].activeMonitors[e.id].isStarted === true){
|
||||
if(e.details.loglevel!=='quiet'){
|
||||
s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang.unexpectedExitText,cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}});
|
||||
}
|
||||
fatalError(e,'Process Unexpected Exit');
|
||||
scanForOrphanedVideos(e,{
|
||||
forceCheck: true,
|
||||
checkMax: 2
|
||||
})
|
||||
s.onMonitorUnexpectedExitExtensions.forEach(function(extender){
|
||||
extender(Object.assign(s.group[e.ke].rawMonitorConfigurations[e.id],{}),e)
|
||||
})
|
||||
}
|
||||
}
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.on('end',s.group[e.ke].activeMonitors[e.id].spawn_exit)
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.on('exit',s.group[e.ke].activeMonitors[e.id].spawn_exit)
|
||||
s.group[e.ke].activeMonitors[e.id].spawn.on('error',function(er){
|
||||
s.userLog(e,{type:'Spawn Error',msg:er});fatalError(e,'Spawn Error')
|
||||
})
|
||||
s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].activeMonitors[e.id].ffmpeg}})
|
||||
// if(s.isWin === false){
|
||||
// var strippedHost = s.stripAuthFromHost(e)
|
||||
// var sendProcessCpuUsage = function(){
|
||||
// s.getMonitorCpuUsage(e,function(percent){
|
||||
// s.group[e.ke].activeMonitors[e.id].currentCpuUsage = percent
|
||||
// s.tx({
|
||||
// f: 'camera_cpu_usage',
|
||||
// ke: e.ke,
|
||||
// id: e.id,
|
||||
// percent: percent
|
||||
// },'MON_STREAM_'+e.ke+e.id)
|
||||
// })
|
||||
// }
|
||||
// clearInterval(s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage)
|
||||
// s.group[e.ke].activeMonitors[e.id].getMonitorCpuUsage = setInterval(function(){
|
||||
// if(e.details.skip_ping !== '1'){
|
||||
// connectionTester.test(strippedHost,e.port,2000,function(err,response){
|
||||
// if(response.success){
|
||||
// sendProcessCpuUsage()
|
||||
// }else{
|
||||
// launchMonitorProcesses(e)
|
||||
// }
|
||||
// })
|
||||
// }else{
|
||||
// sendProcessCpuUsage()
|
||||
// }
|
||||
// },1000 * 60)
|
||||
// }
|
||||
}
|
||||
return {
|
||||
cameraDestroy: cameraDestroy,
|
||||
createSnapshot: createSnapshot,
|
||||
processKill: processKill,
|
||||
addCredentialsToStreamLink: addCredentialsToStreamLink,
|
||||
monitorConfigurationMigrator: monitorConfigurationMigrator,
|
||||
spawnSubstreamProcess: spawnSubstreamProcess,
|
||||
destroySubstreamProcess: destroySubstreamProcess,
|
||||
attachStreamChannelHandlers: attachStreamChannelHandlers,
|
||||
setActiveViewer: setActiveViewer,
|
||||
getActiveViewerCount: getActiveViewerCount,
|
||||
setTimedActiveViewerForHttp: setTimedActiveViewerForHttp,
|
||||
attachMainProcessHandlers: attachMainProcessHandlers,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,8 +10,18 @@ module.exports = function(s,config,lang){
|
|||
d.screenshotBuffer = screenShot
|
||||
}
|
||||
}
|
||||
require('./notifications/email.js')(s,config,lang,getSnapshot)
|
||||
if(
|
||||
config.mail &&
|
||||
config.mail.auth &&
|
||||
config.mail.auth.user !== 'your_email@gmail.com' &&
|
||||
config.mail.auth.pass !== 'your_password_or_app_specific_password'
|
||||
){
|
||||
require('./notifications/email.js')(s,config,lang,getSnapshot)
|
||||
}
|
||||
require('./notifications/emailByUser.js')(s,config,lang,getSnapshot)
|
||||
require('./notifications/discordBot.js')(s,config,lang,getSnapshot)
|
||||
require('./notifications/telegram.js')(s,config,lang,getSnapshot)
|
||||
require('./notifications/pushover.js')(s,config,lang,getSnapshot)
|
||||
require('./notifications/webhook.js')(s,config,lang,getSnapshot)
|
||||
require('./notifications/mqtt.js')(s,config,lang,getSnapshot)
|
||||
}
|
||||
|
|
|
@ -48,14 +48,14 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
}
|
||||
}
|
||||
const onEventTriggerBeforeFilterForDiscord = function(d,filter){
|
||||
filter.discord = true
|
||||
filter.discord = false
|
||||
}
|
||||
const onEventTriggerForDiscord = async (d,filter) => {
|
||||
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id]
|
||||
// d = event object
|
||||
//discord bot
|
||||
const isEnabled = monitorConfig.details.detector_discordbot === '1' || monitorConfig.details.notify_discord === '1'
|
||||
if(filter.discord && s.group[d.ke].discordBot && isEnabled && !s.group[d.ke].activeMonitors[d.id].detector_discordbot){
|
||||
const isEnabled = filter.discord || monitorConfig.details.detector_discordbot === '1' || monitorConfig.details.notify_discord === '1'
|
||||
if(s.group[d.ke].discordBot && isEnabled && !s.group[d.ke].activeMonitors[d.id].detector_discordbot){
|
||||
var detector_discordbot_timeout
|
||||
if(!monitorConfig.details.detector_discordbot_timeout||monitorConfig.details.detector_discordbot_timeout===''){
|
||||
detector_discordbot_timeout = 1000 * 60 * 10;
|
||||
|
@ -366,6 +366,25 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
}
|
||||
]
|
||||
}
|
||||
s.definitions["Event Filters"].blocks["Action for Selected"].info.push({
|
||||
"name": "actions=discord",
|
||||
"field": lang['Discord'],
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"default": "",
|
||||
"example": "1",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang['Original Choice'],
|
||||
"value": "",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1",
|
||||
}
|
||||
]
|
||||
})
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
console.log('Could not start Discord bot, please run "npm install discord.js" inside the Shinobi folder.')
|
||||
|
|
|
@ -29,22 +29,22 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
]
|
||||
},(err,r) => {
|
||||
r = r[0]
|
||||
var mailOptions = {
|
||||
from: config.mail.from, // sender address
|
||||
to: checkEmail(r.mail), // list of receivers
|
||||
subject: lang.NoMotionEmailText1+' '+e.name+' ('+e.id+')', // Subject line
|
||||
html: '<i>'+lang.NoMotionEmailText2+' ' + (e.details.detector_notrigger_timeout || 10) + ' '+lang.minutes+'.</i>',
|
||||
var mailOptions = {
|
||||
from: config.mail.from, // sender address
|
||||
to: checkEmail(r.mail), // list of receivers
|
||||
subject: lang.NoMotionEmailText1+' '+e.name+' ('+e.id+')', // Subject line
|
||||
html: '<i>'+lang.NoMotionEmailText2+' ' + (e.details.detector_notrigger_timeout || 10) + ' '+lang.minutes+'.</i>',
|
||||
}
|
||||
mailOptions.html+='<div><b>'+lang['Monitor Name']+' </b> : '+e.name+'</div>'
|
||||
mailOptions.html+='<div><b>'+lang['Monitor ID']+' </b> : '+e.id+'</div>'
|
||||
sendMessage(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
s.systemLog('detector:notrigger:sendMail',error)
|
||||
s.tx({f:'error',ff:'detector_notrigger_mail',id:e.id,ke:e.ke,error:error},'GRP_'+e.ke);
|
||||
return ;
|
||||
}
|
||||
mailOptions.html+='<div><b>'+lang['Monitor Name']+' </b> : '+e.name+'</div>'
|
||||
mailOptions.html+='<div><b>'+lang['Monitor ID']+' </b> : '+e.id+'</div>'
|
||||
sendMessage(mailOptions, (error, info) => {
|
||||
if (error) {
|
||||
s.systemLog('detector:notrigger:sendMail',error)
|
||||
s.tx({f:'error',ff:'detector_notrigger_mail',id:e.id,ke:e.ke,error:error},'GRP_'+e.ke);
|
||||
return ;
|
||||
}
|
||||
s.tx({f:'detector_notrigger_mail',id:e.id,ke:e.ke,info:info},'GRP_'+e.ke);
|
||||
})
|
||||
s.tx({f:'detector_notrigger_mail',id:e.id,ke:e.ke,info:info},'GRP_'+e.ke);
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -94,16 +94,11 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
}
|
||||
}
|
||||
const onEventTriggerBeforeFilterForEmail = function(d,filter){
|
||||
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id]
|
||||
if(monitorConfig.details.detector_mail === '1'){
|
||||
filter.mail = true
|
||||
}else{
|
||||
filter.mail = false
|
||||
}
|
||||
filter.mail = false
|
||||
}
|
||||
const onEventTriggerForEmail = async (d,filter) => {
|
||||
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id]
|
||||
if(filter.mail && config.mail && !s.group[d.ke].activeMonitors[d.id].detector_mail){
|
||||
if((filter.mail || monitorConfig.details.detector_mail === '1') && config.mail && !s.group[d.ke].activeMonitors[d.id].detector_mail){
|
||||
s.knexQuery({
|
||||
action: "select",
|
||||
columns: "mail",
|
||||
|
@ -245,6 +240,112 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
s.onFilterEvent(onFilterEventForEmail)
|
||||
s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeoutForEmail)
|
||||
s.onMonitorUnexpectedExit(onMonitorUnexpectedExitForEmail)
|
||||
s.definitions['Account Settings'].blocks['2-Factor Authentication'].info.push( {
|
||||
"name": "detail=factor_mail",
|
||||
"field": `${lang.Email} (${lang['System Level']})`,
|
||||
"description": "Send 2-Factor Authentication codes to the email address of the account.",
|
||||
"default": "1",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
});
|
||||
s.definitions["Event Filters"].blocks["Action for Selected"].info.push( {
|
||||
"name": "actions=mail",
|
||||
"field": `${lang['Email on Trigger']} (${lang['System Level']})`,
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"default": "",
|
||||
"example": "1",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang['Original Choice'],
|
||||
"value": "",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1",
|
||||
}
|
||||
]
|
||||
})
|
||||
s.definitions['Monitor Settings'].blocks['Notifications'].info[0].info.push(
|
||||
{
|
||||
"name": "detail=notify_email",
|
||||
"field": `${lang.Email} (${lang['System Level']})`,
|
||||
"default": "0",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
s.definitions['Monitor Settings'].blocks['Notifications'].info.push(
|
||||
{
|
||||
isFormGroupGroup: true,
|
||||
name: `${lang.Email} (${lang['System Level']})`,
|
||||
color: 'blue',
|
||||
'section-class': 'h_det_input h_det_1',
|
||||
info: [
|
||||
{
|
||||
"name": "detail=detector_mail",
|
||||
"field": lang['Email on Trigger'],
|
||||
"description": "Recieve an email of an image during a motion event to the master account for the camera group. You must setup SMTP details in conf.json.",
|
||||
"default": "0",
|
||||
"selector": "h_det_email",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail=detector_mail_timeout",
|
||||
"field": lang['Allow Next Email'],
|
||||
"description": "The amount of time until a trigger is allowed to send another email with motion details and another image.",
|
||||
"default": "10",
|
||||
},
|
||||
{
|
||||
"name": "detail=detector_notrigger_mail",
|
||||
"field": lang['No Trigger'],
|
||||
"description": "If motion has not been detected after the timeout period you will recieve an email.",
|
||||
"default": "0",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,404 @@
|
|||
var fs = require('fs');
|
||||
const {
|
||||
template,
|
||||
checkEmail,
|
||||
} = require("./emailUtils.js")
|
||||
module.exports = function (s, config, lang, getSnapshot) {
|
||||
const { getEventBasedRecordingUponCompletion } = require('../events/utils.js')(s, config, lang);
|
||||
const nodeMailer = require('nodemailer');
|
||||
try {
|
||||
const sendMessage = async function (sendBody, files, groupKey) {
|
||||
const transporter = s.group[groupKey].emailClient;
|
||||
if (!transporter) {
|
||||
s.userLog(
|
||||
{ ke: groupKey, mid: '$USER' },
|
||||
{
|
||||
type: lang.NotifyErrorText,
|
||||
msg: {
|
||||
msg: lang.AppNotEnabledText,
|
||||
app: lang.Email
|
||||
},
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const emailClientOptions = s.group[groupKey].emailClientOptions;
|
||||
const appOptions = emailClientOptions.transport;
|
||||
const sendTo = emailClientOptions.sendTo;
|
||||
sendTo.forEach((reciepientAddress) => {
|
||||
const sendData = {
|
||||
from: `"${config.mailFromName || 'shinobi.video'}" <${appOptions.auth.user}>`,
|
||||
to: reciepientAddress,
|
||||
subject: sendBody.subject,
|
||||
html: sendBody.html,
|
||||
attachments: files || []
|
||||
};
|
||||
transporter.sendMail(sendData, function (err, result) {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
s.userLog(result);
|
||||
s.debugLog(result);
|
||||
});
|
||||
})
|
||||
console.log(sendBody)
|
||||
} catch (err) {
|
||||
s.debugLog(err)
|
||||
s.userLog(
|
||||
{ ke: groupKey, mid: '$USER' },
|
||||
{ type: lang.NotifyErrorText, msg: err }
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const loadAppForUser = function (user) {
|
||||
const userDetails = s.parseJSON(user.details);
|
||||
const optionsHost = userDetails.emailClient_host
|
||||
const optionsUser = userDetails.emailClient_user
|
||||
const optionsSendTo = userDetails.emailClient_sendTo || ''
|
||||
if (
|
||||
!s.group[user.ke].emailClient &&
|
||||
userDetails.emailClient === '1' &&
|
||||
optionsHost &&
|
||||
optionsUser &&
|
||||
optionsSendTo
|
||||
){
|
||||
const optionsPass = userDetails.emailClient_pass || ''
|
||||
const optionsSecure = userDetails.emailClient_secure === '1' ? true : false
|
||||
const optionsPort = isNaN(userDetails.emailClient_port) ? (optionsSecure ? 465 : 587) : parseInt(userDetails.emailClient_port)
|
||||
const clientOptions = {
|
||||
host: optionsHost,
|
||||
port: optionsPort,
|
||||
secure: optionsSecure,
|
||||
auth: {
|
||||
user: optionsUser,
|
||||
pass: optionsPass
|
||||
}
|
||||
}
|
||||
s.group[user.ke].emailClientOptions = {
|
||||
transport: clientOptions,
|
||||
sendTo: optionsSendTo.split(',').map((text) => {return text.trim()}),
|
||||
}
|
||||
s.group[user.ke].emailClient = nodeMailer.createTransport(clientOptions)
|
||||
}
|
||||
};
|
||||
|
||||
const unloadAppForUser = function (user) {
|
||||
if (
|
||||
s.group[user.ke].emailClient &&
|
||||
s.group[user.ke].emailClient.close
|
||||
) {
|
||||
s.group[user.ke].emailClient.close();
|
||||
}
|
||||
delete s.group[user.ke].emailClient;
|
||||
delete s.group[user.ke].emailClientOptions;
|
||||
};
|
||||
|
||||
const onTwoFactorAuthCodeNotificationForApp = function (r) {
|
||||
// r = user
|
||||
if (r.details.factor_emailClient === '1') {
|
||||
sendMessage({
|
||||
subject: r.lang['2-Factor Authentication'],
|
||||
html: template.createFramework({
|
||||
title: r.lang['2-Factor Authentication'],
|
||||
subtitle: r.lang['Enter this code to proceed'],
|
||||
body: '<b style="font-size: 20pt;">'+s.factorAuth[r.ke][r.uid].key+'</b><br><br>'+r.lang.FactorAuthText1,
|
||||
}),
|
||||
},[],r.ke);
|
||||
}
|
||||
};
|
||||
|
||||
const onEventTriggerForApp = async (d, filter) => {
|
||||
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id];
|
||||
// d = event object
|
||||
if (
|
||||
s.group[d.ke].emailClient &&
|
||||
(filter.emailClient || monitorConfig.details.notify_emailClient === '1') &&
|
||||
!s.group[d.ke].activeMonitors[d.id].detector_emailClient
|
||||
) {
|
||||
var detector_emailClient_timeout;
|
||||
if (!monitorConfig.details.detector_emailClient_timeout){
|
||||
detector_emailClient_timeout = 1000 * 60 * 10
|
||||
}else{
|
||||
detector_emailClient_timeout = parseFloat(monitorConfig.details.detector_emailClient_timeout) * 1000 * 60
|
||||
}
|
||||
s.group[d.ke].activeMonitors[d.id].detector_emailClient = setTimeout(function () {
|
||||
s.group[d.ke].activeMonitors[d.id].detector_emailClient = null;
|
||||
}, detector_emailClient_timeout);
|
||||
|
||||
// lock passed
|
||||
const sendMail = function(files){
|
||||
const infoRows = []
|
||||
Object.keys(d.details).forEach(function(key){
|
||||
var value = d.details[key]
|
||||
var text = value
|
||||
if(value instanceof Object){
|
||||
text = JSON.stringify(value,null,3)
|
||||
}
|
||||
infoRows.push(template.createRow({
|
||||
title: key,
|
||||
text: text
|
||||
}))
|
||||
})
|
||||
sendMessage({
|
||||
subject: lang.Event+' - '+d.screenshotName,
|
||||
html: template.createFramework({
|
||||
title: lang.EventText1 + ' ' + d.currentTimestamp,
|
||||
subtitle: lang.Event,
|
||||
body: infoRows.join(''),
|
||||
}),
|
||||
},files || [],d.ke)
|
||||
}
|
||||
if(monitorConfig.details.detector_mail_send_video === '1'){
|
||||
let videoPath = null
|
||||
let videoName = null
|
||||
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
|
||||
ke: d.ke,
|
||||
mid: d.mid
|
||||
})
|
||||
if(eventBasedRecording.filePath){
|
||||
videoPath = eventBasedRecording.filePath
|
||||
videoName = eventBasedRecording.filename
|
||||
}else{
|
||||
const siftedVideoFileFromRam = await s.mergeDetectorBufferChunks(d)
|
||||
videoPath = siftedVideoFileFromRam.filePath
|
||||
videoName = siftedVideoFileFromRam.filename
|
||||
}
|
||||
if(videoPath){
|
||||
fs.readFile(mergedFilepath,function(err,buffer){
|
||||
if(buffer){
|
||||
sendMail([
|
||||
{
|
||||
filename: videoName,
|
||||
content: buffer
|
||||
}
|
||||
])
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
await getSnapshot(d,monitorConfig)
|
||||
sendMail([
|
||||
{
|
||||
filename: d.screenshotName + '.jpg',
|
||||
content: d.screenshotBuffer
|
||||
}
|
||||
])
|
||||
}
|
||||
};
|
||||
|
||||
const onEventTriggerBeforeFilterForApp = function (d, filter) {
|
||||
filter.emailClient = false;
|
||||
};
|
||||
|
||||
const onDetectorNoTriggerTimeoutForApp = function (e) {
|
||||
//e = monitor object
|
||||
var currentTime = new Date();
|
||||
if (e.details.detector_notrigger_emailClient === '1') {
|
||||
var html =
|
||||
'*' +
|
||||
lang.NoMotionEmailText2 +
|
||||
' ' +
|
||||
(e.details.detector_notrigger_timeout || 10) +
|
||||
' ' +
|
||||
lang.minutes +
|
||||
'.*\n';
|
||||
html +=
|
||||
'**' + lang['Monitor Name'] + '** : ' + e.name + '\n';
|
||||
html += '**' + lang['Monitor ID'] + '** : ' + e.id + '\n';
|
||||
html += currentTime;
|
||||
sendMessage({
|
||||
subject: lang['"No Motion" Detector'],
|
||||
html: template.createFramework({
|
||||
title: lang['"No Motion" Detector'],
|
||||
subtitle: 'Shinobi Event',
|
||||
body: html,
|
||||
}),
|
||||
},[],e.ke);
|
||||
}
|
||||
};
|
||||
|
||||
const onMonitorUnexpectedExitForApp = (monitorConfig) => {
|
||||
if (
|
||||
monitorConfig.details.notify_emailClient === '1' &&
|
||||
monitorConfig.details.notify_onUnexpectedExit === '1'
|
||||
){
|
||||
const ffmpegCommand = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].ffmpeg
|
||||
const subject = lang['Process Unexpected Exit'] + ' : ' + monitorConfig.name
|
||||
const currentTime = new Date();
|
||||
sendMessage({
|
||||
subject: subject,
|
||||
html: template.createFramework({
|
||||
title: subject,
|
||||
subtitle: lang['Process Crashed for Monitor'],
|
||||
body: ffmpegCommand,
|
||||
footerText: currentTime
|
||||
}),
|
||||
},[],monitorConfig.ke);
|
||||
}
|
||||
};
|
||||
|
||||
s.loadGroupAppExtender(loadAppForUser);
|
||||
s.unloadGroupAppExtender(unloadAppForUser);
|
||||
s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotificationForApp);
|
||||
s.onEventTrigger(onEventTriggerForApp);
|
||||
s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilterForApp);
|
||||
s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeoutForApp);
|
||||
s.onMonitorUnexpectedExit(onMonitorUnexpectedExitForApp);
|
||||
s.definitions['Monitor Settings'].blocks['Notifications'].info[0].info.push({
|
||||
name: 'detail=notify_emailClient',
|
||||
field: lang.Email,
|
||||
description: '',
|
||||
default: '0',
|
||||
example: '',
|
||||
selector: 'h_det_emailClient',
|
||||
fieldType: 'select',
|
||||
possible: [
|
||||
{
|
||||
name: lang.No,
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
name: lang.Yes,
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
s.definitions['Monitor Settings'].blocks['Notifications'].info.push(
|
||||
{
|
||||
evaluation: "$user.details.use_emailClient !== '0'",
|
||||
isFormGroupGroup: true,
|
||||
name: lang.Email,
|
||||
color: 'blue',
|
||||
'section-class': 'h_det_emailClient_input h_det_emailClient_1',
|
||||
info: [
|
||||
{
|
||||
name: 'detail=detector_emailClient_timeout',
|
||||
field: `${lang['Allow Next Alert']} (${lang['on Event']})`,
|
||||
default: '10',
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
s.definitions['Account Settings'].blocks['2-Factor Authentication'].info.push({
|
||||
name: 'detail=factor_emailClient',
|
||||
field: lang.Email,
|
||||
default: '1',
|
||||
example: '',
|
||||
fieldType: 'select',
|
||||
possible: [
|
||||
{
|
||||
name: lang.No,
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
name: lang.Yes,
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
s.definitions['Account Settings'].blocks['Email'] = {
|
||||
evaluation: "$user.details.use_emailClient !== '0'",
|
||||
name: lang.Email,
|
||||
id: lang.Email,
|
||||
color: 'blue',
|
||||
info: [
|
||||
{
|
||||
name: 'detail=emailClient',
|
||||
selector: 'u_emailClient',
|
||||
field: lang.Enabled,
|
||||
default: '0',
|
||||
example: '',
|
||||
fieldType: 'select',
|
||||
possible: [
|
||||
{
|
||||
name: lang.No,
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
name: lang.Yes,
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
field: lang.Host,
|
||||
name: 'detail=emailClient_host',
|
||||
example: 'smtp.gmail.com',
|
||||
'form-group-class': 'u_emailClient_input u_emailClient_1',
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
field: lang.Port,
|
||||
name: 'detail=emailClient_port',
|
||||
example: '587',
|
||||
'form-group-class': 'u_emailClient_input u_emailClient_1',
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
name: 'detail=emailClient_secure',
|
||||
'form-group-class': 'u_emailClient_input u_emailClient_1',
|
||||
field: lang.Secure,
|
||||
default: '0',
|
||||
example: '',
|
||||
fieldType: 'select',
|
||||
possible: [
|
||||
{
|
||||
name: lang.No,
|
||||
value: '0',
|
||||
},
|
||||
{
|
||||
name: lang.Yes,
|
||||
value: '1',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
field: lang.Email,
|
||||
name: 'detail=emailClient_user',
|
||||
example: 'test@gmail.com',
|
||||
'form-group-class': 'u_emailClient_input u_emailClient_1',
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
field: lang.Password,
|
||||
fieldType: 'password',
|
||||
name: 'detail=emailClient_pass',
|
||||
'form-group-class': 'u_emailClient_input u_emailClient_1',
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
field: lang['Send to'],
|
||||
name: 'detail=emailClient_sendTo',
|
||||
example: 'testrecipient@gmail.com',
|
||||
'form-group-class': 'u_emailClient_input u_emailClient_1',
|
||||
},
|
||||
],
|
||||
};
|
||||
s.definitions["Event Filters"].blocks["Action for Selected"].info.push({
|
||||
"name": "actions=emailClient",
|
||||
"field": lang['Email'],
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"default": "",
|
||||
"example": "1",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang['Original Choice'],
|
||||
"value": "",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1",
|
||||
}
|
||||
]
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.log('Could not engage Email notifications.');
|
||||
}
|
||||
};
|
|
@ -0,0 +1,397 @@
|
|||
var fs = require("fs")
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
if(config.mqttClient === true){
|
||||
console.log('Loading MQTT Outbound Connectivity...')
|
||||
const mqtt = require('mqtt')
|
||||
const {
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
try{
|
||||
function createMqttSubscription(options){
|
||||
const mqttEndpoint = options.host
|
||||
const subKey = options.subKey
|
||||
const groupKey = options.ke
|
||||
const onData = options.onData || function(){}
|
||||
s.debugLog('Connecting.... mqtt://' + mqttEndpoint)
|
||||
const client = mqtt.connect('mqtt://' + mqttEndpoint)
|
||||
client.on('connect', function () {
|
||||
s.debugLog('Connected! mqtt://' + mqttEndpoint)
|
||||
client.subscribe(subKey, function (err) {
|
||||
if (err) {
|
||||
s.debugLog(err)
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
mid: '$USER'
|
||||
},{
|
||||
type: lang['MQTT Error'],
|
||||
msg: err
|
||||
})
|
||||
}else{
|
||||
client.on('message', function (topic, message) {
|
||||
const data = s.parseJSON(message.toString())
|
||||
onData(data)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
return client
|
||||
}
|
||||
function sendToMqttConnections(groupKey,eventName,addedArgs,checkMonitors){
|
||||
try{
|
||||
(s.group[groupKey].mqttOutbounderKeys || []).forEach(function(key){
|
||||
const outBounder = s.group[groupKey].mqttOutbounders[key]
|
||||
const theAction = outBounder.eventHandlers[eventName]
|
||||
if(!theAction)return;
|
||||
if(checkMonitors){
|
||||
const monitorsToRead = outBounder.monitorsToRead
|
||||
const firstArg = addedArgs[0]
|
||||
const monitorId = firstArg.mid || firstArg.id
|
||||
if(monitorsToRead.indexOf(monitorId) > -1 || monitorsToRead.indexOf('$all') > -1)theAction(...addedArgs);
|
||||
}else{
|
||||
theAction(...addedArgs)
|
||||
}
|
||||
})
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
}
|
||||
}
|
||||
const sendMessage = async function(options,data){
|
||||
const sendBody = s.stringJSON(data)
|
||||
const groupKey = options.ke
|
||||
const subId = options.subId
|
||||
const publishTo = options.to
|
||||
try{
|
||||
s.group[groupKey].mqttOutbounders[subId].client.publish(publishTo,sendBody)
|
||||
}catch(err){
|
||||
s.debugLog('MQTT Error',err)
|
||||
s.userLog({ke:groupKey,mid:'$USER'},{type:lang['MQTT Error'],msg:err})
|
||||
}
|
||||
}
|
||||
const onEventTriggerBeforeFilter = function(d,filter){
|
||||
filter.mqttout = false
|
||||
}
|
||||
const onDetectorNoTriggerTimeout = function(e){
|
||||
if(e.details.detector_notrigger_mqttout === '1'){
|
||||
const groupKey = e.ke
|
||||
sendToMqttConnections(groupKey,'onDetectorNoTriggerTimeout',[e],true)
|
||||
}
|
||||
}
|
||||
const onEventTrigger = (d,filter) => {
|
||||
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id]
|
||||
if((filter.mqttout || monitorConfig.details.notify_mqttout === '1') && !s.group[d.ke].activeMonitors[d.id].detector_mqttout){
|
||||
var detector_mqttout_timeout
|
||||
if(!monitorConfig.details.detector_mqttout_timeout||monitorConfig.details.detector_mqttout_timeout===''){
|
||||
detector_mqttout_timeout = 1000 * 60 * 10;
|
||||
}else{
|
||||
detector_mqttout_timeout = parseFloat(monitorConfig.details.detector_mqttout_timeout) * 1000 * 60;
|
||||
}
|
||||
s.group[d.ke].activeMonitors[d.id].detector_mqttout = setTimeout(function(){
|
||||
clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_mqttout);
|
||||
s.group[d.ke].activeMonitors[d.id].detector_mqttout = null
|
||||
},detector_mqttout_timeout)
|
||||
//
|
||||
const groupKey = d.ke
|
||||
sendToMqttConnections(groupKey,'onEventTrigger',[d,filter],true)
|
||||
}
|
||||
}
|
||||
const onMonitorSave = (monitorConfig) => {
|
||||
const groupKey = monitorConfig.ke
|
||||
sendToMqttConnections(groupKey,'onMonitorSave',[monitorConfig],true)
|
||||
}
|
||||
const onMonitorStart = (monitorConfig) => {
|
||||
const groupKey = monitorConfig.ke
|
||||
sendToMqttConnections(groupKey,'onMonitorStart',[monitorConfig],true)
|
||||
}
|
||||
const onMonitorStop = (monitorConfig) => {
|
||||
const groupKey = monitorConfig.ke
|
||||
sendToMqttConnections(groupKey,'onMonitorStop',[monitorConfig],true)
|
||||
}
|
||||
const onMonitorDied = (monitorConfig) => {
|
||||
const groupKey = monitorConfig.ke
|
||||
sendToMqttConnections(groupKey,'onMonitorDied',[monitorConfig],true)
|
||||
}
|
||||
const onAccountSave = (activeGroup,userDetails,user) => {
|
||||
const groupKey = monitorConfig.ke
|
||||
sendToMqttConnections(groupKey,'onAccountSave',[activeGroup,userDetails,user])
|
||||
}
|
||||
const onUserLog = (logEvent) => {
|
||||
const groupKey = logEvent.ke
|
||||
sendToMqttConnections(groupKey,'onUserLog',[logEvent])
|
||||
}
|
||||
const onTwoFactorAuthCodeNotification = function(user){
|
||||
const groupKey = user.ke
|
||||
if(user.details.factor_mqttout === '1'){
|
||||
sendToMqttConnections(groupKey,'onTwoFactorAuthCodeNotification',[user],true)
|
||||
}
|
||||
}
|
||||
const loadMqttListBotForUser = function(user){
|
||||
const groupKey = user.ke
|
||||
const userDetails = s.parseJSON(user.details);
|
||||
if(userDetails.mqttout === '1'){
|
||||
const mqttClientList = userDetails.mqttout_list || []
|
||||
if(!s.group[groupKey].mqttOutbounders)s.group[groupKey].mqttOutbounders = {};
|
||||
const mqttSubs = s.group[groupKey].mqttOutbounders
|
||||
mqttClientList.forEach(function(row,n){
|
||||
try{
|
||||
const mqttSubId = `${row.host} ${row.pubKey}`
|
||||
const message = row.type || []
|
||||
const eventsToAttachTo = row.msgFor || []
|
||||
const monitorsToRead = row.monitors || []
|
||||
mqttSubs[mqttSubId] = {
|
||||
eventHandlers: {}
|
||||
};
|
||||
mqttSubs[mqttSubId].client = createMqttSubscription({
|
||||
host: row.host,
|
||||
pubKey: row.pubKey,
|
||||
ke: groupKey,
|
||||
});
|
||||
const msgOptions = {
|
||||
ke: groupKey,
|
||||
subId: mqttSubId,
|
||||
to: row.pubKey,
|
||||
}
|
||||
const titleLegend = {
|
||||
onMonitorSave: lang['Monitor Edit'],
|
||||
onMonitorStart: lang['Monitor Start'],
|
||||
onMonitorStop: lang['Monitor Stop'],
|
||||
onMonitorDied: lang['Monitor Died'],
|
||||
onEventTrigger: lang['Event'],
|
||||
onDetectorNoTriggerTimeout: lang['"No Motion" Detector'],
|
||||
onAccountSave: lang['Account Save'],
|
||||
onUserLog: lang['User Log'],
|
||||
onTwoFactorAuthCodeNotification: lang['2-Factor Authentication'],
|
||||
}
|
||||
eventsToAttachTo.forEach(function(eventName){
|
||||
let theAction = function(){}
|
||||
switch(eventName){
|
||||
case'onEventTrigger':
|
||||
theAction = function(d,filter){
|
||||
const eventObject = Object.assign({},d)
|
||||
delete(eventObject.frame);
|
||||
sendMessage(msgOptions,{
|
||||
title: titleLegend[eventName],
|
||||
name: eventName,
|
||||
data: eventObject,
|
||||
time: new Date(),
|
||||
})
|
||||
}
|
||||
break;
|
||||
case'onAccountSave':
|
||||
theAction = function(activeGroup,userDetails,user){
|
||||
sendMessage(msgOptions,{
|
||||
title: titleLegend[eventName],
|
||||
name: eventName,
|
||||
data: {
|
||||
mail: user.mail,
|
||||
ke: user.ke,
|
||||
},
|
||||
time: new Date(),
|
||||
})
|
||||
}
|
||||
break;
|
||||
case'userLog':
|
||||
theAction = function(logEvent){
|
||||
sendMessage(msgOptions,{
|
||||
title: titleLegend[eventName],
|
||||
name: eventName,
|
||||
data: logEvent,
|
||||
time: new Date(),
|
||||
})
|
||||
}
|
||||
break;
|
||||
case'onTwoFactorAuthCodeNotification':
|
||||
theAction = function(user){
|
||||
sendMessage(msgOptions,{
|
||||
title: titleLegend[eventName],
|
||||
name: eventName,
|
||||
data: {
|
||||
code: s.factorAuth[user.ke][user.uid].key
|
||||
},
|
||||
time: new Date(),
|
||||
})
|
||||
}
|
||||
break;
|
||||
case'onMonitorSave':
|
||||
case'onMonitorStart':
|
||||
case'onMonitorStop':
|
||||
case'onMonitorDied':
|
||||
case'onDetectorNoTriggerTimeout':
|
||||
theAction = function(monitorConfig){
|
||||
//e = monitor object
|
||||
sendMessage(msgOptions,{
|
||||
title: titleLegend[eventName],
|
||||
name: eventName,
|
||||
data: {
|
||||
name: monitorConfig.name,
|
||||
monitorId: monitorConfig.mid || monitorConfig.id,
|
||||
},
|
||||
time: new Date(),
|
||||
})
|
||||
}
|
||||
break;
|
||||
}
|
||||
mqttSubs[mqttSubId].eventHandlers[eventName] = theAction
|
||||
})
|
||||
mqttSubs[mqttSubId].monitorsToRead = monitorsToRead;
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
// s.systemLog(err)
|
||||
}
|
||||
})
|
||||
s.group[groupKey].mqttOutbounderKeys = Object.keys(s.group[groupKey].mqttOutbounders)
|
||||
}else{
|
||||
s.group[groupKey].mqttOutbounderKeys = []
|
||||
}
|
||||
}
|
||||
const unloadMqttListBotForUser = function(user){
|
||||
const groupKey = user.ke
|
||||
const mqttSubs = s.group[groupKey].mqttOutbounders || {}
|
||||
Object.keys(mqttSubs).forEach(function(mqttSubId){
|
||||
try{
|
||||
mqttSubs[mqttSubId].client.end()
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
// s.userLog({
|
||||
// ke: groupKey,
|
||||
// mid: '$USER'
|
||||
// },{
|
||||
// type: lang['MQTT Error'],
|
||||
// msg: err
|
||||
// })
|
||||
}
|
||||
delete(mqttSubs[mqttSubId])
|
||||
})
|
||||
}
|
||||
const onBeforeAccountSave = function(data){
|
||||
data.d.mqttout_list = []
|
||||
}
|
||||
s.loadGroupAppExtender(loadMqttListBotForUser)
|
||||
s.unloadGroupAppExtender(unloadMqttListBotForUser)
|
||||
s.beforeAccountSave(onBeforeAccountSave)
|
||||
s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotification)
|
||||
s.onEventTrigger(onEventTrigger)
|
||||
s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilter)
|
||||
s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeout)
|
||||
s.onMonitorSave(onMonitorSave)
|
||||
s.onMonitorStart(onMonitorStart)
|
||||
s.onMonitorStop(onMonitorStop)
|
||||
s.onMonitorDied(onMonitorDied)
|
||||
s.onUserLog(onUserLog)
|
||||
s.definitions["Monitor Settings"].blocks["Notifications"].info[0].info.push(
|
||||
{
|
||||
"name": "detail=notify_mqttout",
|
||||
"field": lang['MQTT Outbound'],
|
||||
"description": "",
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"selector": "h_det_mqttout",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
s.definitions["Monitor Settings"].blocks["Notifications"].info.push({
|
||||
"evaluation": "$user.details.use_mqttout !== '0'",
|
||||
isFormGroupGroup: true,
|
||||
"name": lang['MQTT Outbound'],
|
||||
"color": "blue",
|
||||
"section-class": "h_det_mqttout_input h_det_mqttout_1",
|
||||
"info": [
|
||||
{
|
||||
"name": "detail=detector_mqttout_timeout",
|
||||
"field": lang['Allow Next Alert'] + ` (${lang['on Event']})`,
|
||||
"description": "",
|
||||
"default": "10",
|
||||
"example": "",
|
||||
"possible": ""
|
||||
},
|
||||
]
|
||||
})
|
||||
s.definitions["Account Settings"].blocks["2-Factor Authentication"].info.push({
|
||||
"name": "detail=factor_mqttout",
|
||||
"field": lang['MQTT Outbound'],
|
||||
"default": "1",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
})
|
||||
s.definitions["Account Settings"].blocks["MQTT Outbound"] = {
|
||||
"evaluation": "$user.details.use_mqttout !== '0'",
|
||||
"name": lang['MQTT Outbound'],
|
||||
"color": "blue",
|
||||
"info": [
|
||||
{
|
||||
"name": "detail=mqttout",
|
||||
"selector":"u_mqttout",
|
||||
"field": lang.Enabled,
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"fieldType": "btn",
|
||||
"class": `btn-success mqtt-out-add-row`,
|
||||
"btnContent": `<i class="fa fa-plus"></i> ${lang['Add']}`,
|
||||
},
|
||||
{
|
||||
"id": "mqttout_list",
|
||||
"fieldType": "div",
|
||||
},
|
||||
{
|
||||
"fieldType": "script",
|
||||
"src": "assets/js/bs5.mqttOut.js",
|
||||
}
|
||||
]
|
||||
}
|
||||
s.definitions["Event Filters"].blocks["Action for Selected"].info.push({
|
||||
"name": "actions=mqttout",
|
||||
"field": lang['MQTT Outbound'],
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"default": "",
|
||||
"example": "1",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang['Original Choice'],
|
||||
"value": "",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1",
|
||||
}
|
||||
]
|
||||
})
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
console.log('Could not start MQTT Outbound Handling.')
|
||||
}
|
||||
}
|
||||
}
|
|
@ -112,9 +112,8 @@ module.exports = function (s, config, lang, getSnapshot) {
|
|||
s.group[d.ke].rawMonitorConfigurations[d.id];
|
||||
// d = event object
|
||||
if (
|
||||
filter.pushover &&
|
||||
s.group[d.ke].pushover &&
|
||||
monitorConfig.details.notify_pushover === '1' &&
|
||||
(filter.pushover || monitorConfig.details.notify_pushover === '1') &&
|
||||
!s.group[d.ke].activeMonitors[d.id].detector_pushover
|
||||
) {
|
||||
var detector_pushover_timeout;
|
||||
|
@ -163,7 +162,7 @@ module.exports = function (s, config, lang, getSnapshot) {
|
|||
};
|
||||
|
||||
const onEventTriggerBeforeFilterForPushover = function (d, filter) {
|
||||
filter.pushover = true;
|
||||
filter.pushover = false;
|
||||
};
|
||||
|
||||
const onDetectorNoTriggerTimeoutForPushover = function (e) {
|
||||
|
@ -339,6 +338,25 @@ module.exports = function (s, config, lang, getSnapshot) {
|
|||
},
|
||||
],
|
||||
};
|
||||
s.definitions["Event Filters"].blocks["Action for Selected"].info.push({
|
||||
"name": "actions=pushover",
|
||||
"field": lang['Pushover'],
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"default": "",
|
||||
"example": "1",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang['Original Choice'],
|
||||
"value": "",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1",
|
||||
}
|
||||
]
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
console.log(
|
||||
|
@ -346,4 +364,4 @@ module.exports = function (s, config, lang, getSnapshot) {
|
|||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -1,4 +1,11 @@
|
|||
var fs = require("fs")
|
||||
// function asyncSetTimeout(timeout){
|
||||
// return new Promise((resolve,reject) => {
|
||||
// setTimeout(() => {
|
||||
// resolve()
|
||||
// },timeout || 1000)
|
||||
// })
|
||||
// }
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
const {
|
||||
getEventBasedRecordingUponCompletion,
|
||||
|
@ -30,6 +37,7 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
})
|
||||
}
|
||||
}catch(err){
|
||||
s.debugLog('Telegram Error',err)
|
||||
s.userLog({ke:groupKey,mid:'$USER'},{type:lang.NotifyErrorText,msg:err})
|
||||
}
|
||||
}else{
|
||||
|
@ -43,13 +51,13 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
}
|
||||
}
|
||||
const onEventTriggerBeforeFilterForTelegram = function(d,filter){
|
||||
filter.telegram = true
|
||||
filter.telegram = false
|
||||
}
|
||||
const onEventTriggerForTelegram = async (d,filter) => {
|
||||
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id]
|
||||
// d = event object
|
||||
//telegram bot
|
||||
if(filter.telegram && s.group[d.ke].telegramBot && monitorConfig.details.notify_telegram === '1' && !s.group[d.ke].activeMonitors[d.id].detector_telegrambot){
|
||||
if(s.group[d.ke].telegramBot && (filter.telegram || monitorConfig.details.notify_telegram === '1') && !s.group[d.ke].activeMonitors[d.id].detector_telegrambot){
|
||||
var detector_telegrambot_timeout
|
||||
if(!monitorConfig.details.detector_telegrambot_timeout||monitorConfig.details.detector_telegrambot_timeout===''){
|
||||
detector_telegrambot_timeout = 1000 * 60 * 10;
|
||||
|
@ -61,6 +69,7 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
s.group[d.ke].activeMonitors[d.id].detector_telegrambot = null
|
||||
},detector_telegrambot_timeout)
|
||||
if(monitorConfig.details.detector_telegrambot_send_video === '1'){
|
||||
// await asyncSetTimeout(3000)
|
||||
let videoPath = null
|
||||
let videoName = null
|
||||
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
|
||||
|
@ -267,7 +276,7 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
"default": "",
|
||||
"example": "",
|
||||
"possible": ""
|
||||
},
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
"name": "detail=telegrambot_channel",
|
||||
|
@ -281,8 +290,31 @@ module.exports = function(s,config,lang,getSnapshot){
|
|||
}
|
||||
]
|
||||
}
|
||||
s.definitions["Event Filters"].blocks["Action for Selected"].info.push({
|
||||
"name": "actions=telegram",
|
||||
"field": lang['Telegram'],
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"default": "",
|
||||
"example": "1",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.Default,
|
||||
"value": "",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0",
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1",
|
||||
}
|
||||
]
|
||||
})
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
console.error(err)
|
||||
console.log('Could not start Telegram bot, please run "npm install node-telegram-bot-api" inside the Shinobi folder.')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,289 @@
|
|||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
function replaceQueryStringValues(webhookEndpoint,data){
|
||||
let newString = webhookEndpoint
|
||||
.replace(/{{INNER_EVENT_TITLE}}/g,data.title)
|
||||
.replace(/{{INNER_EVENT_INFO}}/g,s.stringJSON(data.info));
|
||||
return newString;
|
||||
}
|
||||
const sendMessage = function(sendBody,files,groupKey){
|
||||
let webhookEndpoint = s.group[groupKey].init.global_webhook_url;
|
||||
if(!webhookEndpoint){
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
mid: '$USER'
|
||||
},{
|
||||
type: lang.NotifyErrorText,
|
||||
infoType: 'global_webhook',
|
||||
msg: lang['Invalid Settings']
|
||||
})
|
||||
return new Promise((resolve,reject) => {
|
||||
resolve({
|
||||
error: lang['Invalid Settings'],
|
||||
ok: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
const doPostMethod = s.group[groupKey].init.global_webhook_method === 'post';
|
||||
// const includeSnapshot = s.group[groupKey].init.global_webhook_include_image === '1';
|
||||
const webhookInfoData = {
|
||||
info: sendBody,
|
||||
files: [],
|
||||
}
|
||||
if(files){
|
||||
const formData = new FormData();
|
||||
files.forEach(async (file,n) => {
|
||||
switch(file.type){
|
||||
case'video':
|
||||
// video cannot be sent this way unless POST
|
||||
if(doPostMethod){
|
||||
const fileName = file.name
|
||||
formData.append(`file${n + 1}`, file.attachment, {
|
||||
contentType: 'video/mp4',
|
||||
name: fileName,
|
||||
filename: fileName,
|
||||
});
|
||||
webhookInfoData.files.push(fileName)
|
||||
}
|
||||
break;
|
||||
case'photo':
|
||||
if(doPostMethod){
|
||||
const fileName = file.name
|
||||
formData.append(`file${n + 1}`, file.attachment, {
|
||||
contentType: 'image/jpeg',
|
||||
name: fileName,
|
||||
filename: fileName,
|
||||
});
|
||||
webhookInfoData.files.push(fileName)
|
||||
}else{
|
||||
const base64StringofImage = file.attachment.toString('base64')
|
||||
webhookInfoData.files.push(base64StringofImage)
|
||||
}
|
||||
break;
|
||||
}
|
||||
})
|
||||
}else{
|
||||
delete(webhookInfoData.files)
|
||||
}
|
||||
webhookEndpoint = replaceQueryStringValues(webhookEndpoint,{
|
||||
title: sendBody.title,
|
||||
info: webhookInfoData,
|
||||
});
|
||||
return new Promise((resolve,reject) => {
|
||||
const response = {
|
||||
ok: true,
|
||||
}
|
||||
fetch(webhookEndpoint,doPostMethod ? {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
} : undefined)
|
||||
.then(res => res.text())
|
||||
.then((text) => {
|
||||
response.response = text;
|
||||
resolve(response)
|
||||
})
|
||||
.catch((err) => {
|
||||
response.ok = false;
|
||||
response.error = err;
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
mid: '$USER'
|
||||
},{
|
||||
type: lang.NotifyErrorText,
|
||||
infoType: 'global_webhook',
|
||||
msg: err
|
||||
})
|
||||
resolve(response)
|
||||
})
|
||||
})
|
||||
}
|
||||
const onEventTriggerBeforeFilterForGlobalWebhook = function(d,filter){
|
||||
filter.global_webhook = false
|
||||
}
|
||||
const onEventTriggerForGlobalWebhook = async (d,filter) => {
|
||||
let filesSent = 0;
|
||||
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id]
|
||||
// d = event object
|
||||
if((filter.global_webhook || monitorConfig.details.notify_global_webhook === '1') && !s.group[d.ke].activeMonitors[d.id].detector_global_webhook){
|
||||
var detector_global_webhook_timeout
|
||||
if(!monitorConfig.details.detector_global_webhook_timeout||monitorConfig.details.detector_global_webhook_timeout===''){
|
||||
detector_global_webhook_timeout = 1000 * 60 * 10;
|
||||
}else{
|
||||
detector_global_webhook_timeout = parseFloat(monitorConfig.details.detector_global_webhook_timeout) * 1000 * 60;
|
||||
}
|
||||
s.group[d.ke].activeMonitors[d.id].detector_global_webhook = setTimeout(function(){
|
||||
clearTimeout(s.group[d.ke].activeMonitors[d.id].detector_global_webhook);
|
||||
s.group[d.ke].activeMonitors[d.id].detector_global_webhook = null
|
||||
},detector_global_webhook_timeout)
|
||||
await getSnapshot(d,monitorConfig)
|
||||
if(d.screenshotBuffer){
|
||||
sendMessage({
|
||||
title: lang.Event+' - '+d.screenshotName,
|
||||
description: lang.EventText1+' '+d.currentTimestamp,
|
||||
},[
|
||||
{
|
||||
type: 'photo',
|
||||
attachment: d.screenshotBuffer,
|
||||
name: d.screenshotName+'.jpg'
|
||||
}
|
||||
],d.ke)
|
||||
++filesSent;
|
||||
}
|
||||
if(filesSent === 0){
|
||||
sendMessage({
|
||||
title: lang.Event,
|
||||
description: lang.EventText1+' '+d.currentTimestamp,
|
||||
eventDetails: d.details
|
||||
},[],d.ke)
|
||||
}
|
||||
}
|
||||
}
|
||||
const onTwoFactorAuthCodeNotificationForGlobalWebhook = function(r){
|
||||
// r = user
|
||||
if(r.details.factor_global_webhook === '1'){
|
||||
sendMessage({
|
||||
title: r.lang['Enter this code to proceed'],
|
||||
description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+r.lang.FactorAuthText1,
|
||||
},[],r.ke)
|
||||
}
|
||||
}
|
||||
// const onDetectorNoTriggerTimeoutForGlobalWebhook = function(e){
|
||||
// //e = monitor object
|
||||
// var currentTime = new Date()
|
||||
// if(e.details.detector_notrigger_global_webhook === '1'){
|
||||
// var html = '*'+lang.NoMotionEmailText2+' ' + (e.details.detector_notrigger_timeout || 10) + ' '+lang.minutes+'.*\n'
|
||||
// html += '**' + lang['Monitor Name'] + '** : '+e.name + '\n'
|
||||
// html += '**' + lang['Monitor ID'] + '** : '+e.id + '\n'
|
||||
// html += currentTime
|
||||
// sendMessage({
|
||||
// title: lang['\"No Motion"\ Detector'],
|
||||
// description: html,
|
||||
// },[],e.ke)
|
||||
// }
|
||||
// }
|
||||
const onMonitorUnexpectedExitForGlobalWebhook = (monitorConfig) => {
|
||||
if(monitorConfig.details.notify_global_webhook === '1' && monitorConfig.details.notify_onUnexpectedExit === '1'){
|
||||
const ffmpegCommand = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].ffmpeg
|
||||
const description = lang['Process Crashed for Monitor'] + '\n' + ffmpegCommand
|
||||
const currentTime = new Date()
|
||||
sendMessage({
|
||||
title: lang['Process Unexpected Exit'] + ' : ' + monitorConfig.name,
|
||||
description: description,
|
||||
},[],monitorConfig.ke)
|
||||
}
|
||||
}
|
||||
s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotificationForGlobalWebhook)
|
||||
s.onEventTrigger(onEventTriggerForGlobalWebhook)
|
||||
s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilterForGlobalWebhook)
|
||||
// s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeoutForGlobalWebhook)
|
||||
s.onMonitorUnexpectedExit(onMonitorUnexpectedExitForGlobalWebhook)
|
||||
s.definitions["Monitor Settings"].blocks["Notifications"].info[0].info.push(
|
||||
{
|
||||
"name": "detail=notify_global_webhook",
|
||||
"field": lang.Webhook,
|
||||
"description": "",
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"selector": "h_det_global_webhook",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
)
|
||||
s.definitions["Account Settings"].blocks["2-Factor Authentication"].info.push({
|
||||
"name": "detail=factor_global_webhook",
|
||||
"field": lang.Webhook,
|
||||
"default": "1",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
})
|
||||
s.definitions["Account Settings"].blocks["Webhook"] = {
|
||||
"evaluation": "$user.details.use_global_webhook !== '0'",
|
||||
"name": lang.Webhook,
|
||||
"color": "blue",
|
||||
info: [
|
||||
{
|
||||
"name": "detail=global_webhook",
|
||||
"selector":"u_global_webhook",
|
||||
"field": lang.Enabled,
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
"name": "detail=global_webhook_url",
|
||||
"placeholder": "http://your-webhook-point/onEvent/{{INNER_EVENT_TITLE}}?info={{INNER_EVENT_INFO}}",
|
||||
"field": lang["Webhook URL"],
|
||||
"form-group-class":"u_global_webhook_input u_global_webhook_1",
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
"name": "detail=factor_global_webhook",
|
||||
"field": lang["2-Factor Authentication"],
|
||||
"form-group-class":"u_global_webhook_input u_global_webhook_1",
|
||||
"default": "1",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
s.definitions["Event Filters"].blocks["Action for Selected"].info.push({
|
||||
"name": "actions=global_webhook",
|
||||
"field": lang['Webhook'],
|
||||
"fieldType": "select",
|
||||
"form-group-class": "actions-row",
|
||||
"default": "",
|
||||
"example": "1",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang['Original Choice'],
|
||||
"value": "",
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1",
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
|
@ -17,6 +17,10 @@ const {
|
|||
} = require('./onvifDeviceManager/utils.js')
|
||||
|
||||
module.exports = function(s,config,lang,app,io){
|
||||
async function getOnvifDevice(groupKey,monitorId){
|
||||
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({id: monitorId, ke: groupKey})).device
|
||||
return onvifDevice
|
||||
}
|
||||
/**
|
||||
* API : Get ONVIF Data from Camera
|
||||
*/
|
||||
|
@ -26,7 +30,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
try{
|
||||
const groupKey = req.params.ke
|
||||
const monitorId = req.params.id
|
||||
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
|
||||
const onvifDevice = await getOnvifDevice(groupKey,monitorId)
|
||||
const cameraInfo = await getUIFieldValues(onvifDevice)
|
||||
endData.onvifData = cameraInfo
|
||||
}catch(err){
|
||||
|
@ -47,7 +51,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
try{
|
||||
const groupKey = req.params.ke
|
||||
const monitorId = req.params.id
|
||||
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
|
||||
const onvifDevice = await getOnvifDevice(groupKey,monitorId)
|
||||
const form = s.getPostData(req)
|
||||
const videoToken = form.VideoConfiguration && form.VideoConfiguration.videoToken ? form.VideoConfiguration.videoToken : null
|
||||
if(form.DateandTime){
|
||||
|
@ -100,7 +104,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
try{
|
||||
const groupKey = req.params.ke
|
||||
const monitorId = req.params.id
|
||||
const onvifDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection
|
||||
const onvifDevice = await getOnvifDevice(groupKey,monitorId)
|
||||
const cameraInfo = await rebootCamera(onvifDevice)
|
||||
endData.onvifData = cameraInfo
|
||||
}catch(err){
|
||||
|
|
|
@ -149,6 +149,7 @@ const getDeviceInformation = async (onvifDevice,options) => {
|
|||
response.ok = false
|
||||
response.error = err.stack.toString().toString()
|
||||
s.debugLog(err)
|
||||
s.debugLog(onvifDevice)
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -1,13 +1,12 @@
|
|||
const fs = require('fs-extra');
|
||||
const express = require('express')
|
||||
const request = require('request')
|
||||
const unzipper = require('unzipper')
|
||||
const fetch = require("node-fetch")
|
||||
const spawn = require('child_process').spawn
|
||||
const {
|
||||
Worker
|
||||
} = require('worker_threads');
|
||||
module.exports = async (s,config,lang,app,io,currentUse) => {
|
||||
const { fetchDownloadAndWrite } = require('../basic/utils.js')(process.cwd(),config)
|
||||
const {
|
||||
currentPluginCpuUsage,
|
||||
currentPluginGpuUsage,
|
||||
|
@ -96,10 +95,9 @@ module.exports = async (s,config,lang,app,io,currentUse) => {
|
|||
fs.mkdirSync(downloadPath)
|
||||
return new Promise(async (resolve, reject) => {
|
||||
fs.mkdir(downloadPath, () => {
|
||||
request(downloadUrl).pipe(fs.createWriteStream(downloadPath + '.zip'))
|
||||
.on('finish',() => {
|
||||
zip = fs.createReadStream(downloadPath + '.zip')
|
||||
.pipe(unzipper.Parse())
|
||||
fetchDownloadAndWrite(downloadUrl,downloadPath + '.zip', 1)
|
||||
.then((readStream) => {
|
||||
readStream.pipe(unzipper.Parse())
|
||||
.on('entry', async (file) => {
|
||||
if(file.type === 'Directory'){
|
||||
try{
|
||||
|
|
|
@ -2,6 +2,7 @@ var os = require('os');
|
|||
const onvif = require("shinobi-onvif");
|
||||
const {
|
||||
stringContains,
|
||||
getBuffer,
|
||||
} = require('../common.js')
|
||||
module.exports = (s,config,lang) => {
|
||||
const ipRange = (start_ip, end_ip) => {
|
||||
|
@ -119,6 +120,7 @@ module.exports = (s,config,lang) => {
|
|||
ProfileToken : device.current_profile.token,
|
||||
Protocol : 'RTSP'
|
||||
})
|
||||
|
||||
var cameraResponse = {
|
||||
ip: camera.ip,
|
||||
port: camera.port,
|
||||
|
@ -142,16 +144,13 @@ module.exports = (s,config,lang) => {
|
|||
}
|
||||
responseList.push(cameraResponse)
|
||||
var imageSnap
|
||||
if(cameraResponse.uri){
|
||||
try{
|
||||
imageSnap = (await s.getSnapshotFromOnvif({
|
||||
username: onvifUsername,
|
||||
password: onvifPassword,
|
||||
uri: cameraResponse.uri,
|
||||
})).toString('base64');
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
}
|
||||
try{
|
||||
const snapUri = (await device.services.media.getSnapshotUri({
|
||||
ProfileToken : device.current_profile.token,
|
||||
})).data.GetSnapshotUriResponse.MediaUri.Uri
|
||||
imageSnap = (await getBuffer(snapUri)).toString('base64');
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
}
|
||||
if(foundCameraCallback)foundCameraCallback(Object.assign(cameraResponse,{f: 'onvif', snapShot: imageSnap}))
|
||||
}catch(err){
|
||||
|
@ -185,7 +184,7 @@ module.exports = (s,config,lang) => {
|
|||
error: errorMessage
|
||||
})
|
||||
}
|
||||
s.debugLog(err)
|
||||
if(config.debugLogVerbose)s.debugLog(err);
|
||||
}
|
||||
})
|
||||
return responseList
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
var fs = require('fs')
|
||||
var request = require('request')
|
||||
const fs = require('fs')
|
||||
const fetch = require('node-fetch')
|
||||
module.exports = function(s,config,lang,app,io){
|
||||
if(config.shinobiHubEndpoint === undefined){config.shinobiHubEndpoint = `https://hub.shinobi.video/`}else{config.shinobiHubEndpoint = s.checkCorrectPathEnding(config.shinobiHubEndpoint)}
|
||||
var stripUsernameAndPassword = function(string,username,password){
|
||||
|
@ -78,7 +78,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
// s.userLog({ke:monitorConfig.ke,mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.mail,ip:cn.ip}})
|
||||
})
|
||||
}
|
||||
if(s.group[monitorConfig.ke] && s.group[monitorConfig.ke].init.shinobihub === '1'){
|
||||
if(s.group[monitorConfig.ke] && s.group[monitorConfig.ke].init && s.group[monitorConfig.ke].init.shinobihub === '1'){
|
||||
uploadConfiguration(s.group[monitorConfig.ke].init.shinobihub_key,'cam',monitorConfig,() => {
|
||||
// s.userLog({ke:monitorConfig.ke,mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.mail,ip:cn.ip}})
|
||||
})
|
||||
|
@ -100,7 +100,11 @@ module.exports = function(s,config,lang,app,io){
|
|||
queryString.push(key + '=' + value)
|
||||
})
|
||||
}
|
||||
request(`${config.shinobiHubEndpoint}api/${shinobiHubApiKey}/getConfiguration/${req.params.type}${req.params.id ? '/' + req.params.id : ''}${queryString.length > 0 ? '?' + queryString.join('&') : ''}`).pipe(res)
|
||||
const configUrl = `${config.shinobiHubEndpoint}api/${shinobiHubApiKey}/getConfiguration/${req.params.type}${req.params.id ? '/' + req.params.id : ''}${queryString.length > 0 ? '?' + queryString.join('&') : ''}`
|
||||
fetch(configUrl).then(actual => {
|
||||
actual.headers.forEach((v, n) => res.setHeader(n, v));
|
||||
actual.body.pipe(res);
|
||||
})
|
||||
}else{
|
||||
s.closeJsonResponse(res,{
|
||||
ok: false,
|
||||
|
|
|
@ -144,39 +144,6 @@ module.exports = function(s,config,lang,io){
|
|||
////socket controller
|
||||
io.on('connection', function (cn) {
|
||||
var tx;
|
||||
//unique h265 socket stream
|
||||
cn.on('h265',function(d){
|
||||
if(!s.group[d.ke]||!s.group[d.ke].activeMonitors||!s.group[d.ke].activeMonitors[d.id]){
|
||||
cn.disconnect();return;
|
||||
}
|
||||
cn.ip=cn.request.connection.remoteAddress;
|
||||
var toUTC = function(){
|
||||
return new Date().toISOString();
|
||||
}
|
||||
var tx=function(z){cn.emit('data',z);}
|
||||
const onFail = (msg) => {
|
||||
tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke});
|
||||
cn.disconnect();
|
||||
}
|
||||
const onSuccess = (r) => {
|
||||
r = r[0];
|
||||
const Emitter = createStreamEmitter(d,cn)
|
||||
validatedAndBindAuthenticationToSocketConnection(cn,d,true)
|
||||
var contentWriter
|
||||
cn.closeSocketVideoStream = function(){
|
||||
Emitter.removeListener('data', contentWriter);
|
||||
}
|
||||
Emitter.on('data',contentWriter = function(base64){
|
||||
tx(base64)
|
||||
})
|
||||
}
|
||||
//check if auth key is user's temporary session key
|
||||
if(s.group[d.ke]&&s.group[d.ke].users&&s.group[d.ke].users[d.auth]){
|
||||
onSuccess(s.group[d.ke].users[d.auth]);
|
||||
}else{
|
||||
streamConnectionAuthentication(d,cn.ip).then(onSuccess).catch(onFail)
|
||||
}
|
||||
})
|
||||
//unique Base64 socket stream
|
||||
cn.on('Base64',function(d){
|
||||
if(!s.group[d.ke]||!s.group[d.ke].activeMonitors||!s.group[d.ke].activeMonitors[d.id]){
|
||||
|
@ -360,7 +327,7 @@ module.exports = function(s,config,lang,io){
|
|||
if(s.group[d.ke].users[d.auth].details.get_server_log!=='0'){
|
||||
cn.join('GRPLOG_'+d.ke)
|
||||
}
|
||||
s.group[d.ke].users[d.auth].lang=s.getLanguageFile(s.group[d.ke].users[d.auth].details.lang)
|
||||
s.group[d.ke].users[d.auth].lang = s.getLanguageFile(s.group[d.ke].users[d.auth].details.lang)
|
||||
s.userLog({ke:d.ke,mid:'$USER'},{type:s.group[d.ke].users[d.auth].lang['Websocket Connected'],msg:{mail:r.mail,id:d.uid,ip:cn.ip}})
|
||||
if(!s.group[d.ke].activeMonitors){
|
||||
s.group[d.ke].activeMonitors={}
|
||||
|
@ -380,7 +347,7 @@ module.exports = function(s,config,lang,io){
|
|||
}
|
||||
})
|
||||
try{
|
||||
Object.values(s.group[d.ke].rawMonitorConfigurations).forEach((monitor) => {
|
||||
(s.group[d.ke] ? Object.values(s.group[d.ke].rawMonitorConfigurations) : []).forEach((monitor) => {
|
||||
s.cameraSendSnapshot({
|
||||
mid: monitor.mid,
|
||||
ke: monitor.ke,
|
||||
|
@ -443,8 +410,18 @@ module.exports = function(s,config,lang,io){
|
|||
]
|
||||
},(err,r) => {
|
||||
if(r && r[0]){
|
||||
const monitorListOrder = {}
|
||||
const orderKeys = Object.keys(d.monitorListOrder)
|
||||
details = JSON.parse(r[0].details)
|
||||
details.monitorListOrder = d.monitorListOrder
|
||||
orderKeys.forEach((orderKey) => {
|
||||
const monitorIds = d.monitorListOrder[orderKey]
|
||||
const uniqueList = {}
|
||||
monitorIds.forEach((monitorId) => {
|
||||
uniqueList[monitorId] = 1
|
||||
})
|
||||
monitorListOrder[orderKey] = Object.keys(uniqueList)
|
||||
})
|
||||
details.monitorListOrder = monitorListOrder
|
||||
s.knexQuery({
|
||||
action: "update",
|
||||
table: "Users",
|
||||
|
@ -692,6 +669,7 @@ module.exports = function(s,config,lang,io){
|
|||
f: 'monitor_watch_on',
|
||||
id: d.id,
|
||||
ke: d.ke,
|
||||
subStreamChannel: s.group[d.ke].activeMonitors[d.id].subStreamChannel,
|
||||
warnings: s.group[d.ke].activeMonitors[d.id].warnings || []
|
||||
})
|
||||
s.camera('watch_on',d,cn)
|
||||
|
@ -1050,7 +1028,7 @@ module.exports = function(s,config,lang,io){
|
|||
delete(s.clientSocketConnection[cn.id])
|
||||
})
|
||||
s.onWebSocketConnectionExtensions.forEach(function(extender){
|
||||
extender(cn)
|
||||
extender(cn,validatedAndBindAuthenticationToSocketConnection,createStreamEmitter)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -11,6 +11,9 @@ module.exports = function(s,config,lang,io){
|
|||
const {
|
||||
checkSubscription
|
||||
} = require('./basic/utils.js')(process.cwd(),config)
|
||||
const {
|
||||
checkForStaticUsers
|
||||
} = require('./user/startup.js')(s,config,lang,io)
|
||||
return new Promise((resolve, reject) => {
|
||||
var checkedAdminUsers = {}
|
||||
console.log('FFmpeg version : '+s.ffmpegVersion)
|
||||
|
@ -409,7 +412,8 @@ module.exports = function(s,config,lang,io){
|
|||
s.databaseEngine = require('knex')(s.databaseOptions)
|
||||
//run prerequsite queries
|
||||
s.preQueries()
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
await checkForStaticUsers()
|
||||
//check for subscription
|
||||
checkSubscription(config.subscriptionId,function(hasSubcribed){
|
||||
config.userHasSubscribed = hasSubcribed
|
||||
|
|
|
@ -2,6 +2,9 @@ var fs = require('fs')
|
|||
var moment = require('moment')
|
||||
var express = require('express')
|
||||
module.exports = function(s,config,lang,app,io){
|
||||
const {
|
||||
sendTimelapseFrameToMasterNode,
|
||||
} = require('./childNode/childUtils.js')(s,config,lang)
|
||||
const timelapseFramesCache = {}
|
||||
const timelapseFramesCacheTimeouts = {}
|
||||
s.getTimelapseFrameDirectory = function(e){
|
||||
|
@ -22,8 +25,8 @@ module.exports = function(s,config,lang,app,io){
|
|||
if(e.details && e.details.dir && e.details.dir !== ''){
|
||||
details.dir = e.details.dir
|
||||
}
|
||||
var timeNow = eventTime || new Date()
|
||||
var queryInfo = {
|
||||
const timeNow = eventTime || new Date()
|
||||
const queryInfo = {
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
details: s.s(details),
|
||||
|
@ -32,45 +35,16 @@ module.exports = function(s,config,lang,app,io){
|
|||
time: timeNow
|
||||
}
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){
|
||||
var currentDate = s.formattedTime(queryInfo.time,'YYYY-MM-DD')
|
||||
s.cx({
|
||||
f: 'open_timelapse_file_transfer',
|
||||
var currentDate = s.formattedTime(timeNow,'YYYY-MM-DD')
|
||||
const childNodeData = {
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
d: s.group[e.ke].rawMonitorConfigurations[e.id],
|
||||
time: currentDate,
|
||||
filename: filename,
|
||||
currentDate: currentDate,
|
||||
queryInfo: queryInfo
|
||||
})
|
||||
var formattedTime = s.timeObject(timeNow).format()
|
||||
fs.createReadStream(filePath,{ highWaterMark: 500 })
|
||||
.on('data',function(data){
|
||||
s.cx({
|
||||
f: 'created_timelapse_file_chunk',
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
time: formattedTime,
|
||||
filesize: e.filesize,
|
||||
chunk: data,
|
||||
d: s.group[e.ke].rawMonitorConfigurations[e.id],
|
||||
filename: filename,
|
||||
currentDate: currentDate,
|
||||
queryInfo: queryInfo
|
||||
})
|
||||
})
|
||||
.on('close',function(){
|
||||
s.cx({
|
||||
f: 'created_timelapse_file',
|
||||
ke: e.ke,
|
||||
mid: e.id,
|
||||
time: formattedTime,
|
||||
filesize: e.filesize,
|
||||
d: s.group[e.ke].rawMonitorConfigurations[e.id],
|
||||
filename: filename,
|
||||
currentDate: currentDate,
|
||||
queryInfo: queryInfo
|
||||
})
|
||||
})
|
||||
}
|
||||
sendTimelapseFrameToMasterNode(filePath,childNodeData)
|
||||
}else{
|
||||
s.insertTimelapseFrameDatabaseRow(e,queryInfo,filePath)
|
||||
}
|
||||
|
@ -202,6 +176,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
endDate: req.query.end,
|
||||
startOperator: req.query.startOperator,
|
||||
endOperator: req.query.endOperator,
|
||||
noLimit: req.query.noLimit,
|
||||
limit: req.query.limit,
|
||||
archived: req.query.archived,
|
||||
rowType: 'frames',
|
||||
|
@ -267,9 +242,9 @@ module.exports = function(s,config,lang,app,io){
|
|||
const frames = []
|
||||
var n = 0
|
||||
framesPosted.forEach((frame) => {
|
||||
var firstParam = ['ke','=',req.params.ke]
|
||||
if(n !== 0)firstParam = (['or']).concat(firstParam)
|
||||
frames.push(firstParam,['mid','=',req.params.id],['filename','=',frame.filename])
|
||||
var firstParam = [['ke','=',req.params.ke],['mid','=',req.params.id],['filename','=',frame.filename]]
|
||||
if(n !== 0)firstParam[0] = (['or']).concat(firstParam[0])
|
||||
frames.push(...firstParam)
|
||||
++n
|
||||
})
|
||||
s.knexQuery({
|
||||
|
@ -278,6 +253,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
table: "Timelapse Frames",
|
||||
where: frames
|
||||
},(err,r) => {
|
||||
s.debugLog("Timelapse Frames Building Video",r.length)
|
||||
if(r.length === 0){
|
||||
s.closeJsonResponse(res,{
|
||||
ok: false
|
||||
|
@ -356,6 +332,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
groupKey: req.params.ke,
|
||||
archived: req.query.archived,
|
||||
filename: req.params.filename,
|
||||
limit: 1,
|
||||
rowType: 'frames',
|
||||
endIsStartTo: true
|
||||
},(response) => {
|
||||
|
|
|
@ -15,6 +15,6 @@ module.exports = function(s,config,lang,app,io){
|
|||
Object.keys(loadedLibraries).forEach((key) => {
|
||||
var loadedLib = loadedLibraries[key](s,config,lang,app,io)
|
||||
loadedLib.isFormGroupGroup = true
|
||||
s.uploaderFields.push(loadedLib)
|
||||
s.definitions["Account Settings"].blocks["Uploaders"].info.push(loadedLib)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -57,6 +57,10 @@ module.exports = function(s,config,lang){
|
|||
b2.listBuckets().then(function(resp){
|
||||
var buckets = resp.buckets
|
||||
var bucketN = -2
|
||||
if(!buckets){
|
||||
s.userLog({mid:'$USER',ke:e.ke},{type: lang['Backblaze Error'],msg: lang['Not Authorized']})
|
||||
return
|
||||
}
|
||||
buckets.forEach(function(item,n){
|
||||
if(item.bucketName === userDetails.bb_b2_bucket){
|
||||
bucketN = n
|
||||
|
|
|
@ -177,6 +177,7 @@ module.exports = function(s,config,lang){
|
|||
mid: queryInfo.mid,
|
||||
ke: queryInfo.ke,
|
||||
time: queryInfo.time,
|
||||
filename: queryInfo.filename,
|
||||
details: s.s({
|
||||
type : 'whcs',
|
||||
location : saveLocation
|
||||
|
|
|
@ -41,11 +41,10 @@ module.exports = function(s,config,lang){
|
|||
userDetails.webdav_dir='/'
|
||||
}
|
||||
userDetails.webdav_dir = s.checkCorrectPathEnding(userDetails.webdav_dir)
|
||||
s.group[e.ke].webdav = webdav(
|
||||
userDetails.webdav_url,
|
||||
userDetails.webdav_user,
|
||||
userDetails.webdav_pass
|
||||
)
|
||||
s.group[e.ke].webdav = webdav.createAdapter(userDetails.webdav_url, {
|
||||
username: userDetails.webdav_user,
|
||||
password: userDetails.webdav_pass
|
||||
})
|
||||
}
|
||||
}
|
||||
var unloadWebDavForUser = function(user){
|
||||
|
|
24
libs/user.js
24
libs/user.js
|
@ -14,6 +14,7 @@ module.exports = function(s,config,lang){
|
|||
deleteFileBinFiles,
|
||||
deleteCloudVideos,
|
||||
deleteCloudTimelapseFrames,
|
||||
resetAllStorageCounters,
|
||||
} = require("./user/utils.js")(s,config,lang);
|
||||
let purgeDiskGroup = () => {}
|
||||
const runQuery = async.queue(function(groupKey, callback) {
|
||||
|
@ -267,6 +268,12 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
})
|
||||
}
|
||||
function filterMonitorListOrder(groupKey,details){
|
||||
const loadedMonitors = s.group[groupKey].rawMonitorConfigurations
|
||||
var monitorListOrder = (details.monitorListOrder && details.monitorListOrder[0] ? details.monitorListOrder[0] : []).filter(monitorId => !!loadedMonitors[monitorId]);
|
||||
monitorListOrder = [...new Set(monitorListOrder)];
|
||||
return monitorListOrder
|
||||
}
|
||||
s.accountSettingsEdit = function(d,dontRunExtensions){
|
||||
s.knexQuery({
|
||||
action: "select",
|
||||
|
@ -302,6 +309,7 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
//admin permissions
|
||||
formDetails.permissions = details.permissions
|
||||
formDetails.max_camera = details.max_camera
|
||||
formDetails.edit_size = details.edit_size
|
||||
formDetails.edit_days = details.edit_days
|
||||
formDetails.use_admin = details.use_admin
|
||||
|
@ -358,7 +366,9 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
readStorageArray()
|
||||
///
|
||||
formDetails = JSON.stringify(s.mergeDeep(details,formDetails))
|
||||
formDetails = s.mergeDeep(details,formDetails)
|
||||
if(formDetails.monitorListOrder)formDetails.monitorListOrder[0] = filterMonitorListOrder(d.ke,formDetails);
|
||||
formDetailsString = JSON.stringify(s.mergeDeep(details,formDetails))
|
||||
///
|
||||
const updateQuery = {}
|
||||
if(form.pass && form.pass !== ''){
|
||||
|
@ -371,7 +381,7 @@ module.exports = function(s,config,lang){
|
|||
const value = form[key]
|
||||
updateQuery[key] = value
|
||||
})
|
||||
updateQuery.details = formDetails
|
||||
updateQuery.details = formDetailsString
|
||||
s.knexQuery({
|
||||
action: "update",
|
||||
table: "Users",
|
||||
|
@ -381,20 +391,20 @@ module.exports = function(s,config,lang){
|
|||
['uid','=',d.uid],
|
||||
]
|
||||
},() => {
|
||||
const user = Object.assign({ke : d.ke},form)
|
||||
if(!details.sub){
|
||||
var user = Object.assign(form,{ke : d.ke})
|
||||
var userDetails = JSON.parse(formDetails)
|
||||
s.group[d.ke].sizeLimit = parseFloat(newSize)
|
||||
resetAllStorageCounters(d.ke)
|
||||
if(!dontRunExtensions){
|
||||
s.onAccountSaveExtensions.forEach(function(extender){
|
||||
extender(s.group[d.ke],userDetails,user)
|
||||
})
|
||||
s.unloadGroupAppExtensions.forEach(function(extender){
|
||||
extender(user)
|
||||
})
|
||||
s.loadGroupApps(d)
|
||||
}
|
||||
}
|
||||
s.onAccountSaveExtensions.forEach(function(extender){
|
||||
extender(s.group[d.ke],formDetails,user)
|
||||
})
|
||||
if(d.cnid)s.tx({f:'user_settings_change',uid:d.uid,ke:d.ke,form:form},d.cnid)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
module.exports = function(s,config,lang,io){
|
||||
const {
|
||||
createAdminUser
|
||||
} = require("./utils.js")(s,config,lang);
|
||||
function checkStaticUser(staticUser){
|
||||
return new Promise((resolve,reject) => {
|
||||
const whereQuery = {
|
||||
mail: staticUser.mail
|
||||
}
|
||||
if(staticUser.ke)whereQuery.ke = staticUser.ke
|
||||
s.knexQuery({
|
||||
action: "select",
|
||||
columns: "mail,ke,uid",
|
||||
table: "Users",
|
||||
where: whereQuery,
|
||||
limit: 1,
|
||||
},function(err,users) {
|
||||
resolve(users[0])
|
||||
})
|
||||
})
|
||||
}
|
||||
async function checkForStaticUsers(){
|
||||
if(config.staticUsers){
|
||||
try{
|
||||
for (let i = 0; i < config.staticUsers.length; i++) {
|
||||
const staticUser = config.staticUsers[i]
|
||||
s.debugLog(`Checking Static User...`,staticUser.mail)
|
||||
const userExists = await checkStaticUser(staticUser)
|
||||
if(!userExists){
|
||||
s.debugLog(`Static User does not exist, creating...`)
|
||||
const creationResponse = await createAdminUser(staticUser)
|
||||
s.debugLog(`Static User created!`,creationResponse)
|
||||
}else{
|
||||
s.debugLog(`Static User exists!`)
|
||||
}
|
||||
}
|
||||
}catch(err){
|
||||
s.debugLog(`Static User check error!`,err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
checkForStaticUsers,
|
||||
}
|
||||
}
|
|
@ -451,6 +451,61 @@ module.exports = (s,config,lang) => {
|
|||
callback()
|
||||
}
|
||||
}
|
||||
function resetAllStorageCounters(groupKey){
|
||||
var storageIndexes = Object.keys(s.group[groupKey].addStorageUse)
|
||||
storageIndexes.forEach((storageIndex) => {
|
||||
s.setDiskUsedForGroupAddStorage(groupKey,{
|
||||
size: 0,
|
||||
storageIndex: storageIndex
|
||||
})
|
||||
})
|
||||
s.setDiskUsedForGroup(groupKey,0)
|
||||
}
|
||||
function createAdminUser(user){
|
||||
return new Promise((resolve,reject) => {
|
||||
const detailsColumn = Object.assign({
|
||||
"factorAuth":"0",
|
||||
"size": user.diskLimit || user.size || '',
|
||||
"days":"",
|
||||
"event_days":"",
|
||||
"log_days":"",
|
||||
"max_camera": user.cameraLimit || user.max_camera || '',
|
||||
"permissions":"all",
|
||||
"edit_size":"1",
|
||||
"edit_days":"1",
|
||||
"edit_event_days":"1",
|
||||
"edit_log_days":"1",
|
||||
"use_admin":"1",
|
||||
"use_aws_s3":"1",
|
||||
"use_whcs":"1",
|
||||
"use_sftp":"1",
|
||||
"use_webdav":"1",
|
||||
"use_discordbot":"1",
|
||||
"use_ldap":"1",
|
||||
"aws_use_global":"0",
|
||||
"b2_use_global":"0",
|
||||
"webdav_use_global":"0"
|
||||
},s.parseJSON(user.details) || {});
|
||||
const insertQuery = {
|
||||
ke: user.ke || s.gid(7),
|
||||
uid: user.uid || s.gid(6),
|
||||
mail: user.mail,
|
||||
pass: s.createHash(user.initialPassword || user.pass || s.gid()),
|
||||
details: JSON.stringify(detailsColumn)
|
||||
}
|
||||
s.knexQuery({
|
||||
action: "insert",
|
||||
table: "Users",
|
||||
insert: insertQuery
|
||||
},function(err,users) {
|
||||
resolve({
|
||||
ok: !err,
|
||||
inserted: !err ? insertQuery : undefined,
|
||||
err: err
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
return {
|
||||
deleteSetOfVideos: deleteSetOfVideos,
|
||||
deleteSetOfTimelapseFrames: deleteSetOfTimelapseFrames,
|
||||
|
@ -461,5 +516,7 @@ module.exports = (s,config,lang) => {
|
|||
deleteFileBinFiles: deleteFileBinFiles,
|
||||
deleteCloudVideos: deleteCloudVideos,
|
||||
deleteCloudTimelapseFrames: deleteCloudTimelapseFrames,
|
||||
resetAllStorageCounters: resetAllStorageCounters,
|
||||
createAdminUser: createAdminUser,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -187,7 +187,7 @@ module.exports = (s,config,lang) => {
|
|||
let fileExt = inputFilePath.split('.')
|
||||
fileExt = fileExt[fileExt.length -1]
|
||||
const filename = `${s.gid(10)}.${fileExt}`
|
||||
const videoOutPath = `${tempDirectory}`
|
||||
const videoOutPath = `${tempDirectory}${filename}`
|
||||
const cuttingProcess = spawn(config.ffmpegDir,['-loglevel','warning','-i', inputFilePath, '-c','copy','-t',`${cutLength}`,videoOutPath])
|
||||
cuttingProcess.stderr.on('data',(data) => {
|
||||
const err = data.toString()
|
||||
|
@ -196,6 +196,7 @@ module.exports = (s,config,lang) => {
|
|||
cuttingProcess.on('close',(data) => {
|
||||
fs.stat(videoOutPath,(err) => {
|
||||
if(!err){
|
||||
response.ok = true
|
||||
response.filename = filename
|
||||
response.filePath = videoOutPath
|
||||
setTimeout(() => {
|
||||
|
|
201
libs/videos.js
201
libs/videos.js
|
@ -2,6 +2,9 @@ var fs = require('fs');
|
|||
var exec = require('child_process').exec;
|
||||
var spawn = require('child_process').spawn;
|
||||
module.exports = function(s,config,lang){
|
||||
const {
|
||||
sendVideoToMasterNode,
|
||||
} = require('./childNode/childUtils.js')(s,config,lang)
|
||||
/**
|
||||
* Gets the video directory of the supplied video or monitor database row.
|
||||
* @constructor
|
||||
|
@ -95,105 +98,85 @@ module.exports = function(s,config,lang){
|
|||
if(!k)k={};
|
||||
e.dir = s.getVideoDirectory(e)
|
||||
k.dir = e.dir.toString()
|
||||
if(s.group[e.ke].activeMonitors[e.id].childNode){
|
||||
s.cx({
|
||||
f: 'insertCompleted',
|
||||
d: s.group[e.ke].rawMonitorConfigurations[e.id],
|
||||
k: k
|
||||
},s.group[e.ke].activeMonitors[e.id].childNodeId);
|
||||
}else{
|
||||
//get file directory
|
||||
const activeMonitor = s.group[e.ke].activeMonitors[e.id]
|
||||
//get file directory
|
||||
k.fileExists = fs.existsSync(k.dir+k.file)
|
||||
if(k.fileExists!==true){
|
||||
k.dir = s.dir.videos+'/'+e.ke+'/'+e.id+'/'
|
||||
k.fileExists = fs.existsSync(k.dir+k.file)
|
||||
if(k.fileExists!==true){
|
||||
k.dir = s.dir.videos+'/'+e.ke+'/'+e.id+'/'
|
||||
k.fileExists = fs.existsSync(k.dir+k.file)
|
||||
if(k.fileExists !== true){
|
||||
s.dir.addStorage.forEach(function(v){
|
||||
if(k.fileExists !== true){
|
||||
k.dir = s.checkCorrectPathEnding(v.path)+e.ke+'/'+e.id+'/'
|
||||
k.fileExists = fs.existsSync(k.dir+k.file)
|
||||
}
|
||||
})
|
||||
}
|
||||
if(k.fileExists !== true){
|
||||
s.dir.addStorage.forEach(function(v){
|
||||
if(k.fileExists !== true){
|
||||
k.dir = s.checkCorrectPathEnding(v.path)+e.ke+'/'+e.id+'/'
|
||||
k.fileExists = fs.existsSync(k.dir+k.file)
|
||||
}
|
||||
})
|
||||
}
|
||||
if(k.fileExists===true){
|
||||
//close video row
|
||||
k.details = k.details && k.details instanceof Object ? k.details : {}
|
||||
k.stat = fs.statSync(k.dir+k.file)
|
||||
k.filesize = k.stat.size
|
||||
k.filesizeMB = parseFloat((k.filesize/1048576).toFixed(2))
|
||||
}
|
||||
if(k.fileExists===true){
|
||||
//close video row
|
||||
k.details = k.details && k.details instanceof Object ? k.details : {}
|
||||
k.stat = fs.statSync(k.dir+k.file)
|
||||
k.filesize = k.stat.size
|
||||
k.filesizeMB = parseFloat((k.filesize/1048576).toFixed(2))
|
||||
|
||||
k.startTime = new Date(s.nameToTime(k.file))
|
||||
k.endTime = new Date(k.endTime || k.stat.mtime)
|
||||
if(config.useUTC === true){
|
||||
fs.rename(k.dir+k.file, k.dir+s.formattedTime(k.startTime)+'.'+e.ext, (err) => {
|
||||
if (err) return console.error(err);
|
||||
});
|
||||
k.filename = s.formattedTime(k.startTime)+'.'+e.ext
|
||||
}else{
|
||||
k.filename = k.file
|
||||
}
|
||||
if(!e.ext){e.ext = k.filename.split('.')[1]}
|
||||
//send event for completed recording
|
||||
const response = {
|
||||
k.startTime = new Date(s.nameToTime(k.file))
|
||||
k.endTime = new Date(k.endTime || k.stat.mtime)
|
||||
if(config.useUTC === true){
|
||||
fs.rename(k.dir+k.file, k.dir+s.formattedTime(k.startTime)+'.'+e.ext, (err) => {
|
||||
if (err) return console.error(err);
|
||||
});
|
||||
k.filename = s.formattedTime(k.startTime)+'.'+e.ext
|
||||
}else{
|
||||
k.filename = k.file
|
||||
}
|
||||
if(!e.ext){e.ext = k.filename.split('.')[1]}
|
||||
//send event for completed recording
|
||||
const response = {
|
||||
mid: e.mid,
|
||||
ke: e.ke,
|
||||
filename: k.filename,
|
||||
filesize: k.filesize,
|
||||
time: s.timeObject(k.startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
end: s.timeObject(k.endTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){
|
||||
var filePath = k.dir + k.filename;
|
||||
sendVideoToMasterNode(filePath,response)
|
||||
}else{
|
||||
var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename
|
||||
if(config.useUTC === true)href += '?isUTC=true';
|
||||
const monitorEventsCounted = activeMonitor.detector_motion_count
|
||||
s.txWithSubPermissions({
|
||||
f: 'video_build_success',
|
||||
hrefNoAuth: href,
|
||||
filename: k.filename,
|
||||
mid: e.mid,
|
||||
ke: e.ke,
|
||||
filename: k.filename,
|
||||
d: s.cleanMonitorObject(s.group[e.ke].rawMonitorConfigurations[e.id]),
|
||||
filesize: k.filesize,
|
||||
time: s.timeObject(k.startTime).format('YYYY-MM-DD HH:mm:ss'),
|
||||
end: s.timeObject(k.endTime).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){
|
||||
fs.createReadStream(k.dir+k.filename,{ highWaterMark: 500 })
|
||||
.on('data',function(data){
|
||||
s.cx(Object.assign(response,{
|
||||
f:'created_file_chunk',
|
||||
chunk: data,
|
||||
}))
|
||||
})
|
||||
.on('close',function(){
|
||||
clearTimeout(s.group[e.ke].activeMonitors[e.id].recordingChecker)
|
||||
clearTimeout(s.group[e.ke].activeMonitors[e.id].streamChecker)
|
||||
s.cx(Object.assign(response,{
|
||||
f:'created_file',
|
||||
}))
|
||||
time: k.startTime,
|
||||
size: k.filesize,
|
||||
end: k.endTime,
|
||||
events: monitorEventsCounted && monitorEventsCounted.length > 0 ? monitorEventsCounted : null
|
||||
},'GRP_'+e.ke,'video_view')
|
||||
//purge over max
|
||||
s.purgeDiskForGroup(e.ke)
|
||||
//send new diskUsage values
|
||||
var storageIndex = s.getVideoStorageIndex(e)
|
||||
if(storageIndex){
|
||||
s.setDiskUsedForGroupAddStorage(e.ke,{
|
||||
size: k.filesizeMB,
|
||||
storageIndex: storageIndex
|
||||
})
|
||||
}else{
|
||||
var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename
|
||||
if(config.useUTC === true)href += '?isUTC=true';
|
||||
const monitorEventsCounted = s.group[e.ke].activeMonitors[e.mid].detector_motion_count
|
||||
s.txWithSubPermissions({
|
||||
f: 'video_build_success',
|
||||
hrefNoAuth: href,
|
||||
filename: k.filename,
|
||||
mid: e.mid,
|
||||
ke: e.ke,
|
||||
time: k.startTime,
|
||||
size: k.filesize,
|
||||
end: k.endTime,
|
||||
events: monitorEventsCounted && monitorEventsCounted.length > 0 ? monitorEventsCounted : null
|
||||
},'GRP_'+e.ke,'video_view')
|
||||
//purge over max
|
||||
s.purgeDiskForGroup(e.ke)
|
||||
//send new diskUsage values
|
||||
var storageIndex = s.getVideoStorageIndex(e)
|
||||
if(storageIndex){
|
||||
s.setDiskUsedForGroupAddStorage(e.ke,{
|
||||
size: k.filesizeMB,
|
||||
storageIndex: storageIndex
|
||||
})
|
||||
}else{
|
||||
s.setDiskUsedForGroup(e.ke,k.filesizeMB)
|
||||
}
|
||||
s.onBeforeInsertCompletedVideoExtensions.forEach(function(extender){
|
||||
extender(e,k)
|
||||
})
|
||||
s.insertDatabaseRow(e,k,callback)
|
||||
s.insertCompletedVideoExtensions.forEach(function(extender){
|
||||
extender(e,k,response)
|
||||
})
|
||||
s.setDiskUsedForGroup(e.ke,k.filesizeMB)
|
||||
}
|
||||
s.onBeforeInsertCompletedVideoExtensions.forEach(function(extender){
|
||||
extender(e,k)
|
||||
})
|
||||
s.insertDatabaseRow(e,k,callback)
|
||||
s.insertCompletedVideoExtensions.forEach(function(extender){
|
||||
extender(e,k,response)
|
||||
})
|
||||
}
|
||||
}
|
||||
s.group[e.ke].activeMonitors[e.mid].detector_motion_count = []
|
||||
|
@ -237,7 +220,7 @@ module.exports = function(s,config,lang){
|
|||
filename: filename,
|
||||
mid: e.id,
|
||||
ke: e.ke,
|
||||
time: s.nameToTime(filename),
|
||||
time: new Date(s.nameToTime(filename)),
|
||||
end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')
|
||||
},'GRP_'+e.ke);
|
||||
var storageIndex = s.getVideoStorageIndex(e)
|
||||
|
@ -488,13 +471,13 @@ module.exports = function(s,config,lang){
|
|||
file.pipe(res)
|
||||
return file
|
||||
}
|
||||
s.createVideoFromTimelapse = function(timelapseFrames,framesPerSecond,callback){
|
||||
s.createVideoFromTimelapse = async function(timelapseFrames,framesPerSecond,callback){
|
||||
framesPerSecond = parseInt(framesPerSecond)
|
||||
if(!framesPerSecond || isNaN(framesPerSecond))framesPerSecond = 2
|
||||
var frames = timelapseFrames.reverse()
|
||||
var ke = frames[0].ke
|
||||
var mid = frames[0].mid
|
||||
var finalFileName = frames[0].filename.split('.')[0] + '_' + frames[frames.length - 1].filename.split('.')[0] + `-${framesPerSecond}fps`
|
||||
var finalFileName = `${s.md5(JSON.stringify(frames))}-${framesPerSecond}fps`
|
||||
var concatFiles = []
|
||||
var createLocation
|
||||
frames.forEach(function(frame,frameNumber){
|
||||
|
@ -530,7 +513,7 @@ module.exports = function(s,config,lang){
|
|||
if(currentFile === concatFiles.length - 1){
|
||||
videoBuildProcess.kill('SIGTERM')
|
||||
}
|
||||
},4000)
|
||||
},60000)
|
||||
})
|
||||
videoBuildProcess.on('exit',function(data){
|
||||
var timeNow = new Date()
|
||||
|
@ -564,12 +547,11 @@ module.exports = function(s,config,lang){
|
|||
if(!err)videoBuildProcess.stdin.write(buffer)
|
||||
if(currentFile === concatFiles.length - 1){
|
||||
//is last
|
||||
|
||||
}else{
|
||||
setTimeout(function(){
|
||||
setTimeout(async function(){
|
||||
++currentFile
|
||||
readFile()
|
||||
},1/framesPerSecond)
|
||||
},10/framesPerSecond)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -582,13 +564,22 @@ module.exports = function(s,config,lang){
|
|||
msg: lang['Started Building']
|
||||
})
|
||||
}else{
|
||||
callback({
|
||||
ok: false,
|
||||
fileExists: true,
|
||||
filename: finalFileName + '.mp4',
|
||||
fileLocation: finalMp4OutputLocation,
|
||||
msg: lang['Already exists']
|
||||
})
|
||||
if(s.group[ke].activeMonitors[mid].buildingTimelapseVideo){
|
||||
callback({
|
||||
ok: false,
|
||||
fileExists: false,
|
||||
fileLocation: finalMp4OutputLocation,
|
||||
msg: lang.Building
|
||||
})
|
||||
}else{
|
||||
callback({
|
||||
ok: false,
|
||||
fileExists: true,
|
||||
filename: finalFileName + '.mp4',
|
||||
fileLocation: finalMp4OutputLocation,
|
||||
msg: lang['Already exists']
|
||||
})
|
||||
}
|
||||
}
|
||||
}else{
|
||||
callback({
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
var fs = require('fs');
|
||||
var http = require('http');
|
||||
var https = require('https');
|
||||
var express = require('express');
|
||||
var app = express()
|
||||
const fs = require('fs');
|
||||
const url = require('url');
|
||||
const http = require('http');
|
||||
const https = require('https');
|
||||
const express = require('express');
|
||||
const app = express()
|
||||
module.exports = function(s,config,lang,io){
|
||||
//get page URL
|
||||
if(!config.baseURL){
|
||||
|
@ -103,10 +104,21 @@ module.exports = function(s,config,lang,io){
|
|||
})
|
||||
}
|
||||
//start HTTP
|
||||
const onHttpRequestUpgradeExtensions = s.onHttpRequestUpgradeExtensions;
|
||||
var server = http.createServer(app);
|
||||
server.listen(config.port,config.bindip,function(){
|
||||
console.log(lang.Shinobi+' : Web Server Listening on '+config.port);
|
||||
});
|
||||
server.on('upgrade', function upgrade(request, socket, head) {
|
||||
const pathname = url.parse(request.url).pathname;
|
||||
if(typeof onHttpRequestUpgradeExtensions[pathname] === 'function'){
|
||||
onHttpRequestUpgradeExtensions[pathname](request, socket, head)
|
||||
} else if (pathname.indexOf('/socket.io') > -1) {
|
||||
return;
|
||||
} else {
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
if(config.webPaths.home !== '/'){
|
||||
io.attach(server,{
|
||||
path:'/socket.io',
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var moment = require('moment')
|
||||
var request = require('request')
|
||||
var jsonfile = require("jsonfile")
|
||||
var exec = require('child_process').exec;
|
||||
var spawn = require('child_process').spawn;
|
||||
var execSync = require('child_process').execSync;
|
||||
|
@ -51,6 +49,8 @@ module.exports = function(s,config,lang,app){
|
|||
s.closeJsonResponse(res,endData)
|
||||
return
|
||||
}
|
||||
}else{
|
||||
updateQuery.mail = form.mail
|
||||
}
|
||||
}
|
||||
await s.knexQueryPromise({
|
||||
|
@ -96,6 +96,7 @@ module.exports = function(s,config,lang,app){
|
|||
*/
|
||||
app.all(config.webPaths.adminApiPrefix+':auth/accounts/:ke/delete', function (req,res){
|
||||
s.auth(req.params,function(user){
|
||||
const groupKey = req.params.ke;
|
||||
var endData = {
|
||||
ok : false
|
||||
}
|
||||
|
@ -106,47 +107,60 @@ module.exports = function(s,config,lang,app){
|
|||
}
|
||||
var form = s.getPostData(req) || {}
|
||||
var uid = form.uid || s.getPostData(req,'uid',false)
|
||||
var mail = form.mail || s.getPostData(req,'mail',false)
|
||||
s.knexQuery({
|
||||
action: "delete",
|
||||
table: "Users",
|
||||
where: {
|
||||
ke: req.params.ke,
|
||||
uid: uid,
|
||||
mail: mail,
|
||||
}
|
||||
})
|
||||
s.knexQuery({
|
||||
action: "select",
|
||||
columns: "*",
|
||||
table: "API",
|
||||
table: "Users",
|
||||
where: [
|
||||
['ke','=',req.params.ke],
|
||||
['ke','=',groupKey],
|
||||
['uid','=',uid],
|
||||
]
|
||||
},function(err,rows){
|
||||
if(rows && rows[0]){
|
||||
rows.forEach(function(row){
|
||||
delete(s.api[row.code])
|
||||
})
|
||||
},function(err,usersFound){
|
||||
const theUserUpForDeletion = usersFound[0]
|
||||
if(theUserUpForDeletion){
|
||||
s.knexQuery({
|
||||
action: "delete",
|
||||
table: "API",
|
||||
table: "Users",
|
||||
where: {
|
||||
ke: req.params.ke,
|
||||
ke: groupKey,
|
||||
uid: uid,
|
||||
}
|
||||
})
|
||||
s.knexQuery({
|
||||
action: "select",
|
||||
columns: "*",
|
||||
table: "API",
|
||||
where: [
|
||||
['ke','=',groupKey],
|
||||
['uid','=',uid],
|
||||
]
|
||||
},function(err,rows){
|
||||
if(rows && rows[0]){
|
||||
rows.forEach(function(row){
|
||||
delete(s.api[row.code])
|
||||
})
|
||||
s.knexQuery({
|
||||
action: "delete",
|
||||
table: "API",
|
||||
where: {
|
||||
ke: groupKey,
|
||||
uid: uid,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
s.tx({
|
||||
f: 'delete_sub_account',
|
||||
ke: groupKey,
|
||||
uid: uid,
|
||||
mail: theUserUpForDeletion.mail
|
||||
},'ADM_'+groupKey)
|
||||
endData.ok = true
|
||||
}else{
|
||||
endData.msg = user.lang['User Not Found']
|
||||
}
|
||||
s.closeJsonResponse(res,endData)
|
||||
})
|
||||
s.tx({
|
||||
f: 'delete_sub_account',
|
||||
ke: req.params.ke,
|
||||
uid: uid,
|
||||
mail: mail
|
||||
},'ADM_'+req.params.ke)
|
||||
endData.ok = true
|
||||
s.closeJsonResponse(res,endData)
|
||||
},res,req)
|
||||
})
|
||||
/**
|
||||
|
|
|
@ -3,7 +3,7 @@ var fs = require('fs');
|
|||
var bodyParser = require('body-parser');
|
||||
var os = require('os');
|
||||
var moment = require('moment');
|
||||
var request = require('request');
|
||||
var fetch = require('node-fetch');
|
||||
var execSync = require('child_process').execSync;
|
||||
var exec = require('child_process').exec;
|
||||
var spawn = require('child_process').spawn;
|
||||
|
@ -26,6 +26,10 @@ module.exports = function(s,config,lang,app,io){
|
|||
twoFactorVerification,
|
||||
ldapLogin,
|
||||
} = require('./auth/utils.js')(s,config,lang)
|
||||
const {
|
||||
spawnSubstreamProcess,
|
||||
destroySubstreamProcess,
|
||||
} = require('./monitor/utils.js')(s,config,lang)
|
||||
s.renderPage = function(req,res,paths,passables,callback){
|
||||
passables.window = {}
|
||||
passables.data = req.params
|
||||
|
@ -75,9 +79,16 @@ module.exports = function(s,config,lang,app,io){
|
|||
if(config.webPaths.home !== '/'){
|
||||
app.use('/libs',express.static(s.mainDirectory + '/web/libs'))
|
||||
}
|
||||
app.use(s.checkCorrectPathEnding(config.webPaths.home)+'libs',express.static(s.mainDirectory + '/web/libs'))
|
||||
app.use(s.checkCorrectPathEnding(config.webPaths.admin)+'libs',express.static(s.mainDirectory + '/web/libs'))
|
||||
app.use(s.checkCorrectPathEnding(config.webPaths.super)+'libs',express.static(s.mainDirectory + '/web/libs'))
|
||||
[
|
||||
[config.webPaths.home,'libs','/web/libs'],
|
||||
[config.webPaths.admin,'libs','/web/libs'],
|
||||
[config.webPaths.super,'libs','/web/libs'],
|
||||
[config.webPaths.home,'assets','/web/assets'],
|
||||
[config.webPaths.admin,'assets','/web/assets'],
|
||||
[config.webPaths.super,'assets','/web/assets'],
|
||||
].forEach((piece) => {
|
||||
app.use(s.checkCorrectPathEnding(piece[0])+piece[1],express.static(s.mainDirectory + piece[2]))
|
||||
})
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({extended: true}));
|
||||
app.use(function (req,res,next){
|
||||
|
@ -316,6 +327,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
})
|
||||
break;
|
||||
case'admin':
|
||||
// dash
|
||||
default:
|
||||
var chosenRender = 'home'
|
||||
if(userInfo.details.sub && userInfo.details.landing_page && userInfo.details.landing_page !== '' && config.renderPaths[userInfo.details.landing_page]){
|
||||
|
@ -424,9 +436,12 @@ module.exports = function(s,config,lang,app,io){
|
|||
$user:{
|
||||
ke: user.ke,
|
||||
uid: user.uid,
|
||||
mail: user.mail
|
||||
mail: user.mail,
|
||||
details: {
|
||||
sub: user.details.sub
|
||||
}
|
||||
},
|
||||
lang: user.lang
|
||||
lang: user.lang,
|
||||
})
|
||||
return;
|
||||
}
|
||||
|
@ -444,7 +459,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
failedAuthentication(req.body.function,req.body.mail)
|
||||
}
|
||||
}
|
||||
if(req.body.function === 'super'){
|
||||
if(req.body.function === 'super' && !config.superUserLoginDisabled){
|
||||
const superLoginResponse = await superLogin(req.body.mail,req.body.pass);
|
||||
if(superLoginResponse.ok){
|
||||
renderPage(config.renderPaths.super,{
|
||||
|
@ -757,6 +772,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
r[n].currentCpuUsage = activeMonitor.currentCpuUsage
|
||||
r[n].status = activeMonitor.monitorStatus
|
||||
r[n].code = activeMonitor.monitorStatusCode
|
||||
r[n].subStreamChannel = activeMonitor.subStreamChannel
|
||||
}
|
||||
var buildStreamURL = function(type,channelNumber){
|
||||
var streamURL
|
||||
|
@ -807,6 +823,43 @@ module.exports = function(s,config,lang,app,io){
|
|||
},res,req);
|
||||
});
|
||||
/**
|
||||
* API : Toggle Substream Process on and off
|
||||
*/
|
||||
app.get(config.webPaths.apiPrefix+':auth/toggleSubstream/:ke/:id', function (req,res){
|
||||
const response = {ok: false};
|
||||
s.auth(req.params,async (user) => {
|
||||
const groupKey = req.params.ke
|
||||
const monitorId = req.params.id
|
||||
if(
|
||||
user.permissions.control_monitors === "0" ||
|
||||
user.details.sub &&
|
||||
user.details.allmonitors !== '1' &&
|
||||
user.details.monitor_edit.indexOf(monitorId) === -1
|
||||
){
|
||||
response.msg = user.lang['Not Permitted']
|
||||
}else{
|
||||
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
const substreamConfig = monitorConfig.details.substream
|
||||
if(
|
||||
substreamConfig.output
|
||||
){
|
||||
if(!activeMonitor.subStreamProcess){
|
||||
response.ok = true
|
||||
activeMonitor.allowDestroySubstream = false;
|
||||
spawnSubstreamProcess(monitorConfig)
|
||||
}else{
|
||||
activeMonitor.allowDestroySubstream = true
|
||||
await destroySubstreamProcess(activeMonitor)
|
||||
}
|
||||
}else{
|
||||
response.msg = lang['Invalid Settings']
|
||||
}
|
||||
}
|
||||
s.closeJsonResponse(res,response);
|
||||
},res,req);
|
||||
});
|
||||
/**
|
||||
* API : Merge Recorded Videos into one file
|
||||
*/
|
||||
app.get(config.webPaths.apiPrefix+':auth/videosMerge/:ke', function (req,res){
|
||||
|
@ -897,6 +950,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
endTime: req.query.end,
|
||||
startTimeOperator: req.query.startOperator,
|
||||
endTimeOperator: req.query.endOperator,
|
||||
noLimit: req.query.noLimit,
|
||||
limit: req.query.limit,
|
||||
archived: req.query.archived,
|
||||
endIsStartTo: !!req.query.endIsStartTo,
|
||||
|
@ -932,29 +986,55 @@ module.exports = function(s,config,lang,app,io){
|
|||
const monitorId = req.params.id
|
||||
const groupKey = req.params.ke
|
||||
const hasRestrictions = userDetails.sub && userDetails.allmonitors !== '1';
|
||||
s.sqlQueryBetweenTimesWithPermissions({
|
||||
table: 'Events',
|
||||
user: user,
|
||||
groupKey: req.params.ke,
|
||||
monitorId: req.params.id,
|
||||
startTime: req.query.start,
|
||||
endTime: req.query.end,
|
||||
startTimeOperator: req.query.startOperator,
|
||||
endTimeOperator: req.query.endOperator,
|
||||
limit: req.query.limit,
|
||||
endIsStartTo: true,
|
||||
parseRowDetails: true,
|
||||
noFormat: true,
|
||||
noCount: true,
|
||||
rowName: 'events',
|
||||
preliminaryValidationFailed: (
|
||||
user.permissions.watch_videos === "0" ||
|
||||
hasRestrictions &&
|
||||
(!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1)
|
||||
)
|
||||
},(response) => {
|
||||
res.end(s.prettyPrint(response))
|
||||
})
|
||||
const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId)
|
||||
const preliminaryValidationFailed = (
|
||||
user.permissions.watch_videos === "0" ||
|
||||
hasRestrictions &&
|
||||
(!userDetails.video_view || userDetails.video_view.indexOf(monitorId)===-1)
|
||||
);
|
||||
if(req.query.onlyCount === '1' && !preliminaryValidationFailed){
|
||||
const response = {ok: true}
|
||||
s.knexQuery({
|
||||
action: "count",
|
||||
columns: "mid",
|
||||
table: "Events",
|
||||
where: [
|
||||
['ke','=',groupKey],
|
||||
['time','>=',req.query.start],
|
||||
['time','<=',req.query.end],
|
||||
monitorRestrictions
|
||||
]
|
||||
},(err,r) => {
|
||||
if(err){
|
||||
s.debugLog(err)
|
||||
response.ok = false
|
||||
}else{
|
||||
response.count = r[0]['count(`mid`)']
|
||||
}
|
||||
s.closeJsonResponse(res,response)
|
||||
})
|
||||
}else{
|
||||
s.sqlQueryBetweenTimesWithPermissions({
|
||||
table: 'Events',
|
||||
user: user,
|
||||
groupKey: req.params.ke,
|
||||
monitorId: req.params.id,
|
||||
startTime: req.query.start,
|
||||
endTime: req.query.end,
|
||||
startTimeOperator: req.query.startOperator,
|
||||
endTimeOperator: req.query.endOperator,
|
||||
noLimit: req.query.noLimit,
|
||||
limit: req.query.limit,
|
||||
endIsStartTo: true,
|
||||
parseRowDetails: true,
|
||||
noFormat: true,
|
||||
noCount: true,
|
||||
rowName: 'events',
|
||||
preliminaryValidationFailed: preliminaryValidationFailed
|
||||
},(response) => {
|
||||
res.end(s.prettyPrint(response))
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
/**
|
||||
|
@ -1197,7 +1277,11 @@ module.exports = function(s,config,lang,app,io){
|
|||
res.end(user.lang['File Not Found in Database'])
|
||||
})
|
||||
}else{
|
||||
req.pipe(request(r.href)).pipe(res)
|
||||
// req.pipe(request(r.href)).pipe(res)
|
||||
fetch(actualUrl).then(actual => {
|
||||
actual.headers.forEach((v, n) => res.setHeader(n, v));
|
||||
actual.body.pipe(res);
|
||||
})
|
||||
}
|
||||
}else{
|
||||
res.end(user.lang['File Not Found in Database'])
|
||||
|
@ -1282,6 +1366,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
const groupKey = req.params.ke
|
||||
const monitorId = req.params.id
|
||||
const monitorRestrictions = s.getMonitorRestrictions(user.details,monitorId)
|
||||
|
||||
if(user.details.sub && user.details.allmonitors === '0' && monitorRestrictions.length === 0){
|
||||
s.closeJsonResponse(res,{
|
||||
ok: false,
|
||||
|
@ -1289,13 +1374,15 @@ module.exports = function(s,config,lang,app,io){
|
|||
})
|
||||
return
|
||||
}
|
||||
|
||||
const d = {
|
||||
id: req.params.id,
|
||||
ke: req.params.ke
|
||||
}
|
||||
|
||||
if(req.query.data){
|
||||
try{
|
||||
var d = {
|
||||
id: req.params.id,
|
||||
ke: req.params.ke,
|
||||
details: s.parseJSON(req.query.data)
|
||||
}
|
||||
Object.assign(d, {details: s.parseJSON(req.query.data)});
|
||||
}catch(err){
|
||||
s.closeJsonResponse(res,{
|
||||
ok: false,
|
||||
|
@ -1303,7 +1390,19 @@ module.exports = function(s,config,lang,app,io){
|
|||
})
|
||||
return
|
||||
}
|
||||
}else{
|
||||
}
|
||||
// fallback for cameras that doesn't support JSON in query parameters ( i.e Sercom ICamera1000 will fail to save HTTP_Notifications as invalid url)
|
||||
else if( req.query.plug && req.query.name && req.query.reason && req.query.confidence) {
|
||||
Object.assign(d, {
|
||||
details: {
|
||||
plug: req.query.plug,
|
||||
reason: req.query.reason,
|
||||
confidence: req.query.confidence,
|
||||
name: req.query.name
|
||||
}
|
||||
});
|
||||
}
|
||||
else{
|
||||
s.closeJsonResponse(res,{
|
||||
ok: false,
|
||||
msg: user.lang['No Data']
|
||||
|
|
|
@ -3,7 +3,6 @@ var fs = require('fs');
|
|||
var bodyParser = require('body-parser');
|
||||
var os = require('os');
|
||||
var moment = require('moment');
|
||||
var request = require('request');
|
||||
var execSync = require('child_process').execSync;
|
||||
var exec = require('child_process').exec;
|
||||
var spawn = require('child_process').spawn;
|
||||
|
@ -117,10 +116,11 @@ module.exports = function(s,config,lang,app){
|
|||
}
|
||||
|
||||
var Emitter
|
||||
const chosenChannel = parseInt(req.params.channel) + config.pipeAddition
|
||||
if(!req.params.channel){
|
||||
Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitter
|
||||
}else{
|
||||
Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition]
|
||||
Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitterChannel[chosenChannel]
|
||||
}
|
||||
res.writeHead(200, {
|
||||
'Content-Type': 'multipart/x-mixed-replace; boundary=shinobi',
|
||||
|
@ -168,9 +168,13 @@ module.exports = function(s,config,lang,app){
|
|||
* API : Get HLS Stream
|
||||
*/
|
||||
app.get([config.webPaths.apiPrefix+':auth/hls/:ke/:id/:file',config.webPaths.apiPrefix+':auth/hls/:ke/:id/:channel/:file'], function (req,res){
|
||||
req.fn=function(user){
|
||||
s.auth(req.params,function(user){
|
||||
s.checkChildProxy(req.params,function(){
|
||||
noCache(res)
|
||||
if(user.permissions.watch_stream==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){
|
||||
res.end(user.lang['Not Permitted'])
|
||||
return
|
||||
}
|
||||
req.dir=s.dir.streams+req.params.ke+'/'+req.params.id+'/'
|
||||
if(req.params.channel){
|
||||
req.dir+='channel'+(parseInt(req.params.channel)+config.pipeAddition)+'/'+req.params.file;
|
||||
|
@ -184,8 +188,7 @@ module.exports = function(s,config,lang,app){
|
|||
res.end(lang['File Not Found'])
|
||||
}
|
||||
},res,req)
|
||||
}
|
||||
s.auth(req.params,req.fn,res,req);
|
||||
},res,req);
|
||||
})
|
||||
/**
|
||||
* API : Get JPEG Snapshot
|
||||
|
@ -289,50 +292,6 @@ module.exports = function(s,config,lang,app){
|
|||
},res,req)
|
||||
})
|
||||
/**
|
||||
* API : Get H.265/h265 HEVC stream
|
||||
*/
|
||||
app.get([config.webPaths.apiPrefix+':auth/h265/:ke/:id/s.hevc',config.webPaths.apiPrefix+':auth/h265/:ke/:id/:channel/s.hevc'], function(req,res) {
|
||||
s.auth(req.params,function(user){
|
||||
s.checkChildProxy(req.params,function(){
|
||||
noCache(res)
|
||||
var Emitter,chunkChannel
|
||||
if(!req.params.channel){
|
||||
Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitter
|
||||
chunkChannel = 'MAIN'
|
||||
}else{
|
||||
Emitter = s.group[req.params.ke].activeMonitors[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition]
|
||||
chunkChannel = parseInt(req.params.channel)+config.pipeAddition
|
||||
}
|
||||
//variable name of contentWriter
|
||||
var contentWriter
|
||||
//set headers
|
||||
res.setHeader('Content-Type', 'video/mp4');
|
||||
res.setHeader('Access-Control-Allow-Origin','*');
|
||||
var ip = s.getClientIp(req)
|
||||
s.camera('watch_on',{
|
||||
id : req.params.id,
|
||||
ke : req.params.ke
|
||||
},{
|
||||
id : req.params.auth + ip + req.headers['user-agent']
|
||||
})
|
||||
//write new frames as they happen
|
||||
Emitter.on('data',contentWriter=function(buffer){
|
||||
res.write(buffer)
|
||||
})
|
||||
//remove contentWriter when client leaves
|
||||
res.on('close', function () {
|
||||
Emitter.removeListener('data',contentWriter)
|
||||
s.camera('watch_off',{
|
||||
id : req.params.id,
|
||||
ke : req.params.ke
|
||||
},{
|
||||
id : req.params.auth + ip + req.headers['user-agent']
|
||||
})
|
||||
})
|
||||
},res,req)
|
||||
},res,req)
|
||||
})
|
||||
/**
|
||||
* API : Get H.264 over HTTP
|
||||
*/
|
||||
app.get([
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
var fs = require('fs');
|
||||
var os = require('os');
|
||||
var moment = require('moment')
|
||||
var request = require('request')
|
||||
var exec = require('child_process').exec;
|
||||
var spawn = require('child_process').spawn;
|
||||
var execSync = require('child_process').execSync;
|
||||
|
|
File diff suppressed because it is too large
Load Diff
35
package.json
35
package.json
|
@ -18,40 +18,43 @@
|
|||
"aws-sdk": "^2.731.0",
|
||||
"backblaze-b2": "^0.9.12",
|
||||
"body-parser": "^1.19.0",
|
||||
"bson": "^4.6.1",
|
||||
"connection-tester": "^0.2.0",
|
||||
"cws": "^1.2.11",
|
||||
"discord.js": "^12.2.0",
|
||||
"cws": "^2.0.0",
|
||||
"digest-fetch": "^1.2.1",
|
||||
"discord.js": "^13.6.0",
|
||||
"ejs": "^2.5.5",
|
||||
"express": "^4.16.4",
|
||||
"express-fileupload": "^1.1.6-alpha.6",
|
||||
"fs-extra": "9.0.1",
|
||||
"ftp-srv": "^4.4.0",
|
||||
"googleapis": "^71.0.0",
|
||||
"googleapis": "^100.0.0",
|
||||
"http-proxy": "^1.17.0",
|
||||
"jsonfile": "^3.0.1",
|
||||
"knex": "^0.21.4",
|
||||
"ldapauth-fork": "^5.0.1",
|
||||
"ldapauth-fork": "^5.0.2",
|
||||
"moment": "^2.27.0",
|
||||
"mp4frag": "^0.2.0",
|
||||
"mysql": "^2.18.1",
|
||||
"node-fetch": "3.0.0-beta.9",
|
||||
"node-ssh": "^11.1.1",
|
||||
"node-abort-controller": "^3.0.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-ssh": "^12.0.4",
|
||||
"node-telegram-bot-api": "^0.52.0",
|
||||
"nodemailer": "^6.4.11",
|
||||
"node-pushover": "^1.0.0",
|
||||
"pam-diff": "^1.0.0",
|
||||
"pam-diff": "^1.1.0",
|
||||
"path": "^0.12.7",
|
||||
"pipe2pam": "^0.6.2",
|
||||
"request": "^2.88.0",
|
||||
"pixel-change": "^1.1.0",
|
||||
"pushover-notifications": "^1.2.2",
|
||||
"sat": "^0.7.1",
|
||||
"shinobi-onvif": "0.1.9",
|
||||
"shinobi-sound-detection": "^0.1.8",
|
||||
"smtp-server": "^3.5.0",
|
||||
"socket.io": "^2.3.0",
|
||||
"socket.io-client": "^2.3.0",
|
||||
"socket.io": "^4.4.1",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"unzipper": "0.10.11",
|
||||
"webdav-fs": "^3.0.0"
|
||||
"webdav-fs": "^4.0.1"
|
||||
},
|
||||
"bin": "camera.js",
|
||||
"scripts": {
|
||||
|
@ -66,12 +69,12 @@
|
|||
},
|
||||
"pkg": {
|
||||
"targets": [
|
||||
"node12"
|
||||
"node16"
|
||||
],
|
||||
"scripts": [
|
||||
"libs/cameraThread/detector.js",
|
||||
"libs/cameraThread/singleCamera.js",
|
||||
"libs/cameraThread/snapshot.js"
|
||||
"libs/cameraThread/detector.js",
|
||||
"libs/cameraThread/singleCamera.js",
|
||||
"libs/cameraThread/snapshot.js"
|
||||
],
|
||||
"assets": [
|
||||
"definitions/**/*",
|
||||
|
|
|
@ -28,7 +28,7 @@ $(document).ready(function(){
|
|||
<div class="face-image-bg" style="background-image:url('${superApiPrefix}${$user.sessionKey}/faceManager/image/${name}/${image}')">
|
||||
<div class="controls row m-0">
|
||||
<div class="col p-0">
|
||||
<a href="#" class="btn btn-sm btn-danger m-0 delete"><i class="fa fa-trash-o"></i></a>
|
||||
<a class="btn btn-sm btn-danger m-0 delete"><i class="fa fa-trash-o"></i></a>
|
||||
</div>
|
||||
<div class="col p-0 text-right">
|
||||
<span class="badge badge-sm bg-dark pull-right">${name}</span>
|
||||
|
|
|
@ -304,7 +304,7 @@ module.exports = function(__dirname, config){
|
|||
if(config.mode === 'host'){
|
||||
plugLog('Plugin started as Host')
|
||||
//start plugin as host
|
||||
var io = require('socket.io')(server,{
|
||||
const io = new (require('socket.io').Server)(server,{
|
||||
transports: ['websocket']
|
||||
})
|
||||
io.engine.ws = new (require('cws').Server)({
|
||||
|
|
|
@ -1,37 +1,56 @@
|
|||
#!/bin/bash
|
||||
echo "ARM CPU Installation is currently NOT supported! Jetson Nano with GPU enabled is currently only supported."
|
||||
echo "Jetson Nano may experience \"Unsupported Errors\", you may ignore them. Patches will be applied."
|
||||
if [[ ! $(head -1 /etc/nv_tegra_release) =~ R32.*4\.[34] ]] ; then
|
||||
echo "ERROR: not JetPack-4.4"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cudaCompute=$(cat /sys/module/tegra_fuse/parameters/tegra_chip_id)
|
||||
export cudaCompute
|
||||
# 33 : Nano, TX1
|
||||
# 24 : TX2
|
||||
# 25 : Xavier NX and AGX Xavier
|
||||
|
||||
DIR=$(dirname $0)
|
||||
echo $DIR
|
||||
DIR=$(dirname "${0}")
|
||||
|
||||
echo "Replacing package.json for tfjs 2.3.0..."
|
||||
wget -O $DIR/package.json https://cdn.shinobi.video/binaries/tensorflow/2.3.0/package.json
|
||||
cp "${DIR}/package-jetson.json" "${DIR}/package.json"
|
||||
|
||||
echo "Removing existing Tensorflow Node.js modules..."
|
||||
rm -rf $DIR/node_modules
|
||||
rm -rf "${DIR}/node_modules"
|
||||
|
||||
echo "Installing Yarn package manager"
|
||||
npm install yarn -g --unsafe-perm --force
|
||||
npm install dotenv
|
||||
|
||||
[ -d "${DIR}/tfjs-tfjs-v2.3.0" ] && echo "Removing existing Tensorflow source directory" && rm -rf "${DIR}/tfjs-tfjs-v2.3.0"
|
||||
|
||||
npm install @tensorflow/tfjs-backend-cpu@2.3.0 @tensorflow/tfjs-backend-webgl@2.3.0 @tensorflow/tfjs-converter@2.3.0 @tensorflow/tfjs-core@2.3.0 @tensorflow/tfjs-layers@2.3.0 @tensorflow/tfjs-node@2.3.0 --unsafe-perm --force --legacy-peer-deps
|
||||
npm install @tensorflow/tfjs-node-gpu@2.3.0 --unsafe-perm
|
||||
customBinaryLocation="node_modules/@tensorflow/tfjs-node-gpu/scripts/custom-binary.json"
|
||||
echo '{"tf-lib": "https://cdn.shinobi.video/binaries/tensorflow/2.3.0/libtensorflow.tar.gz"}' > "$customBinaryLocation"
|
||||
npm rebuild @tensorflow/tfjs-node-gpu --build-addon-from-source --unsafe-perm
|
||||
echo "Downloading Tensorflow source tarball"
|
||||
wget -O "${DIR}/tfjs-v2.3.0.tar.gz" https://github.com/tensorflow/tfjs/archive/refs/tags/tfjs-v2.3.0.tar.gz
|
||||
|
||||
echo "Extracting and preparing Tensorflow source"
|
||||
tar -xf tfjs-v2.3.0.tar.gz -C "${DIR}"
|
||||
(cd "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu" || exit ; ./prep-gpu.sh)
|
||||
|
||||
# Little hack to make it run on jetson and download precompiled binaries
|
||||
sed -i "s/os.arch() === 'arm'/os.arch() === 'arm64'/ig" "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu/scripts/install.js"
|
||||
sed -i "s/https:\/\/storage.googleapis.com\/tf-builds\/libtensorflow_r1_14_linux_arm.tar.gz/https:\/\/cdn.shinobi.video\/binaries\/tensorflow\/2.3.0\/libtensorflow.tar.gz/ig" "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu/scripts/install.js"
|
||||
|
||||
echo "Building Tensorflow Node GPU package"
|
||||
(cd "${DIR}/tfjs-tfjs-v2.3.0/tfjs-node-gpu" || exit ; yarn && yarn build)
|
||||
|
||||
echo "Removing Tensorflow source tarball"
|
||||
rm -f "${DIR}/tfjs-v2.3.0.tar.gz"
|
||||
|
||||
echo "Installing Tensorflow addon"
|
||||
yarn
|
||||
|
||||
echo "Clean Yarn cache"
|
||||
yarn cache clean --all
|
||||
|
||||
# # npm audit fix --force
|
||||
if [ ! -e "$DIR/conf.json" ]; then
|
||||
if [ ! -e "${DIR}/conf.json" ]; then
|
||||
echo "Creating conf.json"
|
||||
sudo cp $DIR/conf.sample.json $DIR/conf.json
|
||||
sudo cp "${DIR}/conf.sample.json" "${DIR}/conf.json"
|
||||
else
|
||||
echo "conf.json already exists..."
|
||||
fi
|
||||
|
@ -39,7 +58,12 @@ fi
|
|||
tfjsBuildVal="gpu"
|
||||
|
||||
echo "Adding Random Plugin Key to Main Configuration"
|
||||
node $DIR/../../tools/modifyConfigurationForPlugin.js tensorflow key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}') tfjsBuild=$tfjsBuildVal
|
||||
node "${DIR}/../../tools/modifyConfigurationForPlugin.js" tensorflow key="$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}')" tfjsBuild="${tfjsBuildVal}"
|
||||
|
||||
echo "TF_FORCE_GPU_ALLOW_GROWTH=true" > "$DIR/.env"
|
||||
echo "#CUDA_VISIBLE_DEVICES=0,2" >> "$DIR/.env"
|
||||
echo "TF_FORCE_GPU_ALLOW_GROWTH=true" > "${DIR}/.env"
|
||||
echo "#CUDA_VISIBLE_DEVICES=0,2" >> "${DIR}/.env"
|
||||
|
||||
echo "Instalation finished"
|
||||
echo "For plugin automatic start run: pm2 start shinobi-tensorflow.js && pm2 save"
|
||||
echo "Or run manualy: node shinobi-tensorflow.js"
|
||||
echo "To make Shinobi avare of new plugin, restart it by: pm2 restart camera"
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "shinobi-tensorflow",
|
||||
"author": "Shinob Systems, Moinul Alam",
|
||||
"version": "1.0.5",
|
||||
"description": "Object Detection plugin based on @tensorflow/tfjs-node",
|
||||
"main": "shinobi-tensorflow.js",
|
||||
"dependencies": {
|
||||
"@tensorflow-models/coco-ssd": "2.2.1",
|
||||
"@tensorflow/tfjs": "2.3.0",
|
||||
"@tensorflow/tfjs-backend-cpu": "2.3.0",
|
||||
"@tensorflow/tfjs-backend-webgl": "2.3.0",
|
||||
"@tensorflow/tfjs-converter": "2.3.0",
|
||||
"@tensorflow/tfjs-core": "2.3.0",
|
||||
"@tensorflow/tfjs-layers": "2.3.0",
|
||||
"@tensorflow/tfjs-node-gpu": "file:tfjs-tfjs-v2.3.0/tfjs-node-gpu",
|
||||
"alea": "^1.0.1",
|
||||
"dotenv": "^8.6.0",
|
||||
"express": "4.16.2",
|
||||
"moment": "2.19.2",
|
||||
"random-seedable": "^1.0.8",
|
||||
"seedrandom": "2.4.3",
|
||||
"rimraf": "^3.0.2",
|
||||
"socket.io": "2.0.4",
|
||||
"socket.io-client": "1.7.4",
|
||||
"xor128": "^0.1.0"
|
||||
},
|
||||
"bin": "shinobi-tensorflow.js",
|
||||
"scripts": {
|
||||
"package": "pkg package.json -t linux,macos,win --out-path dist",
|
||||
"package-x64": "pkg package.json -t linux-x64,macos-x64,win-x64 --out-path dist/x64",
|
||||
"package-x86": "pkg package.json -t linux-x86,macos-x86,win-x86 --out-path dist/x86",
|
||||
"package-armv6": "pkg package.json -t linux-armv6,macos-armv6,win-armv6 --out-path dist/armv6",
|
||||
"package-armv7": "pkg package.json -t linux-armv7,macos-armv7,win-armv7 --out-path dist/armv7",
|
||||
"package-arm64": "pkg package.json -t linux-arm64,macos-arm64,win-arm64 --out-path dist/arm64",
|
||||
"package-all": "npm run package && npm run package-x64 && npm run package-x86 && npm run package-armv6 && npm run package-armv7 && npm run package-arm64"
|
||||
},
|
||||
"pkg": {
|
||||
"targets": [
|
||||
"node16"
|
||||
],
|
||||
"scripts": [
|
||||
"../pluginBase.js"
|
||||
],
|
||||
"assets": []
|
||||
},
|
||||
"disabled": false
|
||||
}
|
|
@ -5,15 +5,18 @@
|
|||
"description": "Object Detection plugin based on @tensorflow/tfjs-node",
|
||||
"main": "shinobi-tensorflow.js",
|
||||
"dependencies": {
|
||||
"@tensorflow-models/coco-ssd": "^2.1.0",
|
||||
"@tensorflow/tfjs-converter": "^2.7.0",
|
||||
"@tensorflow/tfjs-core": "^2.7.0",
|
||||
"@tensorflow/tfjs-layers": "^2.7.0",
|
||||
"@tensorflow/tfjs-node": "^2.7.0",
|
||||
"@tensorflow/tfjs-node-gpu": "^2.7.0",
|
||||
"@tensorflow-models/coco-ssd": "^2.2.2",
|
||||
"@tensorflow/tfjs-converter": "^3.13.0",
|
||||
"@tensorflow/tfjs-core": "^3.13.0",
|
||||
"@tensorflow/tfjs-layers": "^3.13.0",
|
||||
"@tensorflow/tfjs-node": "^3.13.0",
|
||||
"@tensorflow/tfjs-node-gpu": "^3.13.0",
|
||||
"@tensorflow/tfjs-backend-cpu": "^3.13.0",
|
||||
"@tensorflow/tfjs-backend-webgl": "^3.13.0",
|
||||
"moment": "^2.29.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"cws": "^2.0.0",
|
||||
"express": "^4.16.2",
|
||||
"moment": "^2.19.2",
|
||||
"socket.io": "^2.3.0",
|
||||
"socket.io-client": "^2.3.0"
|
||||
},
|
||||
|
|
|
@ -4,11 +4,13 @@
|
|||
"description": "YoloV3 plugin for Shinobi that uses C++ functions for detection.",
|
||||
"main": "shinobi-yolo.js",
|
||||
"dependencies": {
|
||||
"socket.io-client": "^1.7.4",
|
||||
"express": "^4.16.2",
|
||||
"moment": "^2.19.2",
|
||||
"socket.io": "^2.0.4",
|
||||
"imagickal": "^4.0.0",
|
||||
"moment": "^2.29.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"cws": "^2.0.0",
|
||||
"express": "^4.16.4",
|
||||
"socket.io": "^4.4.1",
|
||||
"socket.io-client": "^4.4.1",
|
||||
"node-yolo-shinobi": "^2.0.3"
|
||||
},
|
||||
"devDependencies": {},
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
module.exports = function(s,config,lang,getSnapshot){
|
||||
const {
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
const onEventTrigger = async (d,filter) => {
|
||||
console.log('CUSTOM COMMAND ON EVENT eventBasedRecording')
|
||||
const monitorConfig = s.group[d.ke].rawMonitorConfigurations[d.id]
|
||||
let videoPath = null
|
||||
let videoName = null
|
||||
console.log('await eventBasedRecording')
|
||||
const eventBasedRecording = await getEventBasedRecordingUponCompletion({
|
||||
ke: d.ke,
|
||||
mid: d.mid
|
||||
})
|
||||
console.log('complete eventBasedRecording')
|
||||
console.log(eventBasedRecording)
|
||||
if(eventBasedRecording.filePath){
|
||||
videoPath = eventBasedRecording.filePath
|
||||
videoName = eventBasedRecording.filename
|
||||
}else{
|
||||
const siftedVideoFileFromRam = await s.mergeDetectorBufferChunks(d)
|
||||
console.log('siftedVideoFileFromRam')
|
||||
console.log(siftedVideoFileFromRam)
|
||||
videoPath = siftedVideoFileFromRam.filePath
|
||||
videoName = siftedVideoFileFromRam.filename
|
||||
}
|
||||
}
|
||||
s.onEventTrigger(onEventTrigger)
|
||||
}
|
|
@ -1,13 +1,173 @@
|
|||
const fs = require('fs')
|
||||
const definitionFile = process.cwd() + '/definitions/en_CA.js'
|
||||
const newDefinitionFile = process.cwd() + '/tools/en_CA.js'
|
||||
const languagesFile = process.cwd() + '/languages/en_CA.json'
|
||||
const newLanguagesFile = process.cwd() + '/tools/en_CA.json'
|
||||
const languagesData = require(languagesFile)
|
||||
var definitonsRawData = fs.readFileSync(definitionFile).toString()
|
||||
const definitionData = require(definitionFile)({
|
||||
gid: () => {return 'randomId'},
|
||||
listOfStorage: [],
|
||||
},
|
||||
{},
|
||||
{
|
||||
timeZones: [
|
||||
{
|
||||
"text": "UTC−12:00, Y",
|
||||
"value": -720
|
||||
},
|
||||
{
|
||||
"text": "UTC−11:00, X",
|
||||
"value": -660
|
||||
},
|
||||
{
|
||||
"text": "UTC−10:00, W",
|
||||
"value": -600
|
||||
},
|
||||
{
|
||||
"text": "UTC−09:30, V†",
|
||||
"value": -570
|
||||
},
|
||||
{
|
||||
"text": "UTC−09:00, V",
|
||||
"value": -540
|
||||
},
|
||||
{
|
||||
"text": "UTC−08:00, U",
|
||||
"value": -480
|
||||
},
|
||||
{
|
||||
"text": "UTC−07:00, T",
|
||||
"value": -420
|
||||
},
|
||||
{
|
||||
"text": "UTC−06:00, S",
|
||||
"value": -360
|
||||
},
|
||||
{
|
||||
"text": "UTC−05:00, R",
|
||||
"value": -300
|
||||
},
|
||||
{
|
||||
"text": "UTC−04:00, Q",
|
||||
"value": -240
|
||||
},
|
||||
{
|
||||
"text": "UTC−03:30, P†",
|
||||
"value": -210
|
||||
},
|
||||
{
|
||||
"text": "UTC−03:00, P",
|
||||
"value": -180
|
||||
},
|
||||
{
|
||||
"text": "UTC−02:00, O",
|
||||
"value": -120
|
||||
},
|
||||
{
|
||||
"text": "UTC−01:00, N",
|
||||
"value": -60
|
||||
},
|
||||
{
|
||||
"text": "UTC±00:00, Z",
|
||||
"value": 0,
|
||||
"selected": true
|
||||
},
|
||||
{
|
||||
"text": "UTC+01:00, A",
|
||||
"value": 60
|
||||
},
|
||||
{
|
||||
"text": "UTC+02:00, B",
|
||||
"value": 120
|
||||
},
|
||||
{
|
||||
"text": "UTC+03:00, C",
|
||||
"value": 180
|
||||
},
|
||||
{
|
||||
"text": "UTC+03:30, C†",
|
||||
"value": 210
|
||||
},
|
||||
{
|
||||
"text": "UTC+04:00, D",
|
||||
"value": 240
|
||||
},
|
||||
{
|
||||
"text": "UTC+04:30, D†",
|
||||
"value": 270
|
||||
},
|
||||
{
|
||||
"text": "UTC+05:00, E",
|
||||
"value": 300
|
||||
},
|
||||
{
|
||||
"text": "UTC+05:30, E†",
|
||||
"value": 330
|
||||
},
|
||||
{
|
||||
"text": "UTC+05:45, E*",
|
||||
"value": 345
|
||||
},
|
||||
{
|
||||
"text": "UTC+06:00, F",
|
||||
"value": 360
|
||||
},
|
||||
{
|
||||
"text": "UTC+06:30, F†",
|
||||
"value": 390
|
||||
},
|
||||
{
|
||||
"text": "UTC+07:00, G",
|
||||
"value": 420
|
||||
},
|
||||
{
|
||||
"text": "UTC+08:00, H",
|
||||
"value": 480
|
||||
},
|
||||
{
|
||||
"text": "UTC+08:45, H*",
|
||||
"value": 525
|
||||
},
|
||||
{
|
||||
"text": "UTC+09:00, I",
|
||||
"value": 540
|
||||
},
|
||||
{
|
||||
"text": "UTC+09:30, I†",
|
||||
"value": 570
|
||||
},
|
||||
{
|
||||
"text": "UTC+10:00, K",
|
||||
"value": 600
|
||||
},
|
||||
{
|
||||
"text": "UTC+10:30, K†",
|
||||
"value": 630
|
||||
},
|
||||
{
|
||||
"text": "UTC+11:00, L",
|
||||
"value": 660
|
||||
},
|
||||
{
|
||||
"text": "UTC+12:00, M",
|
||||
"value": 720
|
||||
},
|
||||
{
|
||||
"text": "UTC+12:45, M*",
|
||||
"value": 765
|
||||
},
|
||||
{
|
||||
"text": "UTC+13:00, M†",
|
||||
"value": 780
|
||||
},
|
||||
{
|
||||
"text": "UTC+14:00, M†",
|
||||
"value": 840
|
||||
}
|
||||
]
|
||||
},
|
||||
languagesData
|
||||
)
|
||||
);
|
||||
|
||||
const capitalize = (s) => {
|
||||
if (typeof s !== 'string') return ''
|
||||
|
@ -15,16 +175,34 @@ const capitalize = (s) => {
|
|||
}
|
||||
const capitalizeAllWords = (string) => {
|
||||
let firstPart = ``
|
||||
let secondPart = ``
|
||||
let thirdPart = ``
|
||||
let newString = ``
|
||||
string.split(' ').forEach((part) => {
|
||||
string
|
||||
.replace(/"/g,'')
|
||||
.split(' ').forEach((part) => {
|
||||
firstPart += capitalize(part)
|
||||
})
|
||||
firstPart.split('_').forEach((part) => {
|
||||
secondPart += capitalize(part)
|
||||
})
|
||||
secondPart.split('-').forEach((part) => {
|
||||
thirdPart += capitalize(part)
|
||||
})
|
||||
thirdPart.split('=').forEach((part) => {
|
||||
newString += capitalize(part)
|
||||
})
|
||||
return newString
|
||||
}
|
||||
|
||||
function replaceTextWithLandParam(langText,langParam){
|
||||
if(definitonsRawData.indexOf(`"${langText}"`) > -1){
|
||||
definitonsRawData = definitonsRawData.replace(`"${langText}"`,`lang["${langParam}"]`)
|
||||
console.log('Replacing : ',definitonsRawData.indexOf(`"${langText}"`),`"${langText}"`,langParam)
|
||||
}else if(definitonsRawData.indexOf(`'${langText}'`) > -1){
|
||||
definitonsRawData = definitonsRawData.replace(`'${langText}'`,`lang["${langParam}"]`)
|
||||
console.log('Replacing : ',definitonsRawData.indexOf(`'${langText}'`),`'${langText}'`,langParam)
|
||||
}
|
||||
}
|
||||
const processSection = (section) => {
|
||||
try{
|
||||
if(section.info){
|
||||
|
@ -38,6 +216,7 @@ const processSection = (section) => {
|
|||
const langParam = `fieldText` + capitalizeAllWords(cleanName)
|
||||
const langText = field.description
|
||||
newLangParams[langParam] = langText
|
||||
replaceTextWithLandParam(langText,langParam)
|
||||
}
|
||||
if(field.possible instanceof Array){
|
||||
field.possible.forEach((possibility) => {
|
||||
|
@ -45,6 +224,7 @@ const processSection = (section) => {
|
|||
const langParam = `fieldText` + capitalizeAllWords(cleanName) + capitalizeAllWords(possibility.name)
|
||||
const langText = possibility.info
|
||||
newLangParams[langParam] = langText
|
||||
replaceTextWithLandParam(langText,langParam)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -53,6 +233,7 @@ const processSection = (section) => {
|
|||
})
|
||||
}
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
console.error(section)
|
||||
console.error(err)
|
||||
}
|
||||
|
@ -69,7 +250,19 @@ pageKeys.forEach((pageKey) => {
|
|||
processSection(section)
|
||||
})
|
||||
}else{
|
||||
console.log(page)
|
||||
// console.log(page)
|
||||
}
|
||||
})
|
||||
console.log(newLangParams)
|
||||
const newLanguageFile = Object.assign(languagesData,newLangParams)
|
||||
// console.log(definitonsRawData)
|
||||
console.log(newLanguageFile)
|
||||
setTimeout(() => {
|
||||
try{
|
||||
console.log('Writing New Definitions File, en_CA.js')
|
||||
fs.writeFileSync(newDefinitionFile,definitonsRawData)
|
||||
console.log('Writing New Language File, en_CA.json')
|
||||
fs.writeFileSync(newLanguagesFile,JSON.stringify(newLanguageFile,null,3))
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
},2000)
|
||||
|
|
|
@ -1,84 +1,120 @@
|
|||
console.log('This translation tool uses Yandex.')
|
||||
const fs = require('fs');
|
||||
console.log('This translation tool uses a Google Translate scraper. Use responsibly or your IP will be blocked by Google from using the service.')
|
||||
if(!process.argv[2]||!process.argv[3]||!process.argv[4]){
|
||||
console.log('You must input arguments.')
|
||||
console.log('# node translateLanguageFile.js <SOURCE> <FROM_LANGUAGE> <TO_LANGUAGE>')
|
||||
console.log('Example:')
|
||||
console.log('# node translateLanguageFile.js en_US en ar')
|
||||
console.log('# node translateLanguageFile.js en_CA en ar')
|
||||
return
|
||||
}
|
||||
var langDir='../languages/'
|
||||
var fs=require('fs');
|
||||
var https = require('https');
|
||||
var jsonfile=require('jsonfile');
|
||||
var source=require(langDir+process.argv[2]+'.json')
|
||||
var list = Object.keys(source)
|
||||
let translate;
|
||||
try{
|
||||
translate = require('@vitalets/google-translate-api')
|
||||
}catch(err){
|
||||
console.log(`You are missing a module to use this tool. Run "npm install @vitalets/google-translate-api" to install the required module.`)
|
||||
return
|
||||
}
|
||||
const langDir = `${__dirname}/../languages/`
|
||||
const sourceLangauge = process.argv[3]
|
||||
const inputFileLangauge = process.argv[2]
|
||||
const inputFileName = inputFileLangauge + '.json'
|
||||
const source = require(langDir + inputFileName)
|
||||
const list = Object.keys(source)
|
||||
console.log(list.length)
|
||||
var extra = ''
|
||||
var current = 1
|
||||
var currentItem = list[0]
|
||||
var chosenFile = langDir+process.argv[4]+'.json'
|
||||
const outputFileLangauge = process.argv[4]
|
||||
const outputFileName = outputFileLangauge + '.json'
|
||||
const chosenFile = langDir + outputFileName
|
||||
const generatedLanguageFilesPath = `${__dirname}/generatedLanguageFiles/`
|
||||
const generatedFilePath = `${generatedLanguageFilesPath}${outputFileName}`
|
||||
const throttleTime = parseInt(process.argv[5]) || 1000
|
||||
const usePendingFileForOutputSource = process.argv[6] === '1'
|
||||
let newList
|
||||
try{
|
||||
newList=require(chosenFile)
|
||||
const buildOutputSource = usePendingFileForOutputSource ? generatedFilePath : chosenFile
|
||||
console.log(`Source Path : ${buildOutputSource}`)
|
||||
eval(`newList = ${fs.readFileSync(buildOutputSource,'utf8')}`)
|
||||
console.log(`The word "Save" in this language : `,newList['Save'])
|
||||
}catch(err){
|
||||
console.log(chosenFile)
|
||||
var newList={}
|
||||
console.log(`There was an error loading : ${chosenFile}`)
|
||||
console.log(`Using blank base file. This will translate against all available terms!!!`)
|
||||
newList = {}
|
||||
}
|
||||
var newListAlphabetical={}
|
||||
var goNext=function(){
|
||||
++current
|
||||
currentItem = list[current]
|
||||
if(list.length===current){
|
||||
console.log('complete checking.. please wait')
|
||||
Object.keys(newList).sort().forEach(function(y,t){
|
||||
newListAlphabetical[y]=newList[y]
|
||||
})
|
||||
jsonfile.writeFile(chosenFile,newListAlphabetical,{spaces: 2},function(){
|
||||
console.log('complete writing')
|
||||
})
|
||||
}else{
|
||||
next(currentItem)
|
||||
async function writeLanguageFile(theList,alternatePath){
|
||||
const newListAlphabetical = {}
|
||||
const sourceTermKeysOrdered = Object.keys(theList).sort()
|
||||
sourceTermKeysOrdered.forEach(function(y){
|
||||
newListAlphabetical[y] = theList[y]
|
||||
})
|
||||
await fs.promises.writeFile(alternatePath ? alternatePath : generatedFilePath,JSON.stringify(newListAlphabetical,null,3))
|
||||
}
|
||||
function asyncSetTimeout(timeout){
|
||||
return new Promise((resolve,reject) => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
},timeout || 1000)
|
||||
})
|
||||
}
|
||||
async function makeFolderForOutput(timeout){
|
||||
try{
|
||||
await fs.promises.mkdir(generatedLanguageFilesPath)
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
}
|
||||
}
|
||||
var next=function(v){
|
||||
if(v===undefined){return false}
|
||||
//trnsl.1.1.20170718T033617Z.a9bbd3b739ca59df.7f89b7474ec69812afd0014b5e338328ebf3fc39
|
||||
if(newList[v]&&newList[v]!==source[v]){
|
||||
goNext()
|
||||
return
|
||||
async function moveNewLanguageFile(){
|
||||
try{
|
||||
await fs.promises.unlink(chosenFile)
|
||||
}catch(err){
|
||||
console.log('Failed to Delete old File!')
|
||||
console.log(err)
|
||||
}
|
||||
if(/<[a-z][\s\S]*>/i.test(source[v])===true){
|
||||
extra+='&format=html'
|
||||
try{
|
||||
await writeLanguageFile(newList,chosenFile)
|
||||
}catch(err){
|
||||
console.log('Failed to Move File!')
|
||||
console.log(err)
|
||||
}
|
||||
var url = 'https://translate.yandex.net/api/v1.5/tr.json/translate?key=trnsl.1.1.20160311T042953Z.341f2f63f38bdac6.c7e5c01fff7f57160141021ca61b60e36ff4d379'+extra+'&lang='+process.argv[3]+'-'+process.argv[4]+'&text='+source[v]
|
||||
https.request(url, function(data) {
|
||||
data.setEncoding('utf8');
|
||||
var chunks='';
|
||||
data.on('data', (chunk) => {
|
||||
chunks+=chunk;
|
||||
}
|
||||
function runTranslation(termKey,numberInLine){
|
||||
return new Promise((resolve,reject) => {
|
||||
if(termKey === undefined)return false;
|
||||
const existingTerm = newList[termKey]
|
||||
if(existingTerm && existingTerm !== source[termKey]){
|
||||
resolve(existingTerm)
|
||||
console.log('found a rule for this one, skipping : ',source[termKey]);
|
||||
return
|
||||
}
|
||||
console.log(`${numberInLine} of ${list.length}`)
|
||||
translate(source[termKey], {
|
||||
to: outputFileLangauge,
|
||||
from: sourceLangauge
|
||||
}).then(res => {
|
||||
translation = res.text;
|
||||
newList[termKey] = translation;
|
||||
console.log(termKey,' ---> ',translation)
|
||||
setTimeout(() => {
|
||||
resolve(translation)
|
||||
},throttleTime)
|
||||
}).catch(err => {
|
||||
translation = `${source[termKey]}`
|
||||
console.log('translation failed : ',translation);
|
||||
console.error(err);
|
||||
newList[termKey] = translation;
|
||||
resolve()
|
||||
});
|
||||
data.on('end', () => {
|
||||
try{
|
||||
chunks=JSON.parse(chunks)
|
||||
if(chunks.html){
|
||||
if(chunks.html[0]){
|
||||
var translation=chunks.html[0]
|
||||
}else{
|
||||
var translation=chunks.html
|
||||
}
|
||||
|
||||
}else{
|
||||
var translation=chunks.text[0]
|
||||
}
|
||||
}catch(err){
|
||||
var translation=source[v]
|
||||
}
|
||||
newList[v]=translation;
|
||||
console.log(current+'/'+list.length+','+v+' ---> '+translation)
|
||||
goNext()
|
||||
});
|
||||
}).on('error', function(e) {
|
||||
console.log('ERROR : 500 '+v)
|
||||
res.sendStatus(500);
|
||||
}).end();
|
||||
})
|
||||
}
|
||||
next(currentItem)
|
||||
async function runTranslatorOnSourceTerms(){
|
||||
await makeFolderForOutput()
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
let termKey = list[i]
|
||||
await runTranslation(termKey,i)
|
||||
await writeLanguageFile(newList)
|
||||
}
|
||||
await moveNewLanguageFile()
|
||||
console.log('Building Language File Complete!')
|
||||
}
|
||||
runTranslatorOnSourceTerms()
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,51 @@
|
|||
.dark .list-group-item{border-color: #444;background:#222}
|
||||
.dark .list-group-item.active{background:#c49a68;border-color:#a7865f}
|
||||
.novideos{text-transform: uppercase;text-align: center;border-bottom:0!important;padding-top: 55%!important;letter-spacing:2px}
|
||||
|
||||
.btn-warning {
|
||||
color: #fff;
|
||||
background-color: #c49a68;
|
||||
border-color: #c49a68;
|
||||
}
|
||||
.dark .table-striped>tbody>tr>td{border-color:#222;color:#fff}
|
||||
.dark code{color: #c49a68;background-color: #36333d;}
|
||||
.dark a:not(.btn){color: #c49a68;}
|
||||
|
||||
|
||||
|
||||
|
||||
.dark.form-control,
|
||||
.dark .form-control,
|
||||
.dark.form-select,
|
||||
.dark .form-select
|
||||
{
|
||||
background-color: #2b3c4b;
|
||||
border: 1px solid #2b3c4b;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark.form-control:focus,
|
||||
.dark .form-control:focus,
|
||||
.dark.form-select:focus,
|
||||
.dark .form-select:focus
|
||||
{
|
||||
color: #ddd;
|
||||
background-color: #2b3c4b;
|
||||
border: 1px solid #222;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.dark .slider-selection {
|
||||
background: #375182;
|
||||
}
|
||||
.dark .slider-handle {
|
||||
background: #375182;
|
||||
}
|
||||
.dark .slider-track {
|
||||
background: #333;
|
||||
}
|
||||
|
||||
.progress {
|
||||
height: 5px;
|
||||
background: #1e2b37;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
form.modal-body{margin:0}
|
||||
.dark .form-group-group{color:#fff;}
|
||||
.form-group-group label{margin: 0}
|
||||
.form-group-group blockquote:before,.form-group-group blockquote:after{display:none!important}
|
||||
.form-group-group blockquote{letter-spacing:normal;font-style:normal}
|
||||
.form-group-group blockquote p:empty{display:none}
|
||||
.form-group-group blockquote p{font-size:inherit}
|
||||
.form-group-group blockquote p:last-child{margin-bottom:0}
|
||||
.form-group-group-group>div,.form-group-group-group .h_us_advanced>div{margin-bottom:15px;}
|
||||
.form-group-group table{width:100%}
|
||||
.form-group-group table tr td{padding:10px 5px}
|
||||
.form-group-group table tr:not(:last-child) td{border-bottom:1px dotted #eee}
|
||||
/* .form-group-group .mdl-list__item{border-bottom:1px solid #eee;}
|
||||
.form-group-group .mdl-list__item:hover{background:#e6e6e6;border-radius:4px;}
|
||||
.dark .form-group-group .mdl-list__item{color:#fff;border-bottom:1px solid #444;}
|
||||
.dark .form-group-group .mdl-list__item:hover{background:#555;} */
|
||||
.form-group-group:visible:last-child,.form-group-group > .form-group:last-child{margin-bottom:0}
|
||||
.form-group-group:last-child {margin-bottom: 0}
|
||||
|
||||
.btn-group-justified {display: flex}
|
||||
.btn-group-justified .btn {flex: 1}
|
||||
|
||||
|
||||
/* .hide-box-wrapper.form-group-group > h4{
|
||||
margin: 0;
|
||||
}
|
||||
.hide-box-wrapper.form-group-group{
|
||||
padding: 0;
|
||||
} */
|
||||
.hide-box-wrapper > .box-wrapper{
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
}
|
|
@ -1,4 +1,45 @@
|
|||
#monitors_live {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.mdl-card__media {
|
||||
background-color: #333;
|
||||
background-repeat: repeat;
|
||||
background-position: 50% 50%;
|
||||
background-size: cover;
|
||||
background-origin: padding-box;
|
||||
background-attachment: scroll;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monitor_item .mdl-card__media {
|
||||
box-sizing: border-box;
|
||||
background-size: cover;
|
||||
padding: 24px;
|
||||
display: -webkit-flex;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-flex-grow: 1;
|
||||
-ms-flex-positive: 1;
|
||||
flex-grow: 1;
|
||||
-webkit-flex-direction: row;
|
||||
-ms-flex-direction: row;
|
||||
flex-direction: row;
|
||||
-webkit-align-items: flex-end;
|
||||
-ms-flex-align: end;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.monitor_item * {
|
||||
transition: 0.2s!important;
|
||||
}
|
||||
.monitor_item .mdl-card__supporting-text:not(:last-child) {
|
||||
box-sizing: border-box;
|
||||
padding: 10px 16px;
|
||||
min-height: auto;
|
||||
}
|
||||
.monitor_item .mdl-card__supporting-text{
|
||||
width: 100%;
|
||||
}
|
||||
.jpegMode .cpu_load .progress-bar,.jpegMode .ram_load .progress-bar{background-color:#5cb85c}
|
||||
.jpegMode [system="jpegToggle"],[system].text-success{color:#5cb85c!important}
|
||||
|
||||
|
@ -12,12 +53,64 @@
|
|||
.monitor_item .stream-hud:hover .bottom-text{top:0;}
|
||||
.monitor_item .stream-hud .bottom-text .detector-fade{background: rgba(0,0,0,0.4);padding:10px 20px;border-radius:10px}
|
||||
.monitor_item .stream-hud .lamp{position:absolute;top:5px;right:5px;z-index:1;text-shadow: 0 0 15px #333;}
|
||||
.monitor_item .mdl-overlay-menu{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
min-width: 200px;
|
||||
height: 50%;
|
||||
min-height: 200px;
|
||||
overflow: auto;
|
||||
z-index: 100;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: #fff;
|
||||
}
|
||||
.monitor_item .mdl-overlay-menu-backdrop{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.monitor_item .mdl-overlay-menu{
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
min-width: 200px;
|
||||
height: 50%;
|
||||
min-height: 150px;
|
||||
overflow: auto;
|
||||
z-index: 100;
|
||||
background: rgba(0,0,0,0.7);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
.monitor_item .mdl-overlay-menu li{
|
||||
padding: 1rem;
|
||||
}
|
||||
.monitor_item .mdl-overlay-menu li i{
|
||||
float: left;
|
||||
font-size: 1.5em;
|
||||
}
|
||||
.monitor_item .mdl-overlay-menu li:hover{
|
||||
background: #021B79;
|
||||
}
|
||||
.monitor_item[mode="Disabled"] .stream-hud .lamp{color:#5d5d5d}
|
||||
.monitor_item[mode="Watch Only"] .stream-hud .lamp{color:#5da8e8}
|
||||
.monitor_item[mode="Idle"] .stream-hud .lamp{color:#fff}
|
||||
.monitor_item[mode="Record"] .stream-hud .lamp{color:#d9534f}
|
||||
/*.data-menu{max-height:700px}*/
|
||||
.data-menu:not(:last-child){border-right:1px solid #fff;}
|
||||
.data-menu.logs{list-style:none;}
|
||||
.monitor_item .motionVision{display:none}
|
||||
|
||||
|
@ -33,7 +126,7 @@
|
|||
.monitor_item .stream-element{border: 0;object-fit: fill;height: 100%;width:100%}
|
||||
.monitor_item{position:relative;padding:0;transition:none;background:#000}
|
||||
.monitor_item .mdl-card{min-height:auto;border:1px solid #272727;border-radius:0px;overflow:hidden}
|
||||
.monitor_item .mdl-card__media{position:relative;padding:0!important;display:block!important;background:#000;}
|
||||
.monitor_item .mdl-card__media{height:100%;position:relative;padding:0!important;display:block!important;background:#000;}
|
||||
.monitor_item.selected .stream-element{height:600px}
|
||||
.monitor_item.selected .fa-expand:before{content:"\f066"}
|
||||
.monitor_item .mdl-card__supporting-text{background:#222;color:#fff!important;display:block;min-height:auto!important}
|
||||
|
@ -50,11 +143,7 @@
|
|||
.monitor_item .stream-hud .controls .btn{opacity:0.7}
|
||||
.monitor_item.doObjectDetection .progress-bar{background-color: #57d94f}
|
||||
|
||||
.data-menu{text-align:left}
|
||||
.data-menu ul,.side-menu ul{list-style:none;margin:0;padding:0;}
|
||||
.data-menu li,.side-menu li:not(.mdl-menu__item){
|
||||
border-bottom:1px solid #54502d;padding:10px;
|
||||
}
|
||||
.data-menu{text-align:left;height: 100%; overflow: auto;}
|
||||
.data-menu .progress-circle{margin:0 10px 0 0;position:relative;height:40px;width:40px;float:left}
|
||||
.data-menu .progress-circle span:after{content:''}
|
||||
img.circle-img,div.circle-img{border-radius:50%;height:50px;width:50px}
|
||||
|
@ -91,7 +180,6 @@ img.circle-img,div.circle-img{border-radius:50%;height:50px;width:50px}
|
|||
#monSectionStreamChannels:empty,#monSectionInputMaps:empty{display:none}
|
||||
|
||||
|
||||
#monitors_list .monitor_block{transition:none}
|
||||
.dropdown-menu.scrollable{max-height:300px}
|
||||
.upload_file input{display:none}
|
||||
#video_preview .stream-objects{right:0;margin:auto;display:inline-block;position:relative;width:auto}
|
||||
|
@ -121,3 +209,6 @@ img.circle-img,div.circle-img{border-radius:50%;height:50px;width:50px}
|
|||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
.dont-stretch-monitors .monitor_item .stream-element {
|
||||
object-fit: initial!important;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
.tab-livePlayer video{
|
||||
width: 100%;
|
||||
background: #333;
|
||||
}
|
||||
.tab-livePlayer-event-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
.livePlayer-event-objects .tag {
|
||||
position: absolute;
|
||||
bottom: 100%;
|
||||
left: 0;
|
||||
background: red;
|
||||
color: #fff;
|
||||
font-family: monospace;
|
||||
font-size: 80%;
|
||||
border-radius: 5px 5px 0 0;
|
||||
padding: 3px 5px;
|
||||
}
|
||||
|
||||
.tab-livePlayer-event-objects .stream-detected-object {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 3px solid red;
|
||||
background: transparent;
|
||||
border-radius: 5px
|
||||
}
|
||||
|
||||
.tab-livePlayer:hover .tab-livePlayer-event-objects {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-livePlayer .stream-element {
|
||||
width: 100%;
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue