Merge branch 'dev' into 'master'
Zom Be Gone See merge request Shinobi-Systems/Shinobi!318cron-addstorage-fix
commit
04a7baed2c
|
|
@ -87,22 +87,8 @@ docker run -d --name='Shinobi' -p '8080:8080/tcp' -v "/dev/shm/Shinobi/streams":
|
|||
|
||||
### Environment Variables
|
||||
|
||||
| Environment Variable | Description | Default |
|
||||
|----------------------|----------------------------------------------------------------------|--------------------|
|
||||
| SUBSCRIPTION_ID | **THIS IS NOT REQUIRED**. If you are a subscriber to any of the Shinobi services you may use that key as the value for this parameter. If you have donated by PayPal you may use your Transaction ID to activate the license as well. | *None* |
|
||||
| DB_USER | Username that the Shinobi process will connect to the database with. | majesticflame |
|
||||
| DB_PASSWORD | Password that the Shinobi process will connect to the database with. | *None* |
|
||||
| DB_HOST | Address that the Shinobi process will connect to the database with. | localhost |
|
||||
| DB_DATABASE | Database that the Shinobi process will interact with. | ccio |
|
||||
| DB_DISABLE_INCLUDED | Disable included database to use your own. Set to `true` to disable.| false |
|
||||
| PLUGIN_KEYS | The object containing connection keys for plugins running in client mode (non-host, default). | {} |
|
||||
| SSL_ENABLED | Enable or Disable SSL. | false |
|
||||
| SSL_COUNTRY | Country Code for SSL. | CA |
|
||||
| SSL_STATE | Province/State Code for SSL. | BC |
|
||||
| SSL_LOCATION | Location of where SSL key is being used. | Vancouver |
|
||||
| SSL_ORGANIZATION | Company Name associated to key. | Shinobi Systems |
|
||||
| SSL_ORGANIZATION_UNIT | Department associated to key. | IT Department |
|
||||
| SSL_COMMON_NAME | Common Name associated to key. | nvr.ninja |
|
||||
> Environment Variables have been disabled. You must now make changes in the conf.json itself when it is mounted on the host.
|
||||
> If conf.json does not exist inside the mounted folder then you may create it and Shinobi will use that on next container start.
|
||||
|
||||
> You must add (to the docker container) `/config/ssl/server.key` and `/config/ssl/server.cert`. The `/config` folder is mapped to `$HOME/Shinobi/config` on the host by default with the quick run methods. Place `key` and `cert` in `$HOME/Shinobi/config/ssl`. If `SSL_ENABLED=true` and these files don't exist they will be generated with `openssl`.
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ elif [ ! -e "./conf.json" ]; then
|
|||
cp conf.sample.json conf.json
|
||||
fi
|
||||
sudo sed -i -e 's/change_this_to_something_very_random__just_anything_other_than_this/'"$cronKey"'/g' conf.json
|
||||
node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG"
|
||||
# node tools/modifyConfiguration.js cpuUsageMarker=CPU subscriptionId=$SUBSCRIPTION_ID thisIsDocker=true pluginKeys="$PLUGIN_KEYS" db="$DATABASE_CONFIG" ssl="$SSL_CONFIG"
|
||||
|
||||
|
||||
echo "============="
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ VOLUME ["/config"]
|
|||
VOLUME ["/customAutoLoad"]
|
||||
VOLUME ["/var/lib/mysql"]
|
||||
|
||||
EXPOSE 8080
|
||||
EXPOSE 8080 443 21 25
|
||||
|
||||
ENTRYPOINT ["sh","/home/Shinobi/Docker/init.sh"]
|
||||
|
||||
|
|
|
|||
|
|
@ -2261,7 +2261,7 @@ module.exports = function(s,config,lang){
|
|||
hidden: true,
|
||||
"name": "detail=detector_trigger",
|
||||
"field": lang['Trigger Record'],
|
||||
"description": "This will order the camera to record if it is set to \"Watch-Only\" when a motion even is detected.",
|
||||
"description": "This will order the camera to record if it is set to \"Watch-Only\" when an Event is detected.",
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"form-group-class": "h_det_input h_det_1",
|
||||
|
|
@ -4012,6 +4012,23 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "detail=notify_useRawSnapshot",
|
||||
"field": lang['Use Raw Snapshot'],
|
||||
"default": "0",
|
||||
"example": "1",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
"Logging": {
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"deleteSubAccountText": "Do you want to delete this Sub-Account? You cannot recover it.",
|
||||
"Turn Speed": "Turn Speed",
|
||||
"Session Key": "Session Key",
|
||||
"Use Raw Snapshot": "Use Raw Snapshot",
|
||||
"Login": "Login",
|
||||
"API Key Action Failed": "API Key Action Failed",
|
||||
"Authenticate": "Authenticate",
|
||||
|
|
|
|||
|
|
@ -179,6 +179,8 @@ module.exports = async (s,config,lang,app,io) => {
|
|||
const moduleName = shinobiModule.name
|
||||
s.customAutoLoadModules[moduleName] = {}
|
||||
var customModulePath = modulesBasePath + '/' + moduleName
|
||||
s.debugLog(customModulePath)
|
||||
s.debugLog(JSON.stringify(shinobiModule,null,3))
|
||||
if(shinobiModule.isIgnitor){
|
||||
s.customAutoLoadModules[moduleName].type = 'file'
|
||||
try{
|
||||
|
|
@ -329,6 +331,7 @@ module.exports = async (s,config,lang,app,io) => {
|
|||
adminLibsCss: [],
|
||||
superPageBlocks: [],
|
||||
superLibsJs: [],
|
||||
superRawJs: [],
|
||||
superLibsCss: []
|
||||
}
|
||||
fs.readdir(modulesBasePath,function(err,folderContents){
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ module.exports = (s,config,lang,app,io) => {
|
|||
const {
|
||||
moveCameraPtzToMatrix
|
||||
} = require('../control/ptz.js')(s,config,lang)
|
||||
const {
|
||||
cutVideoLength
|
||||
} = require('../video/utils.js')(s,config,lang)
|
||||
async function saveImageFromEvent(options,frameBuffer){
|
||||
const monitorId = options.mid || options.id
|
||||
const groupKey = options.ke
|
||||
|
|
@ -425,13 +428,24 @@ 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 recordingDirectory = s.getVideoDirectory(monitorConfig)
|
||||
const fileTime = eventBasedRecording.lastFileTime
|
||||
const filename = `${fileTime}.mp4`
|
||||
response.filename = `${filename}`
|
||||
response.filePath = `${recordingDirectory}${filename}`
|
||||
eventBasedRecording.process.on('close',function(){
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
if(!isNaN(videoLength)){
|
||||
const cutResponse = await cutVideoLength({
|
||||
ke: groupKey,
|
||||
mid: monitorId,
|
||||
filePath: response.filePath,
|
||||
cutLength: parseInt(videoLength),
|
||||
})
|
||||
response.filename = cutResponse.filename
|
||||
response.filePath = cutResponse.filePath
|
||||
}
|
||||
resolve(response)
|
||||
},1000)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ module.exports = function(s,config,lang){
|
|||
splitForFFPMEG,
|
||||
} = require('./ffmpeg/utils.js')(s,config,lang)
|
||||
const {
|
||||
processKill,
|
||||
cameraDestroy,
|
||||
monitorConfigurationMigrator,
|
||||
} = require('./monitor/utils.js')(s,config,lang)
|
||||
|
|
@ -202,14 +203,14 @@ module.exports = function(s,config,lang){
|
|||
})
|
||||
snapProcess.on('error', (data) => {
|
||||
console.log(data)
|
||||
snapProcess.terminate()
|
||||
processKill(snapProcess)
|
||||
})
|
||||
snapProcess.on('exit', (code) => {
|
||||
clearTimeout(snapProcessTimeout)
|
||||
sendTempImage()
|
||||
})
|
||||
var snapProcessTimeout = setTimeout(function(){
|
||||
snapProcess.terminate()
|
||||
processKill(snapProcess)
|
||||
},dynamicTimeout)
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,40 @@ module.exports = (s,config,lang) => {
|
|||
splitForFFPMEG,
|
||||
} = require('../ffmpeg/utils.js')(s,config,lang)
|
||||
const getUpdateableFields = require('./updatedFields.js')
|
||||
const processKill = (proc) => {
|
||||
const response = {ok: true}
|
||||
return new Promise((resolve,reject) => {
|
||||
function sendError(err){
|
||||
response.ok = false
|
||||
response.err = err
|
||||
resolve(response)
|
||||
}
|
||||
try{
|
||||
proc.stdin.write("q\r\n")
|
||||
setTimeout(() => {
|
||||
if(proc && proc.kill){
|
||||
if(s.isWin){
|
||||
spawn("taskkill", ["/pid", proc.pid, '/t'])
|
||||
}else{
|
||||
proc.kill('SIGTERM')
|
||||
}
|
||||
setTimeout(function(){
|
||||
try{
|
||||
proc.kill()
|
||||
resolve(response)
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
sendError(err)
|
||||
}
|
||||
},1000)
|
||||
}
|
||||
},1000)
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
sendError(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
const cameraDestroy = function(e,p){
|
||||
if(
|
||||
s.group[e.ke] &&
|
||||
|
|
@ -72,27 +106,9 @@ module.exports = (s,config,lang) => {
|
|||
if(activeMonitor.childNode){
|
||||
s.cx({f:'kill',d:s.cleanMonitorObject(e)},activeMonitor.childNodeId)
|
||||
}else{
|
||||
try{
|
||||
proc.stdin.write("q\r\n")
|
||||
setTimeout(() => {
|
||||
if(proc && proc.kill){
|
||||
if(s.isWin){
|
||||
spawn("taskkill", ["/pid", proc.pid, '/t'])
|
||||
}else{
|
||||
proc.kill('SIGTERM')
|
||||
}
|
||||
setTimeout(function(){
|
||||
try{
|
||||
proc.kill()
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
}
|
||||
},1000)
|
||||
}
|
||||
},1000)
|
||||
}catch(err){
|
||||
s.debugLog(err)
|
||||
}
|
||||
processKill(proc).then((response) => {
|
||||
s.debugLog(`cameraDestroy`,response)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -130,21 +146,11 @@ module.exports = (s,config,lang) => {
|
|||
completeRequest()
|
||||
})
|
||||
var snapProcessTimeout = setTimeout(function(){
|
||||
var pid = snapProcess.pid
|
||||
if(s.isWin){
|
||||
spawn("taskkill", ["/pid", pid, '/t'])
|
||||
}else{
|
||||
process.kill(-pid, 'SIGTERM')
|
||||
}
|
||||
setTimeout(function(){
|
||||
if(s.isWin === false){
|
||||
treekill(pid)
|
||||
}else{
|
||||
snapProcess.kill()
|
||||
}
|
||||
processKill(snapProcess).then((response) => {
|
||||
s.debugLog(`createSnapshot-snapProcessTimeout`,response)
|
||||
completeRequest()
|
||||
},10000)
|
||||
},30000)
|
||||
})
|
||||
},5000)
|
||||
})
|
||||
}
|
||||
const addCredentialsToStreamLink = (options) => {
|
||||
|
|
@ -192,6 +198,7 @@ module.exports = (s,config,lang) => {
|
|||
return {
|
||||
cameraDestroy: cameraDestroy,
|
||||
createSnapshot: createSnapshot,
|
||||
processKill: processKill,
|
||||
addCredentialsToStreamLink: addCredentialsToStreamLink,
|
||||
monitorConfigurationMigrator: monitorConfigurationMigrator,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
var fs = require("fs")
|
||||
module.exports = function(s,config,lang){
|
||||
require('./notifications/email.js')(s,config,lang)
|
||||
require('./notifications/discordBot.js')(s,config,lang)
|
||||
require('./notifications/telegram.js')(s,config,lang)
|
||||
async function getSnapshot(d,monitorConfig){
|
||||
d.screenshotBuffer = d.screenshotBuffer || d.frame
|
||||
if(!d.screenshotBuffer || (monitorConfig.details.notify_useRawSnapshot === '1' && !d.usingRawSnapshotBuffer)){
|
||||
d.usingRawSnapshotBuffer = true
|
||||
const { screenShot, isStaticFile } = await s.getRawSnapshotFromMonitor(monitorConfig,{
|
||||
secondsInward: monitorConfig.details.snap_seconds_inward
|
||||
})
|
||||
d.screenshotBuffer = screenShot
|
||||
}
|
||||
}
|
||||
require('./notifications/email.js')(s,config,lang,getSnapshot)
|
||||
require('./notifications/discordBot.js')(s,config,lang,getSnapshot)
|
||||
require('./notifications/telegram.js')(s,config,lang,getSnapshot)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
var fs = require("fs")
|
||||
var Discord = require("discord.js")
|
||||
module.exports = function(s,config,lang){
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
const {
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
|
|
@ -81,7 +81,6 @@ module.exports = function(s,config,lang){
|
|||
videoPath = siftedVideoFileFromRam.filePath
|
||||
videoName = siftedVideoFileFromRam.filename
|
||||
}
|
||||
console.log(videoPath,videoName)
|
||||
if(videoPath){
|
||||
sendMessage({
|
||||
author: {
|
||||
|
|
@ -103,13 +102,7 @@ module.exports = function(s,config,lang){
|
|||
],d.ke)
|
||||
}
|
||||
}
|
||||
d.screenshotBuffer = d.screenshotBuffer || d.frame
|
||||
if(!d.screenshotBuffer){
|
||||
const { screenShot, isStaticFile } = await s.getRawSnapshotFromMonitor(monitorConfig,{
|
||||
secondsInward: monitorConfig.details.snap_seconds_inward
|
||||
})
|
||||
d.screenshotBuffer = screenShot
|
||||
}
|
||||
await getSnapshot(d,monitorConfig)
|
||||
if(d.screenshotBuffer){
|
||||
sendMessage({
|
||||
author: {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const {
|
|||
template,
|
||||
checkEmail,
|
||||
} = require("./emailUtils.js")
|
||||
module.exports = function(s,config,lang){
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
const {
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
|
|
@ -193,13 +193,7 @@ module.exports = function(s,config,lang){
|
|||
})
|
||||
}
|
||||
}
|
||||
d.screenshotBuffer = d.screenshotBuffer || d.frame
|
||||
if(!d.screenshotBuffer){
|
||||
const {screenShot, isStaticFile} = await s.getRawSnapshotFromMonitor(monitorConfig,{
|
||||
secondsInward: monitorConfig.details.snap_seconds_inward
|
||||
})
|
||||
if(screenShot)d.screenshotBuffer = screenShot
|
||||
}
|
||||
await getSnapshot(d,monitorConfig)
|
||||
sendMail([
|
||||
{
|
||||
filename: d.screenshotName + '.jpg',
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
var fs = require("fs")
|
||||
module.exports = function(s,config,lang){
|
||||
module.exports = function(s,config,lang,getSnapshot){
|
||||
const {
|
||||
getEventBasedRecordingUponCompletion,
|
||||
} = require('../events/utils.js')(s,config,lang)
|
||||
|
|
@ -87,13 +87,7 @@ module.exports = function(s,config,lang){
|
|||
],d.ke)
|
||||
}
|
||||
}
|
||||
d.screenshotBuffer = d.screenshotBuffer || d.frame
|
||||
if(!d.screenshotBuffer){
|
||||
const { screenShot, isStaticFile } = await s.getRawSnapshotFromMonitor(monitorConfig,{
|
||||
secondsInward: monitorConfig.details.snap_seconds_inward
|
||||
})
|
||||
d.screenshotBuffer = screenShot
|
||||
}
|
||||
await getSnapshot(d,monitorConfig)
|
||||
if(d.screenshotBuffer){
|
||||
sendMessage({
|
||||
title: lang.Event+' - '+d.screenshotName,
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ module.exports = (config) => {
|
|||
var currentlyUpdating = false
|
||||
return {
|
||||
getSystemInfo: (s) => {
|
||||
return {
|
||||
const response = {
|
||||
"Time Started": s.timeStarted,
|
||||
"Time Ready": s.timeReady,
|
||||
Versions: {
|
||||
"Shinobi": s.currentVersion,
|
||||
"Node.js": process.version,
|
||||
"FFmpeg": s.ffmpegVersion,
|
||||
"isActivated": config.userHasSubscribed,
|
||||
"isActivated": config.userHasSubscribed
|
||||
},
|
||||
Machine: {
|
||||
"CPU Core Count": s.coreCount,
|
||||
|
|
@ -19,6 +19,8 @@ module.exports = (config) => {
|
|||
"Operating System Platform": s.platform,
|
||||
},
|
||||
}
|
||||
if(s.expiryDate)response.Versions["License Expires On"] = s.expiryDate
|
||||
return response
|
||||
},
|
||||
getConfiguration: () => {
|
||||
return new Promise((resolve,reject) => {
|
||||
|
|
@ -32,11 +34,7 @@ module.exports = (config) => {
|
|||
try{
|
||||
if(config.thisIsDocker){
|
||||
const dockerConfigFile = '/config/conf.json'
|
||||
fs.stat(dockerConfigFile,(err) => {
|
||||
if(!err){
|
||||
fs.writeFile(dockerConfigFile,JSON.stringify(postBody,null,3),function(){})
|
||||
}
|
||||
})
|
||||
fs.writeFileSync(dockerConfigFile,JSON.stringify(postBody,null,3))
|
||||
}
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
|
|
|
|||
|
|
@ -176,8 +176,42 @@ module.exports = (s,config,lang) => {
|
|||
finish()
|
||||
}
|
||||
}
|
||||
function cutVideoLength(options){
|
||||
return new Promise((resolve,reject) => {
|
||||
const response = {ok: false}
|
||||
const inputFilePath = options.filePath
|
||||
const monitorId = options.mid
|
||||
const groupKey = options.ke
|
||||
const cutLength = options.cutLength || 10
|
||||
const tempDirectory = s.getStreamsDirectory(options)
|
||||
let fileExt = inputFilePath.split('.')
|
||||
fileExt = fileExt[fileExt.length -1]
|
||||
const filename = `${s.gid(10)}.${fileExt}`
|
||||
const videoOutPath = `${tempDirectory}`
|
||||
const cuttingProcess = spawn(config.ffmpegDir,['-loglevel','warning','-i', inputFilePath, '-c','copy','-t',`${cutLength}`,videoOutPath])
|
||||
cuttingProcess.stderr.on('data',(data) => {
|
||||
const err = data.toString()
|
||||
s.debugLog('cutVideoLength',options,err)
|
||||
})
|
||||
cuttingProcess.on('close',(data) => {
|
||||
fs.stat(videoOutPath,(err) => {
|
||||
if(!err){
|
||||
response.filename = filename
|
||||
response.filePath = videoOutPath
|
||||
setTimeout(() => {
|
||||
s.file('delete',videoOutPath)
|
||||
},1000 * 60 * 3)
|
||||
}else{
|
||||
s.debugLog('cutVideoLength:readFile',options,err)
|
||||
}
|
||||
resolve(response)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
return {
|
||||
orphanedVideoCheck: orphanedVideoCheck,
|
||||
scanForOrphanedVideos: scanForOrphanedVideos,
|
||||
cutVideoLength: cutVideoLength,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1482,7 +1482,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
let video = req.files.video;
|
||||
var time = new Date(parseInt(video.name.split('.')[0]))
|
||||
var filename = s.formattedTime(time) + '.' + monitor.ext
|
||||
video.mv(s.getVideoDirectory(monitor) + filename,function(){
|
||||
video.mv(s.getVideoDirectory(monitor) + filename,function(){
|
||||
s.insertCompletedVideo(monitor,{
|
||||
file: filename,
|
||||
events: s.group[groupKey].activeMonitors[monitorId].detector_motion_count,
|
||||
|
|
|
|||
|
|
@ -254,11 +254,7 @@ module.exports = function(s,config,lang,app){
|
|||
try{
|
||||
if(config.thisIsDocker){
|
||||
const dockerSuperFile = '/config/super.json'
|
||||
fs.stat(dockerSuperFile,(err) => {
|
||||
if(!err){
|
||||
fs.writeFile(dockerSuperFile,JSON.stringify(currentSuperUserList,null,3),function(){})
|
||||
}
|
||||
})
|
||||
fs.writeFileSync(dockerSuperFile,JSON.stringify(currentSuperUserList,null,3))
|
||||
}
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
conf.json
|
||||
dist
|
||||
models
|
||||
node_modules
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
DIR=$(dirname $0)
|
||||
echo "Removing existing Node.js modules..."
|
||||
rm -rf $DIR/node_modules
|
||||
|
||||
nonInteractiveFlag=false
|
||||
|
||||
if [ ! -e "$DIR/conf.json" ]; then
|
||||
dontCreateKeyFlag=false
|
||||
echo "Creating conf.json"
|
||||
sudo cp $DIR/conf.sample.json $DIR/conf.json
|
||||
else
|
||||
echo "conf.json already exists..."
|
||||
fi
|
||||
|
||||
if [ "$dontCreateKeyFlag" = false ]; then
|
||||
echo "Adding Random Plugin Key to Main Configuration"
|
||||
node $DIR/../../tools/modifyConfigurationForPlugin.js platerecognizer key=$(head -c 64 < /dev/urandom | sha256sum | awk '{print substr($1,1,60)}')
|
||||
fi
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
# PlateRecognizer
|
||||
|
||||
> PlateRecognizer is a cloud-based service. You must sign up at http://platerecognizer.com to use this plugin. **In your purchase notes to http://platerecognizer.com be sure to indicate it will be used with Shinobi.**
|
||||
|
||||
1. Go to the Shinobi directory. **/home/Shinobi** is the default directory.
|
||||
|
||||
```
|
||||
cd /home/Shinobi/plugins/platerecognizer
|
||||
```
|
||||
|
||||
2. Install
|
||||
|
||||
```
|
||||
sh INSTALL.sh
|
||||
```
|
||||
|
||||
3. Start the plugin.
|
||||
|
||||
```
|
||||
pm2 start shinobi-platerecognizer.js
|
||||
```
|
||||
|
||||
4. Save to startup list. **OPTIONAL**
|
||||
|
||||
```
|
||||
pm2 save
|
||||
```
|
||||
|
||||
Doing this will reveal options in the monitor configuration. Shinobi does not need to be restarted when a plugin is initiated or stopped.
|
||||
|
||||
## Run the plugin as a Host
|
||||
> The main app (Shinobi) will be the client and the plugin will be the host. The purpose of allowing this method is so that you can use one plugin for multiple Shinobi instances. Allowing you to easily manage connections without starting multiple processes.
|
||||
|
||||
Edit your plugins configuration file. Set the `hostPort` **to be different** than the `listening port for camera.js`.
|
||||
|
||||
```
|
||||
nano conf.json
|
||||
```
|
||||
|
||||
Here is a sample of a Host configuration for the plugin.
|
||||
- `platerecognizerApiKey` is your API Key given by http://platerecognizer.com.
|
||||
- `plug` is the name of the plugin corresponding in the main configuration file.
|
||||
- `https` choose if you want to use SSL or not. Default is `false`.
|
||||
- `hostPort` can be any available port number. **Don't make this the same port number as Shinobi.** Default is `8082`.
|
||||
- `type` tells the main application (Shinobi) what kind of plugin it is. In this case it is a detector.
|
||||
|
||||
```
|
||||
{
|
||||
"plug":"PlateRecognizer",
|
||||
"platerecognizerApiKey": "11111111111111111",
|
||||
"hostPort":8082,
|
||||
"key":"1234567890",
|
||||
"mode":"host",
|
||||
"type":"detector"
|
||||
}
|
||||
```
|
||||
|
||||
Now modify the **main configuration file** located in the main directory of Shinobi.
|
||||
|
||||
```
|
||||
nano conf.json
|
||||
```
|
||||
|
||||
Add the `plugins` array if you don't already have it. Add the following *object inside the array*.
|
||||
|
||||
```
|
||||
"plugins":[
|
||||
{
|
||||
"id" : "PlateRecognizer",
|
||||
"https" : false,
|
||||
"host" : "localhost",
|
||||
"port" : 8082,
|
||||
"key" : "1234567890",
|
||||
"mode" : "host",
|
||||
"type" : "detector"
|
||||
}
|
||||
],
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"plug": "PlateRecognizer",
|
||||
"host": "localhost",
|
||||
"platerecognizerApiKey": "11111111111111111",
|
||||
"port": 8080,
|
||||
"hostPort": 58084,
|
||||
"key": "1234567890",
|
||||
"mode": "client",
|
||||
"type": "detector"
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"name": "shinobi-platerecognizer",
|
||||
"author": "Moe Alam",
|
||||
"version": "1.0.0",
|
||||
"description": "Object Detection plugin for DeepStack",
|
||||
"main": "shinobi-platerecognizer.js",
|
||||
"dependencies": {
|
||||
"request": "^2.88.0",
|
||||
"express": "^4.16.2",
|
||||
"moment": "^2.19.2",
|
||||
"socket.io": "^2.3.0",
|
||||
"socket.io-client": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {},
|
||||
"bin": "shinobi-platerecognizer.js",
|
||||
"pkg": {
|
||||
"targets": [
|
||||
"node12"
|
||||
],
|
||||
"scripts": [
|
||||
"../pluginBase.js"
|
||||
],
|
||||
"assets": []
|
||||
},
|
||||
"disabled": true
|
||||
}
|
||||
|
|
@ -0,0 +1,192 @@
|
|||
//
|
||||
// Shinobi - Tensorflow Plugin
|
||||
// Copyright (C) 2016-2025 Elad Bar, Moe Alam
|
||||
//
|
||||
// Base Init >>
|
||||
const fs = require('fs');
|
||||
const config = require('./conf.json')
|
||||
const fetch = require('node-fetch');
|
||||
const FormData = require('form-data');
|
||||
var s
|
||||
const {
|
||||
workerData
|
||||
} = require('worker_threads');
|
||||
|
||||
if(workerData && workerData.ok === true){
|
||||
try{
|
||||
s = require('../pluginWorkerBase.js')(__dirname,config)
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
try{
|
||||
s = require('./pluginWorkerBase.js')(__dirname,config)
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
return console.log(config.plug,'WORKER : Plugin start has failed. pluginBase.js was not found.')
|
||||
}
|
||||
}
|
||||
}else{
|
||||
try{
|
||||
s = require('../pluginBase.js')(__dirname,config)
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
try{
|
||||
s = require('./pluginBase.js')(__dirname,config)
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
return console.log(config.plug,'Plugin start has failed. pluginBase.js was not found.')
|
||||
}
|
||||
}
|
||||
try{
|
||||
s = require('../pluginBase.js')(__dirname,config)
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
try{
|
||||
const {
|
||||
haltMessage,
|
||||
checkStartTime,
|
||||
setStartTime,
|
||||
} = require('../pluginCheck.js')
|
||||
|
||||
if(!checkStartTime()){
|
||||
console.log(haltMessage,new Date())
|
||||
s.disconnectWebSocket()
|
||||
return
|
||||
}
|
||||
setStartTime()
|
||||
}catch(err){
|
||||
console.log(`pluginCheck failed`)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Base Init />>
|
||||
|
||||
const licensePlateRegion = config.licensePlateRegion || 'us'
|
||||
const platerecognizerApiKey = config.platerecognizerApiKey || '111111111111111111'
|
||||
if(!config.platerecognizerApiKey){
|
||||
console.log('No Plate Recognizer API Key set.')
|
||||
console.log('set conf.json value for `platerecognizerApiKey`')
|
||||
return process.exit()
|
||||
}
|
||||
const baseUrl = config.platerecognizerEndpoint || "https://api.platerecognizer.com/v1/plate-reader/"
|
||||
|
||||
function platerecognizerRequest(d,frameBuffer){
|
||||
return new Promise((resolve,reject) => {
|
||||
try{
|
||||
let body = new FormData();
|
||||
frameBufferToPath(d,frameBuffer).then((filePath) => {
|
||||
body.append('upload', fs.createReadStream(filePath));
|
||||
// Or body.append('upload', base64Image);
|
||||
body.append('regions', licensePlateRegion); // Change to your country
|
||||
fetch(baseUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Authorization": `Token ${platerecognizerApiKey}`
|
||||
},
|
||||
body: body
|
||||
}).then(res => res.json())
|
||||
.then((json) => {
|
||||
let predictions = []
|
||||
try{
|
||||
const response = json || {results: []}
|
||||
predictions = response["results"] || []
|
||||
}catch(err){
|
||||
console.log(json)
|
||||
console.log(err)
|
||||
console.log(body)
|
||||
}
|
||||
resolve(predictions);
|
||||
fs.unlink(filePath,function(){
|
||||
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
})
|
||||
}catch(err){
|
||||
resolve([])
|
||||
console.log(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
function addVehicleMatrix(v,mats){
|
||||
const label = v.vehicle["type"]
|
||||
const confidence = v.vehicle["score"]
|
||||
const y_min = v.vehicle["ymin"]
|
||||
const x_min = v.vehicle["xmin"]
|
||||
const y_max = v.vehicle["ymax"]
|
||||
const x_max = v.vehicle["xmax"]
|
||||
const vehicleWidth = x_max - x_min
|
||||
const vehicleHeight = y_max - y_min
|
||||
mats.push({
|
||||
x: x_min,
|
||||
y: y_min,
|
||||
width: vehicleWidth,
|
||||
height: vehicleHeight,
|
||||
tag: label,
|
||||
confidence: confidence,
|
||||
})
|
||||
}
|
||||
function frameBufferToPath(d,buffer){
|
||||
return new Promise((resolve,reject) => {
|
||||
const tmpFile = s.gid(5)+'.jpg'
|
||||
if(!fs.existsSync(s.dir.streams)){
|
||||
fs.mkdirSync(s.dir.streams);
|
||||
}
|
||||
frameDirectory = s.dir.streams+d.ke+'/'+d.id+'/'
|
||||
fs.writeFile(frameDirectory+tmpFile,buffer,function(err){
|
||||
if(err) return s.systemLog(err);
|
||||
try{
|
||||
resolve(frameDirectory+tmpFile)
|
||||
}catch(error){
|
||||
console.error('Catch: ' + error);
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
s.detectObject = async function(frameBuffer,d,tx,frameLocation,callback){
|
||||
const timeStart = new Date()
|
||||
const predictions = await platerecognizerRequest(d,frameBuffer)
|
||||
if(predictions.length > 0) {
|
||||
const mats = []
|
||||
predictions.forEach(function(v){
|
||||
const label = v["plate"]
|
||||
const confidence = v["score"]
|
||||
const y_min = v.box["ymin"]
|
||||
const x_min = v.box["xmin"]
|
||||
const y_max = v.box["ymax"]
|
||||
const x_max = v.box["xmax"]
|
||||
const width = x_max - x_min
|
||||
const height = y_max - y_min
|
||||
mats.push({
|
||||
x: x_min,
|
||||
y: y_min,
|
||||
width: width,
|
||||
height: height,
|
||||
tag: label,
|
||||
confidence: confidence,
|
||||
})
|
||||
addVehicleMatrix(v,mats)
|
||||
})
|
||||
const isObjectDetectionSeparate = d.mon.detector_pam === '1' && d.mon.detector_use_detect_object === '1'
|
||||
const width = parseFloat(isObjectDetectionSeparate && d.mon.detector_scale_y_object ? d.mon.detector_scale_y_object : d.mon.detector_scale_y)
|
||||
const height = parseFloat(isObjectDetectionSeparate && d.mon.detector_scale_x_object ? d.mon.detector_scale_x_object : d.mon.detector_scale_x)
|
||||
|
||||
tx({
|
||||
f:'trigger',
|
||||
id:d.id,
|
||||
ke:d.ke,
|
||||
details:{
|
||||
plug: config.plug,
|
||||
name: `PlateRecognizer`,
|
||||
reason: 'object',
|
||||
matrices: mats,
|
||||
imgHeight: width,
|
||||
imgWidth: height,
|
||||
},
|
||||
frame: frameBuffer
|
||||
})
|
||||
}
|
||||
callback()
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<script src="https://cdn.shinobi.video/js/socket.io.js"></script>
|
||||
<script src="https://cdn.shinobi.video/js/jquery.min.js"></script>
|
||||
<!-- my messy GUI -->
|
||||
<style>
|
||||
body {position:relative;}
|
||||
.shinobi_stream{position:absolute;width:100%;height:100%;top:0;left:0;}
|
||||
iframe.stream-element{border:0;}
|
||||
|
||||
|
||||
.stream-objects{position:absolute;top:0;left:0;width:100%;height:100%;z-index:10}
|
||||
.stream-objects .tag{position:absolute;top:5px;left:5px;background:#d9534f;color:#fff;font-family:monospace;font-size:80%;border-radius: 15px;padding:3px 5px;line-height: 1}
|
||||
.stream-objects .stream-detected-object{position:absolute;top:0;left:0;border:3px dotted red;background:transparent;border-radius:5px}
|
||||
.stream-objects .stream-detected-point{position:absolute;top:0;left:0;border:3px solid yellow;background:transparent;border-radius:5px}
|
||||
.stream-objects .point{position:absolute;top:0;left:0;border:3px solid red;border-radius:50%}
|
||||
|
||||
</style>
|
||||
<!-- Full Screen -->
|
||||
<style>
|
||||
body,html{overflow: hidden;height:100%}
|
||||
*{margin:0;padding:0;border:0}
|
||||
.stream-element,.shinobi_stream{position:absolute;top:0;left:0;height:100%}
|
||||
.shinobi_stream video{object-fit: fill}
|
||||
</style>
|
||||
<!-- Full Screen /-->
|
||||
<div class="shinobi_stream" id="stream_container">
|
||||
<div class="stream-objects"></div>
|
||||
<img class="stream-element">
|
||||
</div>
|
||||
<!-- the juice, i mean js -->
|
||||
<script>
|
||||
function getQueryString(){
|
||||
var theObject = {}
|
||||
location.search.substring(1).split('&').forEach(function(string){
|
||||
var parts = string.split('=')
|
||||
theObject[parts[0]] = parts[1]
|
||||
})
|
||||
return theObject
|
||||
}
|
||||
const bodyWidth = $('body').width()
|
||||
const bodyHeight = $('body').height()
|
||||
const queryStringValues = getQueryString()
|
||||
let shinobiOrigin = queryStringValues.shinobiOrigin
|
||||
const monitorId = queryStringValues.monitorId
|
||||
const groupKey = queryStringValues.groupKey
|
||||
const apiKey = queryStringValues.apiKey
|
||||
let loadedWebsocketConnection = null
|
||||
const streamObjectsContainer = $('.stream-objects')
|
||||
if(shinobiOrigin.charAt(shinobiOrigin.length - 1) === '/'){
|
||||
shinobiOrigin = shinobiOrigin.slice(0, -1)
|
||||
}
|
||||
function initMonitorStream(d){
|
||||
$('#stream_container .stream-element').attr('src',`${shinobiOrigin}/${apiKey}/mjpeg/${groupKey}/${monitorId}`)
|
||||
$(window).resize();
|
||||
}
|
||||
function drawMatrices(event){
|
||||
var theContainer = streamObjectsContainer
|
||||
var height = bodyHeight
|
||||
var width = bodyWidth
|
||||
var widthRatio = width / event.details.imgWidth
|
||||
var heightRatio = height / event.details.imgHeight
|
||||
var objectTagGroup = event.details.reason === 'motion' ? 'motion' : event.details.name
|
||||
theContainer.find(`.stream-detected-object[name="${objectTagGroup}"]`).remove()
|
||||
var html = ''
|
||||
$.each(event.details.matrices,function(n,matrix){
|
||||
html += `<div class="stream-detected-object" name="${objectTagGroup}" style="height:${heightRatio * matrix.height}px;width:${widthRatio * matrix.width}px;top:${heightRatio * matrix.y}px;left:${widthRatio * matrix.x}px;">`
|
||||
if(matrix.tag)html += `<span class="tag">${matrix.tag}</span>`
|
||||
html += '</div>'
|
||||
})
|
||||
theContainer.append(html)
|
||||
}
|
||||
|
||||
$(document).ready(function(){
|
||||
$.get(`${shinobiOrigin}/${apiKey}/monitor/${groupKey}/${monitorId}`,function(data){
|
||||
if(
|
||||
!loadedWebsocketConnection ||
|
||||
loadedWebsocketConnection.connected === false
|
||||
){
|
||||
loadedWebsocketConnection = io(shinobiOrigin);
|
||||
loadedWebsocketConnection.on('f',function (d){
|
||||
switch(d.f){
|
||||
case'monitor_watch_off':
|
||||
case'monitor_watch_on':
|
||||
initMonitorStream(d)
|
||||
break;
|
||||
case'detector_trigger':
|
||||
if(d.id !== monitorId)return false;
|
||||
if(
|
||||
d.details.matrices &&
|
||||
d.details.matrices.length > 0
|
||||
){
|
||||
drawMatrices(d)
|
||||
}
|
||||
console.log({
|
||||
ke: groupKey,
|
||||
mid: monitorId,
|
||||
log: {
|
||||
type: 'Event Occurred',
|
||||
msg: d.details,
|
||||
}
|
||||
})
|
||||
break;
|
||||
}
|
||||
})
|
||||
}
|
||||
loadedWebsocketConnection.emit('e',{
|
||||
f: 'init',
|
||||
auth: apiKey,
|
||||
id: monitorId,
|
||||
ke: groupKey
|
||||
})
|
||||
})
|
||||
$(window).resize(function(){
|
||||
$('.stream-element').attr('width',bodyWidth)
|
||||
$('.stream-element').attr('height',bodyHeight)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
|
@ -59,11 +59,7 @@ processArgv.forEach(function(val) {
|
|||
try{
|
||||
if(config.thisIsDocker){
|
||||
const dockerConfigFile = '/config/conf.json'
|
||||
fs.stat(dockerConfigFile,(err) => {
|
||||
if(!err){
|
||||
fs.writeFile(dockerConfigFile,JSON.stringify(config,null,3),function(){})
|
||||
}
|
||||
})
|
||||
fs.writeFileSync(dockerConfigFile,JSON.stringify(config,null,3))
|
||||
}
|
||||
}catch(err){
|
||||
console.log(err)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<form id="hey-activate" class="card shadow mb-3">
|
||||
<div class="card-header">
|
||||
<%- lang['Not Activated'] %>
|
||||
<%- config.hasSubcribed ? lang['Activated'] : lang['Not Activated'] %>
|
||||
</div>
|
||||
<div class="card-body" style="min-height:auto">
|
||||
<div class="form-group">
|
||||
<input name="subscriptionId" id="pass" tabindex="2" class="form-control wide-text" placeholder="License Key / Subscription ID">
|
||||
<input name="subscriptionId" tabindex="2" class="form-control wide-text" placeholder="License Key / Subscription ID">
|
||||
</div>
|
||||
<div class="form-group mb-0">
|
||||
<button class="btn btn-sm btn-round btn-block btn-success" type="submit"><%- lang.Save %></button>
|
||||
|
|
@ -42,10 +42,11 @@
|
|||
noticeTitle = lang.Activated
|
||||
noticeText = lang.activatedText
|
||||
noticeType = 'success'
|
||||
heyActivateCard.remove()
|
||||
heyActivateCard.find('[name="subscriptionId"]').val('')
|
||||
}else{
|
||||
heyActivateCardSubmit.html(lang.Save)
|
||||
}
|
||||
heyActivateCard.find('.card-header').html(noticeTitle)
|
||||
new PNotify({
|
||||
title: noticeTitle,
|
||||
text: noticeText,
|
||||
|
|
|
|||
|
|
@ -103,9 +103,7 @@
|
|||
<div class="tab-pane active" id="accounts" role="tabpanel">
|
||||
<div class="row">
|
||||
<div class="col-md-5 text-left">
|
||||
<% if(!config.userHasSubscribed){ %>
|
||||
<% include blocks/heyActivate.ejs %>
|
||||
<% } %>
|
||||
<% include blocks/heyActivate.ejs %>
|
||||
<div class="card shadow mb-3">
|
||||
<pre class="super-system-info card-body mb-0">
|
||||
|
||||
|
|
@ -349,4 +347,7 @@ $('body')
|
|||
<% customAutoLoad.superLibsJs.forEach(function(lib){ %>
|
||||
<script src="<%-window.libURL%>libs/js/<%-lib%>"></script>
|
||||
<% }) %>
|
||||
<% customAutoLoad.superRawJs.forEach(function(scriptData){ %>
|
||||
<script><%- scriptData %></script>
|
||||
<% }) %>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in New Issue