Merge branch 'dashboard-v3' into 'dev'

Dashboard v3 : Tempered Steel

See merge request Shinobi-Systems/Shinobi!319
cron-as-worker-process
Moe 2022-06-05 23:03:29 +00:00
commit 77792b5da2
315 changed files with 106305 additions and 23897 deletions

1
.gitignore vendored
View File

@ -11,3 +11,4 @@ npm-debug.log
shinobi.sqlite
dist
._*
generatedLanguageFiles

View File

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

36
INSTALL/cuda-11.sh Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

1652
languages/it.json Normal file

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

1649
languages/tr.json Normal file

File diff suppressed because it is too large Load Diff

1647
languages/vi.json Normal file

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

358
libs/childNode/utils.js Normal file
View File

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

View File

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

View File

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

209
libs/commander/workerv2.js Normal file
View File

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

View File

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

View File

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

View File

@ -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,
})
}
/**

View File

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

View File

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

62
libs/dataPort.js Normal file
View File

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

View File

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

View File

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

View File

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

230
libs/dropInEvents/mqtt.js Normal file
View File

@ -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> &nbsp; ${lang['Add']}`,
},
{
"id": "mqttclient_list",
"fieldType": "div",
},
{
"fieldType": "script",
"src": "assets/js/bs5.mqtt.js",
}
]
}
}
}

View File

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

View File

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

View File

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

View File

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

16
libs/ffmpeg/subStream.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.');
}
};

397
libs/notifications/mqtt.js Normal file
View File

@ -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> &nbsp; ${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.')
}
}
}

View File

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

View File

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

View File

@ -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",
}
]
})
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

45
libs/user/startup.js Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

3658
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -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/**/*",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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": {},

View File

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

View File

@ -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": "UTC12:00, Y",
"value": -720
},
{
"text": "UTC11:00, X",
"value": -660
},
{
"text": "UTC10:00, W",
"value": -600
},
{
"text": "UTC09:30, V†",
"value": -570
},
{
"text": "UTC09:00, V",
"value": -540
},
{
"text": "UTC08:00, U",
"value": -480
},
{
"text": "UTC07:00, T",
"value": -420
},
{
"text": "UTC06:00, S",
"value": -360
},
{
"text": "UTC05:00, R",
"value": -300
},
{
"text": "UTC04:00, Q",
"value": -240
},
{
"text": "UTC03:30, P†",
"value": -210
},
{
"text": "UTC03:00, P",
"value": -180
},
{
"text": "UTC02:00, O",
"value": -120
},
{
"text": "UTC01: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)

View File

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

View File

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

View File

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

View File

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

View File

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