From b7d08eb500d457d88529d21f21d94e9180fe159c Mon Sep 17 00:00:00 2001 From: Moe Date: Wed, 24 Oct 2018 21:42:47 -0700 Subject: [PATCH] Blue Turtle - The Refactoring --- camera.js | 7936 +------------------ conf.sample.json | 1 + cron.js | 181 +- definitions/en_CA.json | 34 +- languages/en_CA.json | 35 +- libs/auth.js | 191 + libs/basic.js | 227 + libs/childNode.js | 217 + libs/cloudUploaders.js | 555 ++ libs/config.js | 55 + libs/detector.js | 204 + libs/events.js | 384 + libs/extenders.js | 58 + libs/ffmpeg.js | 910 +++ libs/folders.js | 49 + libs/health.js | 76 + libs/language.js | 61 + libs/monitor.js | 1321 +++ libs/notification.js | 318 + libs/plugins.js | 105 + libs/process.js | 33 + libs/socketio.js | 1433 ++++ libs/sql.js | 88 + libs/startup.js | 165 + libs/user.js | 235 + libs/videos.js | 329 + libs/webServer.js | 35 + libs/webServerAdminPaths.js | 428 + libs/webServerPaths.js | 1843 +++++ libs/webServerStreamPaths.js | 358 + libs/webServerSuperPaths.js | 411 + package.json | 6 +- test.js | 77 + test/run.js | 130 + test/testAdminUser.json | 7 + test/testAdminUserDelete.json | 11 + test/testAdminUserEdit.json | 31 + test/testAdminUserRegister.json | 26 + test/testApiAdd.json | 15 + test/testApiDelete.json | 5 + test/testMonitor-WatchOnly.json | 1 + test/testSubUserDelete.json | 4 + test/testSubUserEdit.json | 10 + test/testSubUserRegister.json | 7 + web/libs/css/admin-page.css | 24 + web/libs/css/main.dash2.css | 29 +- web/libs/css/super-page.css | 6 + web/libs/js/basic.js | 36 + web/libs/js/extra.js | 11 + web/libs/js/main.dash2.js | 750 +- web/libs/js/moment.js | 2 +- web/libs/js/poseidon.js | 13 +- web/pages/admin.ejs | 100 +- web/pages/blocks/api.ejs | 2 +- web/pages/blocks/changeSuperPreferences.ejs | 39 + web/pages/blocks/detectorfilters.ejs | 20 +- web/pages/blocks/header-favicon.ejs | 4 +- web/pages/blocks/header.ejs | 4 +- web/pages/blocks/mainpermissions.ejs | 86 +- web/pages/blocks/monitoredit.ejs | 55 +- web/pages/blocks/region.ejs | 48 +- web/pages/blocks/settings.ejs | 138 +- web/pages/blocks/videoview.ejs | 18 +- web/pages/embed.ejs | 10 +- web/pages/home.ejs | 6 +- web/pages/index.ejs | 5 + web/pages/super.ejs | 91 +- 67 files changed, 11651 insertions(+), 8452 deletions(-) create mode 100644 libs/auth.js create mode 100644 libs/basic.js create mode 100644 libs/childNode.js create mode 100644 libs/cloudUploaders.js create mode 100644 libs/config.js create mode 100644 libs/detector.js create mode 100644 libs/events.js create mode 100644 libs/extenders.js create mode 100644 libs/ffmpeg.js create mode 100644 libs/folders.js create mode 100644 libs/health.js create mode 100644 libs/language.js create mode 100644 libs/monitor.js create mode 100644 libs/notification.js create mode 100644 libs/plugins.js create mode 100644 libs/process.js create mode 100644 libs/socketio.js create mode 100644 libs/sql.js create mode 100644 libs/startup.js create mode 100644 libs/user.js create mode 100644 libs/videos.js create mode 100644 libs/webServer.js create mode 100644 libs/webServerAdminPaths.js create mode 100644 libs/webServerPaths.js create mode 100644 libs/webServerStreamPaths.js create mode 100644 libs/webServerSuperPaths.js create mode 100644 test.js create mode 100644 test/run.js create mode 100644 test/testAdminUser.json create mode 100644 test/testAdminUserDelete.json create mode 100644 test/testAdminUserEdit.json create mode 100644 test/testAdminUserRegister.json create mode 100644 test/testApiAdd.json create mode 100644 test/testApiDelete.json create mode 100644 test/testMonitor-WatchOnly.json create mode 100644 test/testSubUserDelete.json create mode 100644 test/testSubUserEdit.json create mode 100644 test/testSubUserRegister.json create mode 100644 web/libs/css/admin-page.css create mode 100644 web/libs/css/super-page.css create mode 100644 web/libs/js/basic.js create mode 100644 web/libs/js/extra.js create mode 100644 web/pages/blocks/changeSuperPreferences.ejs diff --git a/camera.js b/camera.js index 630ec37..809a995 100644 --- a/camera.js +++ b/camera.js @@ -8,7885 +8,61 @@ // If you like what I am doing here and want me to continue please consider donating :) // PayPal : paypal@m03.ca // -var fs = require('fs'); -process.on('uncaughtException', function (err) { - console.error('Uncaught Exception occured!'); - console.error(err.stack); -}); -process.on('SIGINT', function() { - process.exit(); -}); -var staticFFmpeg = false; -try{ - staticFFmpeg = require('ffmpeg-static').path; - if (!fs.existsSync(staticFFmpeg)) { - staticFFmpeg = false - console.log('"ffmpeg-static" from NPM has failed to provide a compatible library or has been corrupted.') - console.log('You may need to install FFmpeg manually or you can try running "npm uninstall ffmpeg-static && npm install ffmpeg-static".') - } -}catch(err){ - staticFFmpeg = false; - console.log('No Static FFmpeg. Continuing.') +var io = new (require('socket.io'))() +//library loader +var loadLib = function(lib){ + return require(__dirname+'/libs/'+lib+'.js') } -var os = require('os'); -var URL = require('url'); -var path = require('path'); -var mysql = require('mysql'); -var moment = require('moment'); -var request = require("request"); -var express = require('express'); -var app = express(); -var http = require('http'); -var https = require('https'); -var server = http.createServer(app); -var bodyParser = require('body-parser'); -var CircularJSON = require('circular-json'); -var ejs = require('ejs'); -var io = new (require('socket.io'))(); -var execSync = require('child_process').execSync; -var exec = require('child_process').exec; -var spawn = require('child_process').spawn; -var socketIOclient = require('socket.io-client'); -var crypto = require('crypto'); -var webdav = require("webdav"); -var jsonfile = require("jsonfile"); -var connectionTester = require('connection-tester'); -var events = require('events'); -var onvif = require('node-onvif'); -var onvifHawk = require('onvif-nvt'); -var knex = require('knex'); -var Mp4Frag = require('mp4frag'); -var P2P = require('pipe2pam'); -var PamDiff = require('pam-diff'); -var httpProxy = require('http-proxy'); -var proxy = httpProxy.createProxyServer({}) -var location = {} -location.super = __dirname+'/super.json' -location.config = __dirname+'/conf.json' -location.languages = __dirname+'/languages' -location.definitions = __dirname+'/definitions' -var config = require(location.config); -if(!config.productType){ - config.productType='CE' -} -if(config.productType==='Pro'){ - var LdapAuth = require('ldapauth-fork'); -} -if(!config.language){ - config.language='en_CA' -} -try{ - var lang = require(location.languages+'/'+config.language+'.json'); -}catch(er){ - console.error(er) - console.log('There was an error loading your language file.') - var lang = require(location.languages+'/en_CA.json'); -} -try{ - var definitions = require(location.definitions+'/'+config.language+'.json'); -}catch(er){ - console.error(er) - console.log('There was an error loading your language file.') - var definitions = require(location.definitions+'/en_CA.json'); -} -process.send = process.send || function () {}; -if(config.mail){ - if(config.mail.from === undefined){config.mail.from = '"ShinobiCCTV" '} - var nodemailer = require('nodemailer').createTransport(config.mail); -} -//config defaults -if(config.cpuUsageMarker === undefined){config.cpuUsageMarker='%Cpu'} -if(config.customCpuCommand === undefined){config.customCpuCommand=null} -if(config.autoDropCache === undefined){config.autoDropCache=true} -if(config.doSnapshot === undefined){config.doSnapshot=true} -if(config.restart === undefined){config.restart={}} -if(config.systemLog === undefined){config.systemLog=true} -if(config.deleteCorruptFiles === undefined){config.deleteCorruptFiles=true} -if(config.restart.onVideoNotExist === undefined){config.restart.onVideoNotExist=true} -if(config.ip === undefined||config.ip===''||config.ip.indexOf('0.0.0.0')>-1){config.ip='localhost'}else{config.bindip=config.ip}; -if(config.cron === undefined)config.cron={}; -if(config.cron.enabled === undefined)config.cron.enabled=true; -if(config.cron.deleteOld === undefined)config.cron.deleteOld=true; -if(config.cron.deleteOrphans === undefined)config.cron.deleteOrphans=false; -if(config.cron.deleteNoVideo === undefined)config.cron.deleteNoVideo=true; -if(config.cron.deleteNoVideoRecursion === undefined)config.cron.deleteNoVideoRecursion=false; -if(config.cron.deleteOverMax === undefined)config.cron.deleteOverMax=true; -if(config.cron.deleteOverMaxOffset === undefined)config.cron.deleteOverMaxOffset=0.9; -if(config.cron.deleteLogs === undefined)config.cron.deleteLogs=true; -if(config.cron.deleteEvents === undefined)config.cron.deleteEvents=true; -if(config.cron.deleteFileBins === undefined)config.cron.deleteFileBins=true; -if(config.cron.interval === undefined)config.cron.interval=1; -if(config.databaseType === undefined){config.databaseType='mysql'} -if(config.pluginKeys === undefined)config.pluginKeys={}; -if(config.databaseLogs === undefined){config.databaseLogs=false} -if(config.useUTC === undefined){config.useUTC=false} -if(config.iconURL === undefined){config.iconURL = "https://shinobi.video/libs/assets/icon/apple-touch-icon-152x152.png"} -if(config.pipeAddition === undefined){config.pipeAddition=7}else{config.pipeAddition=parseInt(config.pipeAddition)} -//Child Nodes -if(config.childNodes === undefined)config.childNodes = {}; - //enabled - if(config.childNodes.enabled === undefined)config.childNodes.enabled = false; - //mode, set value as `child` for all other machines in the cluster - if(config.childNodes.mode === undefined)config.childNodes.mode = 'master'; - //child node connection port - if(config.childNodes.port === undefined)config.childNodes.port = 8288; - //child node connection key - if(config.childNodes.key === undefined)config.childNodes.key = [ - '3123asdasdf1dtj1hjk23sdfaasd12asdasddfdbtnkkfgvesra3asdsd3123afdsfqw345' - ]; - - -s={ - factorAuth : {}, - totalmem : os.totalmem(), - platform : os.platform(), - s : JSON.stringify, - isWin : (process.platform==='win32'), - utcOffset : moment().utcOffset() -}; -//load languages dynamically -s.loadedLanguages={} -s.loadedLanguages[config.language]=lang; -s.getLanguageFile=function(rule){ - if(rule&&rule!==''){ - var file=s.loadedLanguages[file] - if(!file){ - try{ - s.loadedLanguages[rule]=require(location.languages+'/'+rule+'.json') - file=s.loadedLanguages[rule] - }catch(err){ - file=lang - } - } - }else{ - file=lang - } - return file -} -//load defintions dynamically -s.loadedDefinitons={} -s.loadedDefinitons[config.language]=definitions; -s.getDefinitonFile=function(rule){ - if(rule&&rule!==''){ - var file=s.loadedDefinitons[file] - if(!file){ - try{ - s.loadedDefinitons[rule]=require(location.definitions+'/'+rule+'.json') - file=s.loadedDefinitons[rule] - }catch(err){ - file=definitions - } - } - }else{ - file=definitions - } - return file -} -//sql/database connection with knex -var databaseOptions = { - client: config.databaseType, - connection: config.db, -} -if(databaseOptions.client.indexOf('sqlite')>-1){ - databaseOptions.client = 'sqlite3'; - databaseOptions.useNullAsDefault = true; -} -if(databaseOptions.client === 'sqlite3' && databaseOptions.connection.filename === undefined){ - databaseOptions.connection.filename = __dirname+"/shinobi.sqlite" -} -s.databaseEngine = knex(databaseOptions) -s.mergeQueryValues = function(query,values){ - if(!values){values=[]} - var valuesNotFunction = true; - if(typeof values === 'function'){ - var values = []; - valuesNotFunction = false; - } - if(values&&valuesNotFunction){ - var splitQuery = query.split('?') - var newQuery = '' - splitQuery.forEach(function(v,n){ - newQuery += v - var value = values[n] - if(value){ - if(isNaN(value) || value instanceof Date){ - newQuery += "'"+value+"'" - }else{ - newQuery += value - } - } - }) - }else{ - newQuery = query - } - return newQuery -} -s.stringToSqlTime = function(value){ - newValue = new Date(value.replace('T',' ')) - return newValue -} -s.sqlQuery = function(query,values,onMoveOn){ - if(!values){values=[]} - if(typeof values === 'function'){ - var onMoveOn = values; - var values = []; - } - if(!onMoveOn){onMoveOn=function(){}} - var mergedQuery = s.mergeQueryValues(query,values) - s.debugLog('s.sqlQuery QUERY',mergedQuery) - return s.databaseEngine - .raw(query,values) - .asCallback(function(err,r){ - if(err){ - console.log('s.sqlQuery QUERY ERRORED',query) - console.log('s.sqlQuery ERROR',err) - } - if(onMoveOn && typeof onMoveOn === 'function'){ - switch(databaseOptions.client){ - case'sqlite3': - if(!r)r=[] - break; - default: - if(r)r=r[0] - break; - } - onMoveOn(err,r) - } - }) -} -//discord bot -if(config.discordBot === true){ - try{ - var Discord = require("discord.js") - s.discordMsg = function(data,files,groupKey){ - if(!data)data = {}; - var bot = s.group[groupKey].discordBot - if(!bot){ - s.log({ke:groupKey,mid:'$USER'},{type:lang.DiscordFailedText,msg:lang.DiscordNotEnabledText}) - return - } - var sendBody = Object.assign({ - color: 3447003, - title: 'Alert from Shinobi', - description: "", - fields: [], - timestamp: new Date(), - footer: { - icon_url: config.iconURL, - text: "Shinobi Systems" - } - },data) - bot.channels.get(s.group[groupKey].init.discordbot_channel).send({ - embed: sendBody, - files: files - }).catch(err => { - if(err){ - s.log({ke:groupKey,mid:'$USER'},{type:lang.DiscordErrorText,msg:err}) - s.group[groupKey].discordBot = null - s.init('apps',{ke:groupKey}) - } - }) - } - }catch(err){ - console.log('Could not start Discord bot, please run "npm install discord.js" inside the Shinobi folder.') - s.discordMsg = function(){} - } -} -//kill any ffmpeg running -s.ffmpegKill=function(){ - var cmd='' - if(s.isWin===true){ - cmd="Taskkill /IM ffmpeg.exe /F" - }else{ - cmd="ps aux | grep -ie ffmpeg | awk '{print $2}' | xargs kill -9" - } - exec(cmd,{detached: true}) -}; -process.on('exit',s.ffmpegKill.bind(null,{cleanup:true})); -process.on('SIGINT',s.ffmpegKill.bind(null, {exit:true})); -s.checkRelativePath=function(x){ - if(x.charAt(0)!=='/'){ - x=__dirname+'/'+x - } - return x -} -s.checkCorrectPathEnding=function(x){ - var length=x.length - if(x.charAt(length-1)!=='/'){ - x=x+'/' - } - return x.replace('__DIR__',__dirname) -} -s.md5=function(x){return crypto.createHash('md5').update(x).digest("hex");} -//send data to detector plugin -s.ocvTx=function(data){ - if(!s.ocv){return} - if(s.ocv.isClientPlugin===true){ - s.tx(data,s.ocv.id) - }else{ - s.connectedPlugins[s.ocv.plug].tx(data) - } -} -//send data to socket client function -s.tx = function(z,y,x){if(x){return x.broadcast.to(y).emit('f',z)};io.to(y).emit('f',z);} -s.txToDashcamUsers = function(data,groupKey){ - if(s.group[groupKey] && s.group[groupKey].dashcamUsers){ - Object.keys(s.group[groupKey].dashcamUsers).forEach(function(auth){ - s.tx(data,s.group[groupKey].dashcamUsers[auth].cnid) - }) - } -} -s.txWithSubPermissions = function(z,y,permissionChoices){ - if(typeof permissionChoices==='string'){ - permissionChoices=[permissionChoices] - } - if(s.group[z.ke]){ - Object.keys(s.group[z.ke].users).forEach(function(v){ - var user = s.group[z.ke].users[v] - if(user.details.sub){ - if(user.details.allmonitors!=='1'){ - var valid=0 - var checked=permissionChoices.length - permissionChoices.forEach(function(b){ - if(user.details[b] && user.details[b].indexOf(z.mid)!==-1){ - ++valid - } - }) - if(valid===checked){ - s.tx(z,user.cnid) - } - }else{ - s.tx(z,user.cnid) - } - }else{ - s.tx(z,user.cnid) - } - }) - } -} -//load camera controller vars -s.nameToTime=function(x){x=x.split('.')[0].split('T'),x[1]=x[1].replace(/-/g,':');x=x.join(' ');return x;} -s.ratio=function(width,height,ratio){ratio = width / height;return ( Math.abs( ratio - 4 / 3 ) < Math.abs( ratio - 16 / 9 ) ) ? '4:3' : '16:9';} -s.randomNumber=function(x){ - if(!x){x=10}; - return Math.floor((Math.random() * x) + 1); -}; -s.gid=function(x){ - if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - for( var i=0; i < x; i++ ) - t += p.charAt(Math.floor(Math.random() * p.length)); - return t; -}; -s.nid=function(x){ - if(!x){x=6};var t = "";var p = "0123456789"; - for( var i=0; i < x; i++ ) - t += p.charAt(Math.floor(Math.random() * p.length)); - return t; -}; -s.formattedTime_withOffset=function(e,x){ - if(!e){e=new Date};if(!x){x='YYYY-MM-DDTHH-mm-ss'}; - e=s.timeObject(e);if(config.utcOffset){e=e.utcOffset(config.utcOffset)} - return e.format(x); -} -s.formattedTime=function(e,x){ - if(!e){e=new Date};if(!x){x='YYYY-MM-DDTHH-mm-ss'}; - return s.timeObject(e).format(x); -} -s.utcToLocal = function(time){ - return moment.utc(time).utcOffset(s.utcOffset).format() -} -s.localTimeObject = function(e,x){ - return moment(e) -} -if(config.useUTC === true){ - s.timeObject = function(time){ - return moment(time).utc() - } -}else{ - s.timeObject = moment -} -console.log('config.useUTC',config.useUTC) -s.ipRange=function(start_ip, end_ip) { - var start_long = s.toLong(start_ip); - var end_long = s.toLong(end_ip); - if (start_long > end_long) { - var tmp=start_long; - start_long=end_long - end_long=tmp; - } - var range_array = []; - var i; - for (i=start_long; i<=end_long;i++) { - range_array.push(s.fromLong(i)); - } - return range_array; -} -s.portRange=function(lowEnd,highEnd){ - var list = []; - for (var i = lowEnd; i <= highEnd; i++) { - list.push(i); - } - return list; -} -//toLong taken from NPM package 'ip' -s.toLong=function(ip) { - var ipl = 0; - ip.split('.').forEach(function(octet) { - ipl <<= 8; - ipl += parseInt(octet); - }); - return(ipl >>> 0); -}; - -//fromLong taken from NPM package 'ip' -s.fromLong=function(ipl) { - return ((ipl >>> 24) + '.' + - (ipl >> 16 & 255) + '.' + - (ipl >> 8 & 255) + '.' + - (ipl & 255) ); -}; -s.getFunctionParamNames = function(func) { - var fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, ''); - var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g); - if(result === null) - result = []; - return result; -} -s.getDetectorStreams = function(monitor){ - var pathDir = s.dir.streams+monitor.ke+'/'+monitor.id+'/' - var streamDirItems = fs.readdirSync(pathDir) - var items = [] - streamDirItems.forEach(function(filename){ - if(filename.indexOf('detectorStream') > -1 && filename.indexOf('.m3u8') === -1){ - try{ - items.push(pathDir+filename) - }catch(err){ - console.log(err) - } - } - }) - return items -} -s.createPamDiffEngine = function(e){ - var width, - height, - globalSensitivity, - fullFrame = false - if(s.group[e.ke].mon_conf[e.id].details.detector_scale_x===''||s.group[e.ke].mon_conf[e.id].details.detector_scale_y===''){ - width = s.group[e.ke].mon_conf[e.id].details.detector_scale_x; - height = s.group[e.ke].mon_conf[e.id].details.detector_scale_y; - }else{ - width = e.width - height = e.height - } - if(e.details.detector_sensitivity===''){ - globalSensitivity = 10 - }else{ - globalSensitivity = parseInt(e.details.detector_sensitivity) - } - if(e.details.detector_frame==='1'){ - fullFrame={ - name:'FULL_FRAME', - sensitivity:globalSensitivity, - points:[ - [0,0], - [0,height], - [width,height], - [width,0] - ] - }; - } - var regions = s.createPamDiffRegionArray(s.group[e.ke].mon_conf[e.id].details.cords,globalSensitivity,fullFrame); - if(!s.group[e.ke].mon[e.id].noiseFilterArray)s.group[e.ke].mon[e.id].noiseFilterArray = {} - var noiseFilterArray = s.group[e.ke].mon[e.id].noiseFilterArray - Object.keys(regions.notForPam).forEach(function(name){ - if(!noiseFilterArray[name])noiseFilterArray[name]=[]; - }) - s.group[e.ke].mon[e.id].pamDiff = new PamDiff({grayscale: 'luminosity', regions : regions.forPam}); - s.group[e.ke].mon[e.id].p2p = new P2P(); - var sendTrigger = function(trigger){ - var detectorObject = { - f:'trigger', - id:e.id, - ke:e.ke, - name:trigger.name, - details:{ - plug:'built-in', - name:trigger.name, - reason:'motion', - confidence:trigger.percent, - }, - plates:[], - imgHeight:height, - imgWidth:width - } - detectorObject.doObjectDetection = (s.ocv && e.details.detector_use_detect_object === '1') - s.event('trigger',detectorObject) - if(detectorObject.doObjectDetection === true){ - s.ocvTx({f:'frame',mon:s.group[e.ke].mon_conf[e.id].details,ke:e.ke,id:e.id,time:s.formattedTime(),frame:s.group[e.ke].mon[e.id].lastJpegDetectorFrame}); - } - } - var filterTheNoise = function(trigger){ - if(noiseFilterArray[trigger.name].length > 2){ - var thePreviousTriggerPercent = noiseFilterArray[trigger.name][noiseFilterArray[trigger.name].length - 1]; - var triggerDifference = trigger.percent - thePreviousTriggerPercent; - var noiseRange = e.details.detector_noise_filter_range - if(!noiseRange || noiseRange === ''){ - noiseRange = 6 - } - noiseRange = parseFloat(noiseRange) - if(((trigger.percent - thePreviousTriggerPercent) < noiseRange)||(thePreviousTriggerPercent - trigger.percent) > -noiseRange){ - noiseFilterArray[trigger.name].push(trigger.percent); - } - }else{ - noiseFilterArray[trigger.name].push(trigger.percent); - } - if(noiseFilterArray[trigger.name].length > 10){ - noiseFilterArray[trigger.name] = noiseFilterArray[trigger.name].splice(1,10) - } - var theNoise = 0; - noiseFilterArray[trigger.name].forEach(function(v,n){ - theNoise += v; - }) - theNoise = theNoise / noiseFilterArray[trigger.name].length; -// console.log(noiseFilterArray[trigger.name]) -// console.log(theNoise) - var triggerPercentWithoutNoise = trigger.percent - theNoise; - if(triggerPercentWithoutNoise > regions.notForPam[trigger.name].sensitivity){ - sendTrigger(trigger); - } - } - if(e.details.detector_noise_filter==='1'){ - s.group[e.ke].mon[e.id].pamDiff.on('diff', (data) => { - data.trigger.forEach(filterTheNoise) - }) - }else{ - s.group[e.ke].mon[e.id].pamDiff.on('diff', (data) => { - data.trigger.forEach(sendTrigger) - }) - } -} -s.createPamDiffRegionArray = function(regions,globalSensitivity,fullFrame){ - var pamDiffCompliantArray = [], - arrayForOtherStuff = [], - json - try{ - json = JSON.parse(regions) - }catch(err){ - json = regions - } - if(fullFrame){ - json[fullFrame.name]=fullFrame; - } - Object.values(json).forEach(function(region){ - if(!region)return false; - region.polygon = []; - region.points.forEach(function(points){ - region.polygon.push({x:parseFloat(points[0]),y:parseFloat(points[1])}) - }) - if(region.sensitivity===''){ - region.sensitivity = globalSensitivity - }else{ - region.sensitivity = parseInt(region.sensitivity) - } - pamDiffCompliantArray.push({name: region.name, difference: 9, percent: region.sensitivity, polygon:region.polygon}) - arrayForOtherStuff[region.name] = region; - }) - if(pamDiffCompliantArray.length===0){pamDiffCompliantArray = null} - return {forPam:pamDiffCompliantArray,notForPam:arrayForOtherStuff}; -} -s.getRequest = function(url,callback){ - return http.get(url, function(res){ - var body = ''; - res.on('data', function(chunk){ - body += chunk; - }); - res.on('end',function(){ - try{body = JSON.parse(body)}catch(err){} - callback(body) - }); - }).on('error', function(e){ -// s.systemLog("Get Snapshot Error", e); - }); -} -s.kill = function(x,e,p){ - if(s.group[e.ke]&&s.group[e.ke].mon[e.id]&&s.group[e.ke].mon[e.id].spawn !== undefined){ - if(s.group[e.ke].mon[e.id].spawn){ - s.group[e.ke].mon[e.id].allowStdinWrite = false - s.txToDashcamUsers({ - f : 'disable_stream', - ke : e.ke, - mid : e.id - },e.ke) - s.group[e.ke].mon[e.id].spawn.stdio[3].unpipe(); -// if(s.group[e.ke].mon[e.id].p2pStream){s.group[e.ke].mon[e.id].p2pStream.unpipe();} - if(s.group[e.ke].mon[e.id].p2p){s.group[e.ke].mon[e.id].p2p.unpipe();} - delete(s.group[e.ke].mon[e.id].p2pStream) - delete(s.group[e.ke].mon[e.id].p2p) - delete(s.group[e.ke].mon[e.id].pamDiff) - try{ - s.group[e.ke].mon[e.id].spawn.removeListener('end',s.group[e.ke].mon[e.id].spawn_exit); - s.group[e.ke].mon[e.id].spawn.removeListener('exit',s.group[e.ke].mon[e.id].spawn_exit); - delete(s.group[e.ke].mon[e.id].spawn_exit); - }catch(er){} - } - s.group[e.ke].mon[e.id].firstStreamChunk = {} - clearTimeout(s.group[e.ke].mon[e.id].checker); - delete(s.group[e.ke].mon[e.id].checker); - clearTimeout(s.group[e.ke].mon[e.id].checkStream); - delete(s.group[e.ke].mon[e.id].checkStream); - clearTimeout(s.group[e.ke].mon[e.id].checkSnap); - delete(s.group[e.ke].mon[e.id].checkSnap); - clearTimeout(s.group[e.ke].mon[e.id].watchdog_stop); - delete(s.group[e.ke].mon[e.id].watchdog_stop); - delete(s.group[e.ke].mon[e.id].lastJpegDetectorFrame); - if(e&&s.group[e.ke].mon[e.id].record){ - clearTimeout(s.group[e.ke].mon[e.id].record.capturing); -// if(s.group[e.ke].mon[e.id].record.request){s.group[e.ke].mon[e.id].record.request.abort();delete(s.group[e.ke].mon[e.id].record.request);} - }; - if(s.group[e.ke].mon[e.id].childNode){ - s.cx({f:'kill',d:s.init('noReference',e)},s.group[e.ke].mon[e.id].childNodeId) - }else{ - s.coSpawnClose(e) - if(!x||x===1){return}; - p=x.pid; - if(s.group[e.ke].mon_conf[e.id].type===('dashcam'||'socket'||'jpeg'||'pipe')){ - x.stdin.pause();setTimeout(function(){x.kill('SIGTERM');},500) - }else{ - try{ - x.stdin.setEncoding('utf8');x.stdin.write('q'); - }catch(er){} - } - setTimeout(function(){exec('kill -9 '+p,{detached: true})},1000) - } - } -} -//user log -s.log=function(e,x){ - if(!x||!e.mid){return} - if((e.details&&e.details.sqllog==='1')||e.mid.indexOf('$')>-1){ - s.sqlQuery('INSERT INTO Logs (ke,mid,info) VALUES (?,?,?)',[e.ke,e.mid,s.s(x)]); - } - s.tx({f:'log',ke:e.ke,mid:e.mid,log:x,time:s.timeObject()},'GRPLOG_'+e.ke); -// s.systemLog('s.log : ',{f:'log',ke:e.ke,mid:e.mid,log:x,time:s.timeObject()},'GRP_'+e.ke) -} -//system log -s.systemLog = function(q,w,e){ - if(!w){w=''} - if(!e){e=''} - if(config.systemLog===true){ - if(typeof q==='string'&&s.databaseEngine){ - s.sqlQuery('INSERT INTO Logs (ke,mid,info) VALUES (?,?,?)',['$','$SYSTEM',s.s({type:q,msg:w})]); - s.tx({f:'log',log:{time:s.timeObject(),ke:'$',mid:'$SYSTEM',time:s.timeObject(),info:s.s({type:q,msg:w})}},'$'); - } - return console.log(s.timeObject().format(),q,w,e) - } -} -//system log -s.debugLog = function(q,w,e){ - if(config.debugLog === true){ - if(!w){w = ''} - if(!e){e = ''} - console.log(s.timeObject().format(),q,w,e) - if(config.debugLogVerbose === true){ - console.log(new Error()) - } - } -} -//SSL options -if(config.ssl&&config.ssl.key&&config.ssl.cert){ - config.ssl.key=fs.readFileSync(s.checkRelativePath(config.ssl.key),'utf8') - config.ssl.cert=fs.readFileSync(s.checkRelativePath(config.ssl.cert),'utf8') - if(config.ssl.port === undefined){ - config.ssl.port=443 - } - if(config.ssl.bindip === undefined){ - config.ssl.bindip=config.bindip - } - if(config.ssl.ca&&config.ssl.ca instanceof Array){ - config.ssl.ca.forEach(function(v,n){ - config.ssl.ca[n]=fs.readFileSync(s.checkRelativePath(v),'utf8') - }) - } - var serverHTTPS = https.createServer(config.ssl,app); - serverHTTPS.listen(config.ssl.port,config.bindip,function(){ - console.log('SSL '+lang.Shinobi+' - SSL PORT : '+config.ssl.port); - }); - io.attach(serverHTTPS); -} -//start HTTP -server.listen(config.port,config.bindip,function(){ - console.log(lang.Shinobi+' - PORT : '+config.port); -}); -io.attach(server); -console.log('NODE.JS version : '+execSync("node -v")) -//ffmpeg location -if(!config.ffmpegDir){ - if(staticFFmpeg !== false){ - config.ffmpegDir = staticFFmpeg - }else{ - if(s.isWin===true){ - config.ffmpegDir = __dirname+'/ffmpeg/ffmpeg.exe' - }else{ - config.ffmpegDir = 'ffmpeg' - } - } -} -//ffmpeg version -s.ffmpegVersion=execSync(config.ffmpegDir+" -version").toString().split('Copyright')[0].replace('ffmpeg version','').trim() -console.log('FFMPEG version : '+s.ffmpegVersion) -if(s.ffmpegVersion.indexOf(': 2.')>-1){ - s.systemLog('FFMPEG is too old : '+s.ffmpegVersion+', Needed : 3.2+',err) - throw (new Error()) -} -//directories -s.group={}; -if(!config.windowsTempDir&&s.isWin===true){config.windowsTempDir='C:/Windows/Temp'} -if(!config.defaultMjpeg){config.defaultMjpeg=__dirname+'/web/libs/img/bg.jpg'} -//default stream folder check -if(!config.streamDir){ - if(s.isWin===false){ - config.streamDir='/dev/shm' - }else{ - config.streamDir=config.windowsTempDir - } - if(!fs.existsSync(config.streamDir)){ - config.streamDir=__dirname+'/streams/' - }else{ - config.streamDir+='/streams/' - } -} -if(!config.videosDir){config.videosDir=__dirname+'/videos/'} -if(!config.binDir){config.binDir=__dirname+'/fileBin/'} -if(!config.addStorage){config.addStorage=[]} -s.dir={ - videos:s.checkCorrectPathEnding(config.videosDir), - streams:s.checkCorrectPathEnding(config.streamDir), - fileBin:s.checkCorrectPathEnding(config.binDir), - addStorage:config.addStorage, - languages:location.languages+'/' -}; -//streams dir -if(!fs.existsSync(s.dir.streams)){ - fs.mkdirSync(s.dir.streams); -} -//videos dir -if(!fs.existsSync(s.dir.videos)){ - fs.mkdirSync(s.dir.videos); -} -//fileBin dir -if(!fs.existsSync(s.dir.fileBin)){ - fs.mkdirSync(s.dir.fileBin); -} -//additional storage areas -s.dir.addStorage.forEach(function(v,n){ - v.path=s.checkCorrectPathEnding(v.path) - if(!fs.existsSync(v.path)){ - fs.mkdirSync(v.path); - } +//process handlers +var s = loadLib('process')(process,__dirname) +//configuration loader +var config = loadLib('config')(s) +//language loader +var lang = loadLib('language')(s,config) +//basic functions +loadLib('basic')(s,config) +//load extender functions +loadLib('extenders')(s,config) +//video processing engine +loadLib('ffmpeg')(s,config,function(){ + //database connection : mysql, sqlite3.. + loadLib('sql')(s,config) + //working directories : videos, streams, fileBin.. + loadLib('folders')(s,config) + //authenticator functions : API, dashboard login.. + loadLib('auth')(s,config,lang) + //express web server with ejs + var app = loadLib('webServer')(s,config,lang,io) + //web server routes : page handling.. + loadLib('webServerPaths')(s,config,lang,app) + //web server routes for streams : streams.. + loadLib('webServerStreamPaths')(s,config,lang,app) + //web server admin routes : create sub accounts, share monitors, share videos + loadLib('webServerAdminPaths')(s,config,lang,app) + //web server superuser routes : create admin accounts and manage system functions + loadLib('webServerSuperPaths')(s,config,lang,app) + //websocket connection handlers : login and streams.. + loadLib('socketio')(s,config,lang,io) + //user and group functions + loadLib('user')(s,config) + //monitor/camera handlers + loadLib('monitor')(s,config,lang) + //event functions : motion, object matrix handler + loadLib('events')(s,config,lang) + //built-in detector functions : pam-diff.. + loadLib('detector')(s,config) + //recording functions + loadLib('videos')(s,config,lang) + //plugins : websocket connected services.. + loadLib('plugins')(s,config,lang) + //health : cpu and ram trackers.. + loadLib('health')(s,config,lang,io) + //cluster module + loadLib('childNode')(s,config,lang,app,io) + //cloud uploaders : amazon s3, webdav, backblaze b2.. + loadLib('cloudUploaders')(s,config,lang) + //notifiers : discord.. + loadLib('notification')(s,config,lang) + //on-start actions, daemon(s) starter + loadLib('startup')(s,config,lang) }) -////Camera Controller -s.init=function(x,e,k,fn){ - if(!e){e={}} - if(!k){k={}} - switch(x){ - case 0://init camera - if(!s.group[e.ke]){s.group[e.ke]={}}; - if(!s.group[e.ke].mon){s.group[e.ke].mon={}} - if(!s.group[e.ke].mon[e.mid]){s.group[e.ke].mon[e.mid]={}} - if(!s.group[e.ke].mon[e.mid].streamIn){s.group[e.ke].mon[e.mid].streamIn={}}; - if(!s.group[e.ke].mon[e.mid].emitterChannel){s.group[e.ke].mon[e.mid].emitterChannel={}}; - if(!s.group[e.ke].mon[e.mid].mp4frag){s.group[e.ke].mon[e.mid].mp4frag={}}; - if(!s.group[e.ke].mon[e.mid].firstStreamChunk){s.group[e.ke].mon[e.mid].firstStreamChunk={}}; - if(!s.group[e.ke].mon[e.mid].contentWriter){s.group[e.ke].mon[e.mid].contentWriter={}}; - if(!s.group[e.ke].mon[e.mid].childNodeStreamWriters){s.group[e.ke].mon[e.mid].childNodeStreamWriters={}}; - if(!s.group[e.ke].mon[e.mid].eventBasedRecording){s.group[e.ke].mon[e.mid].eventBasedRecording={}}; - if(!s.group[e.ke].mon[e.mid].watch){s.group[e.ke].mon[e.mid].watch={}}; - if(!s.group[e.ke].mon[e.mid].fixingVideos){s.group[e.ke].mon[e.mid].fixingVideos={}}; - if(!s.group[e.ke].mon[e.mid].record){s.group[e.ke].mon[e.mid].record={yes:e.record}}; - if(!s.group[e.ke].mon[e.mid].started){s.group[e.ke].mon[e.mid].started=0}; - if(s.group[e.ke].mon[e.mid].delete){clearTimeout(s.group[e.ke].mon[e.mid].delete)} - if(!s.group[e.ke].mon_conf){s.group[e.ke].mon_conf={}} - break; - case'group': - if(!s.group[e.ke]){ - s.group[e.ke]={} - } - if(!s.group[e.ke].init){ - s.group[e.ke].init={} - } - if(!s.group[e.ke].fileBin){s.group[e.ke].fileBin={}}; - if(!s.group[e.ke].users){s.group[e.ke].users={}} - if(!s.group[e.ke].dashcamUsers){s.group[e.ke].dashcamUsers={}} - if(!e.limit||e.limit===''){e.limit=10000}else{e.limit=parseFloat(e.limit)} - //save global space limit for group key (mb) - s.group[e.ke].sizeLimit=e.limit; - //save global used space as megabyte value - s.group[e.ke].usedSpace=e.size/1000000; - //emit the changes to connected users - s.init('diskUsedEmit',e) - break; - case'apps': - if(!s.group[e.ke].init){ - s.group[e.ke].init={}; - } - if(!s.group[e.ke].webdav||!s.group[e.ke].sizeLimit){ - s.sqlQuery('SELECT * FROM Users WHERE ke=? AND details NOT LIKE ?',[e.ke,'%"sub"%'],function(ar,r){ - if(r&&r[0]){ - r=r[0]; - ar=JSON.parse(r.details); - //owncloud/webdav - if(ar.webdav_user&& - ar.webdav_user!==''&& - ar.webdav_pass&& - ar.webdav_pass!==''&& - ar.webdav_url&& - ar.webdav_url!=='' - ){ - if(!ar.webdav_dir||ar.webdav_dir===''){ - ar.webdav_dir='/' - } - ar.webdav_dir = s.checkCorrectPathEnding(ar.webdav_dir) - s.group[e.ke].webdav = webdav( - ar.webdav_url, - ar.webdav_user, - ar.webdav_pass - ); - } - //Amazon S3 - if(!s.group[e.ke].aws && - !s.group[e.ke].aws_s3 && - ar.aws_s3 !== '0' && - ar.aws_accessKeyId !== ''&& - ar.aws_secretAccessKey && - ar.aws_secretAccessKey !== ''&& - ar.aws_region && - ar.aws_region !== ''&& - ar.aws_s3_bucket !== '' - ){ - if(!ar.aws_s3_dir || ar.aws_s3_dir === '/'){ - ar.aws_s3_dir = '' - } - if(ar.aws_s3_dir !== ''){ - ar.aws_s3_dir = s.checkCorrectPathEnding(ar.aws_s3_dir) - } - s.group[e.ke].aws = new require("aws-sdk") - s.group[e.ke].aws.config = new s.group[e.ke].aws.Config({ - accessKeyId: ar.aws_accessKeyId, - secretAccessKey: ar.aws_secretAccessKey, - region: ar.aws_region - }) - s.group[e.ke].aws_s3 = new s.group[e.ke].aws.S3(); - } - //discordbot - if(!s.group[e.ke].discordBot && - config.discordBot === true && - ar.discordbot === '1' && - ar.discordbot_token !== '' - ){ - s.group[e.ke].discordBot = new Discord.Client() - s.group[e.ke].discordBot.on('ready', () => { - console.log(`${r.mail} : Discord Bot Logged in as ${s.group[e.ke].discordBot.user.tag}!`) - }) - s.group[e.ke].discordBot.login(ar.discordbot_token) - } - //disk Used Emitter - if(!s.group[e.ke].diskUsedEmitter){ - s.group[e.ke].diskUsedEmitter = new events.EventEmitter() - s.group[e.ke].diskUsedEmitter.on('data',function(currentChange){ - //validate current values - if(!s.group[e.ke].usedSpace){ - s.group[e.ke].usedSpace=0 - }else{ - s.group[e.ke].usedSpace=parseFloat(s.group[e.ke].usedSpace) - } - if(s.group[e.ke].usedSpace<0||isNaN(s.group[e.ke].usedSpace)){ - s.group[e.ke].usedSpace=0 - } - //change global size value - s.group[e.ke].usedSpace=s.group[e.ke].usedSpace+currentChange - //remove value just used from queue - s.init('diskUsedEmit',e) - }) - s.group[e.ke].diskPurgedEmitter = new events.EventEmitter() - s.group[e.ke].diskPurgedEmitter.on('data',function(currentPurge){ - s.init('diskUsedSet',e,currentPurge.filesizeMB) - if(config.cron.deleteOverMax===true){ - //set queue processor - var finish=function(){ - s.init('diskUsedEmit',e) - } - var deleteVideos = function(){ - //run purge command - if(s.group[e.ke].usedSpace>(s.group[e.ke].sizeLimit*config.cron.deleteOverMaxOffset)){ - s.sqlQuery('SELECT * FROM Videos WHERE status != 0 AND details NOT LIKE \'%"archived":"1"%\' AND ke=? ORDER BY `time` ASC LIMIT 2',[e.ke],function(err,evs){ - k.del=[];k.ar=[e.ke]; - if(!evs)return console.log(err) - evs.forEach(function(ev){ - ev.dir=s.video('getDir',ev)+s.formattedTime(ev.time)+'.'+ev.ext; - k.del.push('(mid=? AND `time`=?)'); - k.ar.push(ev.mid),k.ar.push(ev.time); - s.file('delete',ev.dir); - s.init('diskUsedSet',e,-(ev.size/1000000)) - s.tx({f:'video_delete',ff:'over_max',filename:s.formattedTime(ev.time)+'.'+ev.ext,mid:ev.mid,ke:ev.ke,time:ev.time,end:s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')},'GRP_'+e.ke); - }); - if(k.del.length>0){ - k.qu=k.del.join(' OR '); - s.sqlQuery('DELETE FROM Videos WHERE ke =? AND ('+k.qu+')',k.ar,function(){ - deleteVideos() - }) - }else{ - finish() - } - }) - }else{ - finish() - } - } - deleteVideos() - }else{ - s.init('diskUsedEmit',e) - } - }) - } - Object.keys(ar).forEach(function(v){ - s.group[e.ke].init[v]=ar[v] - }) - } - }); - } - break; - case'sync': - e.cn=Object.keys(s.childNodes); - e.cn.forEach(function(v){ - if(s.group[e.ke]){ - s.cx({f:'sync',sync:s.init('noReference',s.group[e.ke].mon_conf[e.mid]),ke:e.ke,mid:e.mid},s.childNodes[v].cnid); - } - }); - break; - case'noReference': - x={keys:Object.keys(e),ar:{}}; - x.keys.forEach(function(v){ - if(v!=='last_frame'&&v!=='record'&&v!=='spawn'&&v!=='running'&&(v!=='time'&&typeof e[v]!=='function')){x.ar[v]=e[v];} - }); - return x.ar; - break; - case'url': - //build a complete url from pieces - e.authd=''; - if(e.details.muser&&e.details.muser!==''&&e.host.indexOf('@')===-1) { - e.username = e.details.muser - e.password = e.details.mpass - e.authd=e.details.muser+':'+e.details.mpass+'@'; - } - if(e.port==80&&e.details.port_force!=='1'){e.porty=''}else{e.porty=':'+e.port} - e.url=e.protocol+'://'+e.authd+e.host+e.porty+e.path;return e.url; - break; - case'url_no_path': - e.authd=''; - if(!e.details.muser){e.details.muser=''} - if(!e.details.mpass){e.details.mpass=''} - if(e.details.muser!==''&&e.host.indexOf('@')===-1) { - e.authd=e.details.muser+':'+e.details.mpass+'@'; - } - if(e.port==80&&e.details.port_force!=='1'){e.porty=''}else{e.porty=':'+e.port} - e.url=e.protocol+'://'+e.authd+e.host+e.porty;return e.url; - break; - case'diskUsedEmit': - //send the amount used disk space to connected users - if(s.group[e.ke]&&s.group[e.ke].init){ - s.tx({f:'diskUsed',size:s.group[e.ke].usedSpace,limit:s.group[e.ke].sizeLimit},'GRP_'+e.ke); - } - break; - case'diskUsedSet': - //`k` will be used as the value to add or substract - s.group[e.ke].diskUsedEmitter.emit('data',k) - break; - case'monitorStatus': -// s.discordMsg({ -// author: { -// name: s.group[e.ke].mon_conf[e.id].name, -// icon_url: config.iconURL -// }, -// title: lang['Status Changed'], -// description: lang['Monitor is now '+e.status], -// fields: [], -// timestamp: new Date(), -// footer: { -// icon_url: config.iconURL, -// text: "Shinobi Systems" -// } -// },[],e.ke) - s.group[e.ke].mon[e.id].monitorStatus = e.status - s.tx(Object.assign(e,{f:'monitor_status'}),'GRP_'+e.ke) - break; - } - if(typeof e.callback==='function'){setTimeout(function(){e.callback()},500);} -} -s.filterEvents=function(x,d){ - switch(x){ - case'archive': - d.videos.forEach(function(v,n){ - s.video('archive',v) - }) - break; - case'email': - if(d.videos&&d.videos.length>0){ - d.videos.forEach(function(v,n){ - - }) - d.mailOptions = { - from: config.mail.from, // sender address - to: d.mail, // list of receivers - subject: lang['Filter Matches']+' : '+d.name, // Subject line - html: lang.FilterMatchesText1+' '+d.videos.length+' '+lang.FilterMatchesText2, - }; - if(d.execute&&d.execute!==''){ - d.mailOptions.html+='
'+lang.Executed+' : '+d.execute+'
' - } - if(d.delete==='1'){ - d.mailOptions.html+='
'+lang.Deleted+' : '+lang.Yes+'
' - } - d.mailOptions.html+='
'+lang.Query+' : '+d.query+'
' - d.mailOptions.html+='
'+lang['Filter ID']+' : '+d.id+'
' - nodemailer.sendMail(d.mailOptions, (error, info) => { - if (error) { - s.tx({f:'error',ff:'filter_mail',ke:d.ke,error:error},'GRP_'+d.ke); - return ; - } - s.tx({f:'filter_mail',ke:d.ke,info:info},'GRP_'+d.ke); - }); - } - break; - case'delete': - d.videos.forEach(function(v,n){ - s.video('delete',v) - }) - break; - case'execute': - exec(d.execute,{detached: true}) - break; - } -} -s.video=function(x,e,k){ - if(!e){e={}}; - switch(x){ - case'getDir': - if(e.mid&&!e.id){e.id=e.mid}; - if(e.details&&(e.details instanceof Object)===false){ - try{e.details=JSON.parse(e.details)}catch(err){} - } - if(e.details&&e.details.dir&&e.details.dir!==''){ - return s.checkCorrectPathEnding(e.details.dir)+e.ke+'/'+e.id+'/' - }else{ - return s.dir.videos+e.ke+'/'+e.id+'/'; - } - break; - } - if(!k)k={}; - if(x!=='getDir'){e.dir=s.video('getDir',e)} - switch(x){ - case'fix': - e.sdir=s.dir.streams+e.ke+'/'+e.id+'/'; - if(!e.filename&&e.time){e.filename=s.formattedTime(e.time)} - if(e.filename.indexOf('.')===-1){ - e.filename=e.filename+'.'+e.ext - } - s.tx({f:'video_fix_start',mid:e.mid,ke:e.ke,filename:e.filename},'GRP_'+e.ke) - s.group[e.ke].mon[e.id].fixingVideos[e.filename]={} - switch(e.ext){ - case'mp4': - e.fixFlags='-vcodec libx264 -acodec aac -strict -2'; - break; - case'webm': - e.fixFlags='-vcodec libvpx -acodec libvorbis'; - break; - } - e.spawn=spawn(config.ffmpegDir,('-i '+e.dir+e.filename+' '+e.fixFlags+' '+e.sdir+e.filename).split(' '),{detached: true}) - e.spawn.stdout.on('data',function(data){ - s.tx({f:'video_fix_data',mid:e.mid,ke:e.ke,filename:e.filename},'GRP_'+e.ke) - }); - e.spawn.on('close',function(data){ - exec('mv '+e.dir+e.filename+' '+e.sdir+e.filename,{detached: true}).on('exit',function(){ - s.tx({f:'video_fix_success',mid:e.mid,ke:e.ke,filename:e.filename},'GRP_'+e.ke) - delete(s.group[e.ke].mon[e.id].fixingVideos[e.filename]); - }) - }); - break; - case'delete': - if(!e.filename && e.time){ - e.filename = s.formattedTime(e.time) - } - var filename, - time - if(e.filename.indexOf('.')>-1){ - filename = e.filename - }else{ - filename = e.filename+'.'+e.ext - } - if(e.filename && !e.time){ - time = s.nameToTime(filename) - }else{ - time = e.time - } - time = new Date(time) - e.save=[e.id,e.ke,time]; - s.sqlQuery('SELECT * FROM Videos WHERE `mid`=? AND `ke`=? AND `time`=?',e.save,function(err,r){ - if(r&&r[0]){ - r=r[0] - var dir=s.video('getDir',r) - s.sqlQuery('DELETE FROM Videos WHERE `mid`=? AND `ke`=? AND `time`=?',e.save,function(){ - fs.stat(dir+filename,function(err,file){ - if(err){ - s.systemLog('File Delete Error : '+e.ke+' : '+' : '+e.mid,err) - } - s.init('diskUsedSet',e,-(r.size/1000000)) - }) - s.tx({f:'video_delete',filename:filename,mid:e.mid,ke:e.ke,time:s.nameToTime(filename),end:s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')},'GRP_'+e.ke); - s.file('delete',dir+filename) - }) - }else{ -// console.log('Delete Failed',e) -// console.error(err) - } - }) - break; -// case'open': -// //on video open -// e.save=[e.id,e.ke,s.nameToTime(e.filename),e.ext]; -// if(!e.status){e.save.push(0)}else{e.save.push(e.status)} -// k.details={} -// if(e.details&&e.details.dir&&e.details.dir!==''){ -// k.details.dir=e.details.dir -// } -// e.save.push(s.s(k.details)) -// s.sqlQuery('INSERT INTO Videos (mid,ke,time,ext,status,details) VALUES (?,?,?,?,?,?)',e.save) -// s.tx({f:'video_build_start',filename:e.filename+'.'+e.ext,mid:e.id,ke:e.ke,time:s.nameToTime(e.filename),end:s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss')},'GRP_'+e.ke); -// break; -// case'close': -// //video function : close -// if(s.group[e.ke]&&s.group[e.ke].mon[e.id]){ -// if(s.group[e.ke].mon[e.id].open&&!e.filename){ -// e.filename=s.group[e.ke].mon[e.id].open; -// e.ext=s.group[e.ke].mon[e.id].open_ext -// } -// if(s.group[e.ke].mon[e.id].childNode){ -// s.cx({f:'close',d:s.init('noReference',e)},s.group[e.ke].mon[e.id].childNodeId); -// }else{ -// k.file = e.filename+'.'+e.ext -// k.dir = e.dir.toString() -// //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){ -// 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.stat = fs.statSync(k.dir+k.file) -// e.filesize = k.stat.size -// e.filesizeMB = parseFloat((e.filesize/1000000).toFixed(2)) -// e.end_time = s.formattedTime(k.stat.mtime,'YYYY-MM-DD HH:mm:ss') -// var save = [ -// e.filesize, -// 1, -// e.end_time, -// e.id, -// e.ke, -// s.nameToTime(e.filename) -// ] -// if(!e.status){ -// save.push(0) -// }else{ -// save.push(e.status) -// } -// s.sqlQuery('UPDATE Videos SET `size`=?,`status`=?,`end`=? WHERE `mid`=? AND `ke`=? AND `time`=? AND `status`=?',save) -// //send event for completed recording -// s.txWithSubPermissions({ -// f:'video_build_success', -// hrefNoAuth:'/videos/'+e.ke+'/'+e.mid+'/'+k.file, -// filename:k.file, -// mid:e.id, -// ke:e.ke, -// time:s.timeObject(s.nameToTime(e.filename)).format(), -// size:e.filesize, -// end:s.timeObject(e.end_time).format() -// },'GRP_'+e.ke,'video_view'); -// //send new diskUsage values -// s.video('diskUseUpdate',e,k) -// }else{ -// s.video('delete',e); -// s.log(e,{type:lang['File Not Exist'],msg:lang.FileNotExistText,ffmpeg:s.group[e.ke].mon[e.id].ffmpeg}) -// if(e.mode && config.restart.onVideoNotExist === true){ -// delete(s.group[e.ke].mon[e.id].open); -// s.log(e,{ -// type : lang['Camera is not recording'], -// msg : { -// msg : lang.CameraNotRecordingText -// } -// }); -// if(s.group[e.ke].mon[e.id].started===1){ -// s.camera('restart',e) -// } -// } -// } -// } -// } -// delete(s.group[e.ke].mon[e.id].open); -// break; - case'linkBuild': - //e = video rows - //k = auth key - e.forEach(function(v){ - var details = JSON.parse(v.details) - var queryString = [] - if(details.isUTC === true){ - queryString.push('isUTC=true') - }else{ - v.time = s.utcToLocal(v.time) - v.end = s.utcToLocal(v.end) - } - if(queryString.length > 0){ - queryString = '?'+queryString.join('&') - }else{ - queryString = '' - } - v.filename = s.formattedTime(v.time)+'.'+v.ext; - v.href = '/'+k+'/videos/'+v.ke+'/'+v.mid+'/'+v.filename; - v.links = { - deleteVideo : v.href+'/delete' + queryString, - changeToUnread : v.href+'/status/1' + queryString, - changeToRead : v.href+'/status/2' + queryString - } - v.href = v.href + queryString - v.details = details - }) - break; - case'diskUseUpdate'://sizePurgeQueue - if(s.group[e.ke].init){ - s.group[e.ke].diskPurgedEmitter.emit('data',k) - } - break; - case'insertCompleted': - k.dir = e.dir.toString() - if(s.group[e.ke].mon[e.id].childNode){ - s.cx({f:'insertCompleted',d:s.group[e.ke].mon_conf[e.id],k:k},s.group[e.ke].mon[e.id].childNodeId); - }else{ - //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){ - 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.stat = fs.statSync(k.dir+k.file) - k.filesize = k.stat.size - k.filesizeMB = parseFloat((k.filesize/1000000).toFixed(2)) - - k.startTime = new Date(s.nameToTime(k.file)) - k.endTime = new Date(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 - if(config.childNodes.enabled === true && config.childNodes.mode === 'child' && config.childNodes.host){ - fs.createReadStream(k.dir+k.filename) - .on('data',function(data){ - s.cx({ - f:'created_file_chunk', - mid:e.id, - ke:e.ke, - chunk:data, - filename:k.filename, - d:s.init('noReference',e), - filesize:e.filesize, - time:s.timeObject(k.startTime).format(), - end:s.timeObject(k.endTime).format() - }) - }) - .on('close',function(){ - clearTimeout(s.group[e.ke].mon[e.id].checker) - clearTimeout(s.group[e.ke].mon[e.id].checkStream) - s.cx({ - f:'created_file', - mid:e.id, - ke:e.ke, - filename:k.filename, - d:s.init('noReference',e), - filesize:k.filesize, - time:s.timeObject(k.startTime).format(), - end:s.timeObject(k.endTime).format() - }) - }); - }else{ - var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename - if(config.useUTC === true)href += '?isUTC=true'; - 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 - },'GRP_'+e.ke,'video_view'); - } - //cloud auto savers - //webdav - // var webDAV = s.group[e.ke].webdav - // if(webDAV&&s.group[e.ke].init.use_webdav!=='0'&&s.group[e.ke].init.webdav_save=="1"){ - // fs.readFile(k.dir+k.filename,function(err,data){ - // var webdavUploadDir = s.group[e.ke].init.webdav_dir+e.ke+'/'+e.mid+'/' - // fs.readFile(k.dir+k.filename,function(err,data){ - // webDAV.putFileContents(webdavUploadDir+k.filename,"binary",data).catch(function(err) { - // if(err){ - // webDAV.createDirectory(webdavUploadDir).catch(function(err) { - // s.log(e,{type:lang['Webdav Error'],msg:{msg:lang.WebdavErrorText+' /'+webdavUploadDir+'',info:err}}) - // }) - // webDAV.putFileContents(webdavUploadDir+k.filename,"binary",data).catch(function(err) { - // s.log(e,{type:lang['Webdav Error'],msg:{msg:lang.WebdavErrorText+' /'+webdavUploadDir+'',info:err}}) - // }) - // s.log(e,{type:lang['Webdav Error'],msg:{msg:lang.WebdavErrorText+' /'+webdavUploadDir+'',info:err}}) - // } - // }); - // }); - // }); - // } - if(s.group[e.ke].webdav&&s.group[e.ke].init.use_webdav!=='0'&&s.group[e.ke].init.webdav_save=='1'){ - fs.readFile(k.dir+k.filename,function(err,data){ - s.group[e.ke].webdav.putFileContents(s.group[e.ke].init.webdav_dir+e.ke+'/'+e.mid+'/'+k.filename,"binary",data) - .catch(function(err) { - s.log(e,{type:lang['Webdav Error'],msg:{msg:lang.WebdavErrorText+' /'+e.ke+'/'+e.id+'',info:err},ffmpeg:s.group[e.ke].mon[e.id].ffmpeg}) - console.error(err); - }); - }); - } - if(s.group[e.ke].aws_s3 && s.group[e.ke].init.use_aws_s3 !== '0' && s.group[e.ke].init.aws_s3_save === '1'){ - var fileStream = fs.createReadStream(k.dir+k.filename); - fileStream.on('error', function (err) { - console.error(err) - }) - s.group[e.ke].aws_s3.upload({ - Bucket: s.group[e.ke].init.aws_s3_bucket, - Key: s.group[e.ke].init.aws_s3_dir+e.ke+'/'+e.mid+'/'+k.filename, - Body:fileStream, - ACL:'public-read' - },function(err,data){ - if(err){ - s.log(e,{type:lang['Amazon S3 Upload Error'],msg:err}) - } - if(s.group[e.ke].init.aws_s3_log === '1' && data && data.Location){ - var save = [ - e.mid, - e.ke, - k.startTime, - 0, - '{}', - k.filesize, - k.endTime, - data.Location - ] - s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) - } - }) - } - k.details = {} - if(e.details&&e.details.dir&&e.details.dir!==''){ - k.details.dir = e.details.dir - } - if(config.useUTC === true)k.details.isUTC = config.useUTC; - var save = [ - e.mid, - e.ke, - k.startTime, - e.ext, - 1, - s.s(k.details), - k.filesize, - k.endTime, - ] - s.sqlQuery('INSERT INTO Videos (mid,ke,time,ext,status,details,size,end) VALUES (?,?,?,?,?,?,?,?)',save) - //send new diskUsage values - s.video('diskUseUpdate',e,k) - } - } - break; - } -} -s.splitForFFPMEG = function (ffmpegCommandAsString) { - //this function ignores spaces inside quotes. - return ffmpegCommandAsString.match(/\\?.|^$/g).reduce((p, c) => { - if(c === '"'){ - p.quote ^= 1; - }else if(!p.quote && c === ' '){ - p.a.push(''); - }else{ - p.a[p.a.length-1] += c.replace(/\\(.)/,"$1"); - } - return p; - }, {a: ['']}).a -}; -s.createFFmpegMap = function(e,arrayOfMaps){ - //`e` is the monitor object - var string = ''; - if(e.details.input_maps && e.details.input_maps.length > 0){ - if(arrayOfMaps && arrayOfMaps instanceof Array && arrayOfMaps.length>0){ - arrayOfMaps.forEach(function(v){ - if(v.map==='')v.map='0' - string += ' -map '+v.map - }) - }else{ - string += ' -map 0:0' - } - } - return string; -} -s.createInputMap = function(e,number,input){ - //`e` is the monitor object - //`x` is an object used to contain temporary values. - var x = {} - x.cust_input = '' - x.hwaccel = '' - if(input.cust_input&&input.cust_input!==''){x.cust_input+=' '+input.cust_input} - //input - analyze duration - if(input.aduration&&input.aduration!==''){x.cust_input+=' -analyzeduration '+input.aduration} - //input - probe size - if(input.probesize&&input.probesize!==''){x.cust_input+=' -probesize '+input.probesize} - //input - stream loop (good for static files/lists) - if(input.stream_loop === '1'){x.cust_input+=' -stream_loop -1'} - //input - fps - if(x.cust_input.indexOf('-r ')===-1&&input.sfps&&input.sfps!==''){ - input.sfps=parseFloat(input.sfps); - if(isNaN(input.sfps)){input.sfps=1} - x.cust_input+=' -r '+input.sfps - } - //input - is mjpeg - if(input.type==='mjpeg'){ - if(x.cust_input.indexOf('-f ')===-1){ - x.cust_input+=' -f mjpeg' - } - //input - frames per second - x.cust_input+=' -reconnect 1' - }else - //input - is h264 has rtsp in address and transport method is chosen - if((input.type==='h264'||input.type==='mp4')&&input.fulladdress.indexOf('rtsp://')>-1&&input.rtsp_transport!==''&&input.rtsp_transport!=='no'){ - x.cust_input += ' -rtsp_transport '+input.rtsp_transport - }else - if((input.type==='mp4'||input.type==='mjpeg')&&x.cust_input.indexOf('-re')===-1){ - x.cust_input += ' -re' - } - //hardware acceleration - if(input.accelerator&&input.accelerator==='1'){ - if(input.hwaccel&&input.hwaccel!==''){ - x.hwaccel+=' -hwaccel '+input.hwaccel; - } - if(input.hwaccel_vcodec&&input.hwaccel_vcodec!==''&&input.hwaccel_vcodec!=='auto'&&input.hwaccel_vcodec!=='no'){ - x.hwaccel+=' -c:v '+input.hwaccel_vcodec; - } - if(input.hwaccel_device&&input.hwaccel_device!==''){ - switch(input.hwaccel){ - case'vaapi': - x.hwaccel+=' -vaapi_device '+input.hwaccel_device+' -hwaccel_output_format vaapi'; - break; - default: - x.hwaccel+=' -hwaccel_device '+input.hwaccel_device; - break; - } - } - } - //custom - input flags - return x.hwaccel+x.cust_input+' -i "'+input.fulladdress+'"'; -} -//create sub stream channel -s.createStreamChannel = function(e,number,channel){ - //`e` is the monitor object - //`x` is an object used to contain temporary values. - var x = { - pipe:'' - } - if(!number||number==''){ - x.channel_sdir = e.sdir; - }else{ - x.channel_sdir = e.sdir+'channel'+number+'/'; - if (!fs.existsSync(x.channel_sdir)){ - fs.mkdirSync(x.channel_sdir); - } - } - x.stream_video_filters=[] - //stream - frames per second - if(channel.stream_vcodec!=='copy'){ - if(!channel.stream_fps||channel.stream_fps===''){ - switch(channel.stream_type){ - case'rtmp': - channel.stream_fps=30 - break; - default: -// channel.stream_fps=5 - break; - } - } - } - if(channel.stream_fps&&channel.stream_fps!==''){x.stream_fps=' -r '+channel.stream_fps}else{x.stream_fps=''} - - //stream - hls vcodec - if(channel.stream_vcodec&&channel.stream_vcodec!=='no'){ - if(channel.stream_vcodec!==''){x.stream_vcodec=' -c:v '+channel.stream_vcodec}else{x.stream_vcodec=' -c:v libx264'} - }else{ - x.stream_vcodec=''; - } - //stream - hls acodec - if(channel.stream_acodec!=='no'){ - if(channel.stream_acodec&&channel.stream_acodec!==''){x.stream_acodec=' -c:a '+channel.stream_acodec}else{x.stream_acodec=''} - }else{ - x.stream_acodec=' -an'; - } - //stream - resolution - if(channel.stream_scale_x&&channel.stream_scale_x!==''&&channel.stream_scale_y&&channel.stream_scale_y!==''){ - x.dimensions = channel.stream_scale_x+'x'+channel.stream_scale_y; - } - //stream - hls segment time - if(channel.hls_time&&channel.hls_time!==''){x.hls_time=channel.hls_time}else{x.hls_time="2"} - //hls list size - if(channel.hls_list_size&&channel.hls_list_size!==''){x.hls_list_size=channel.hls_list_size}else{x.hls_list_size=2} - //stream - custom flags - if(channel.cust_stream&&channel.cust_stream!==''){x.cust_stream=' '+channel.cust_stream}else{x.cust_stream=''} - //stream - preset - if(channel.stream_type !== 'h265' && channel.preset_stream && channel.preset_stream!==''){x.preset_stream=' -preset '+channel.preset_stream;}else{x.preset_stream=''} - //hardware acceleration - if(e.details.accelerator&&e.details.accelerator==='1'){ - if(e.details.hwaccel&&e.details.hwaccel!==''){ - x.hwaccel+=' -hwaccel '+e.details.hwaccel; - } - if(e.details.hwaccel_vcodec&&e.details.hwaccel_vcodec!==''){ - x.hwaccel+=' -c:v '+e.details.hwaccel_vcodec; - } - if(e.details.hwaccel_device&&e.details.hwaccel_device!==''){ - switch(e.details.hwaccel){ - case'vaapi': - x.hwaccel+=' -vaapi_device '+e.details.hwaccel_device+' -hwaccel_output_format vaapi'; - break; - default: - x.hwaccel+=' -hwaccel_device '+e.details.hwaccel_device; - break; - } - } -// else{ -// if(e.details.hwaccel==='vaapi'){ -// x.hwaccel+=' -hwaccel_device 0'; -// } -// } - } - - if(channel.rotate_stream&&channel.rotate_stream!==""&&channel.rotate_stream!=="no"){ - x.stream_video_filters.push('transpose='+channel.rotate_stream); - } - //stream - video filter - if(channel.svf&&channel.svf!==''){ - x.stream_video_filters.push(channel.svf) - } - if(x.stream_video_filters.length>0){ - var string = x.stream_video_filters.join(',').trim() - if(string===''){ - x.stream_video_filters='' - }else{ - x.stream_video_filters=' -vf '+string - } - }else{ - x.stream_video_filters='' - } - if(e.details.input_map_choices&&e.details.input_map_choices.record){ - //add input feed map - x.pipe += s.createFFmpegMap(e,e.details.input_map_choices['stream_channel-'+(number-config.pipeAddition)]) - } - if(channel.stream_vcodec !== 'copy' || channel.stream_type === 'mjpeg' || channel.stream_type === 'b64'){ - x.cust_stream += x.stream_fps - } - switch(channel.stream_type){ - case'mp4': - x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream" -reset_timestamps 1' - if(channel.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; - x.cust_stream+=x.preset_stream - x.cust_stream+=x.stream_video_filters - } - x.pipe+=' -f mp4'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; - break; - case'rtmp': - x.rtmp_server_url=s.checkCorrectPathEnding(channel.rtmp_server_url); - if(channel.stream_vcodec!=='copy'){ - if(channel.stream_vcodec==='libx264'){ - channel.stream_vcodec = 'h264' - } - if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; - x.cust_stream+=x.preset_stream - if(channel.stream_v_br&&channel.stream_v_br!==''){x.cust_stream+=' -b:v '+channel.stream_v_br} - } - if(channel.stream_vcodec!=='no'&&channel.stream_vcodec!==''){ - x.cust_stream+=' -vcodec '+channel.stream_vcodec - } - if(channel.stream_acodec!=='copy'){ - if(!channel.stream_acodec||channel.stream_acodec===''||channel.stream_acodec==='no'){ - channel.stream_acodec = 'aac' - } - if(!channel.stream_a_br||channel.stream_a_br===''){channel.stream_a_br='128k'} - x.cust_stream+=' -ab '+channel.stream_a_br - } - if(channel.stream_acodec!==''){ - x.cust_stream+=' -acodec '+channel.stream_acodec - } - x.pipe+=' -f flv'+x.stream_video_filters+x.cust_stream+' "'+x.rtmp_server_url+channel.rtmp_stream_key+'"'; - break; - case'h264': - if(channel.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; - x.cust_stream+=x.preset_stream - x.cust_stream+=x.stream_video_filters - } - x.pipe+=' -f mpegts'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; - break; - case'flv': - if(channel.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; - x.cust_stream+=x.preset_stream - x.cust_stream+=x.stream_video_filters - } - x.pipe+=' -f flv'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; - break; - case'hls': - if(channel.stream_vcodec!=='h264_vaapi'&&channel.stream_vcodec!=='copy'){ - if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; - if(x.cust_stream.indexOf('-tune')===-1){x.cust_stream+=' -tune zerolatency'} - if(x.cust_stream.indexOf('-g ')===-1){x.cust_stream+=' -g 1'} - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.cust_stream+=x.stream_video_filters - } - x.pipe+=x.preset_stream+x.stream_acodec+x.stream_vcodec+' -f hls'+x.cust_stream+' -hls_time '+x.hls_time+' -hls_list_size '+x.hls_list_size+' -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+x.channel_sdir+'s.m3u8"'; - break; - case'mjpeg': - if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -q:v '+channel.stream_quality; - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.pipe+=' -c:v mjpeg -f mpjpeg -boundary_tag shinobi'+x.cust_stream+x.stream_video_filters+' pipe:'+number; - break; - default: - x.pipe='' - break; - } - return x.pipe -} -s.ffmpegCoProcessor = function(e){ - if(e.coProcessor === false)return; - var x = {} - //x.input is the input and connection - if(e.details.loglevel&&e.details.loglevel!==''){x.loglevel='-loglevel '+e.details.loglevel;}else{x.loglevel='-loglevel error'} - x.input = x.loglevel+' -re -i '+e.sdir+'cpuOnly.m3u8' - - //x.pipe is the stream out methods - x.cust_input='' - x.cust_detect=' ' - x.stream_video_filters=[] - x.hwaccel='' - x.pipe='' - //main stream frames - //stream - timestamp - if(e.details.stream_timestamp&&e.details.stream_timestamp=="1"&&e.details.vcodec!=='copy'){ - //font - if(e.details.stream_timestamp_font&&e.details.stream_timestamp_font!==''){x.stream_timestamp_font=e.details.stream_timestamp_font}else{x.stream_timestamp_font='/usr/share/fonts/truetype/freefont/FreeSans.ttf'} - //position x - if(e.details.stream_timestamp_x&&e.details.stream_timestamp_x!==''){x.stream_timestamp_x=e.details.stream_timestamp_x}else{x.stream_timestamp_x='(w-tw)/2'} - //position y - if(e.details.stream_timestamp_y&&e.details.stream_timestamp_y!==''){x.stream_timestamp_y=e.details.stream_timestamp_y}else{x.stream_timestamp_y='0'} - //text color - if(e.details.stream_timestamp_color&&e.details.stream_timestamp_color!==''){x.stream_timestamp_color=e.details.stream_timestamp_color}else{x.stream_timestamp_color='white'} - //box color - if(e.details.stream_timestamp_box_color&&e.details.stream_timestamp_box_color!==''){x.stream_timestamp_box_color=e.details.stream_timestamp_box_color}else{x.stream_timestamp_box_color='0x00000000@1'} - //text size - if(e.details.stream_timestamp_font_size&&e.details.stream_timestamp_font_size!==''){x.stream_timestamp_font_size=e.details.stream_timestamp_font_size}else{x.stream_timestamp_font_size='10'} - - x.stream_video_filters.push('drawtext=fontfile='+x.stream_timestamp_font+':text=\'%{localtime}\':x='+x.stream_timestamp_x+':y='+x.stream_timestamp_y+':fontcolor='+x.stream_timestamp_color+':box=1:boxcolor='+x.stream_timestamp_box_color+':fontsize='+x.stream_timestamp_font_size); - } - //stream - watermark for -vf - if(e.details.stream_watermark&&e.details.stream_watermark=="1"&&e.details.stream_watermark_location&&e.details.stream_watermark_location!==''){ - switch(e.details.stream_watermark_position){ - case'tl'://top left - x.stream_watermark_position='10:10' - break; - case'tr'://top right - x.stream_watermark_position='main_w-overlay_w-10:10' - break; - case'bl'://bottom left - x.stream_watermark_position='10:main_h-overlay_h-10' - break; - default://bottom right - x.stream_watermark_position='(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' - break; - } - x.stream_video_filters.push('movie='+e.details.stream_watermark_location+'[watermark],[in][watermark]overlay='+x.stream_watermark_position+'[out]'); - } - //stream - rotation - if(e.details.rotate_stream&&e.details.rotate_stream!==""&&e.details.rotate_stream!=="no"&&e.details.stream_vcodec!=='copy'){ - x.stream_video_filters.push('transpose='+e.details.rotate_stream); - } - if(e.details.svf&&e.details.svf!==''){ - x.stream_video_filters.push(e.details.svf) - } - if(x.stream_video_filters.length>0){ - x.stream_video_filters=' -vf '+x.stream_video_filters.join(',') - }else{ - x.stream_video_filters='' - } - if(e.details.cust_stream&&e.details.cust_stream!==''){x.cust_stream=' '+e.details.cust_stream}else{x.cust_stream=''} - if(e.details.stream_fps&&e.details.stream_fps!==''){x.stream_fps=' -r '+e.details.stream_fps}else{x.stream_fps=''} - if(e.details.stream_vcodec !== 'copy' || e.details.stream_type === 'mjpeg' || e.details.stream_type === 'b64'){ - x.cust_stream += x.stream_fps - } - switch(e.details.stream_type){ - case'mjpeg': - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -q:v '+e.details.stream_quality; - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.pipe += ' -an -c:v mjpeg -f mpjpeg -boundary_tag shinobi'+x.cust_stream+x.stream_video_filters+' pipe:1'; - break; - case'b64':case'':case undefined:case null://base64 - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -q:v '+e.details.stream_quality; - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.pipe += ' -an -c:v mjpeg -f image2pipe'+x.cust_stream+x.stream_video_filters+' pipe:1'; - break; - } - //detector frames - if(e.details.detector === '1'){ - if(e.details.detector_fps && e.details.detector_fps !== ''){ - x.detector_fps = e.details.detector_fps - }else{ - x.detector_fps = '2' - } - if(e.details.detector_scale_x && e.details.detector_scale_x !== '' && e.details.detector_scale_y && e.details.detector_scale_y !== ''){ - x.dratio=' -s '+e.details.detector_scale_x+'x'+e.details.detector_scale_y - }else{ - x.dratio=' -s 320x240' - } - - if(e.details.cust_detect&&e.details.cust_detect!==''){x.cust_detect+=e.details.cust_detect;} - if(e.details.detector_pam==='1'){ - x.pipe += ' -an -c:v pam -pix_fmt gray -f image2pipe -r '+x.detector_fps+x.cust_detect+x.dratio+' pipe:3' - if(e.details.detector_use_detect_object === '1'){ - if(e.details.detector_use_motion === '1'){ - if(e.details.detector_scale_x_object && e.details.detector_scale_x_object !== '' && e.details.detector_scale_y_object && e.details.detector_scale_y_object !== ''){ - x.dratio=' -s '+e.details.detector_scale_x_object+'x'+e.details.detector_scale_y_object - } - if(e.details.detector_fps_object && e.details.detector_fps_object !== ''){ - x.detector_fps = e.details.detector_fps_object - } - } - //for object detection - x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) - x.pipe += ' -f singlejpeg -vf fps='+x.detector_fps+x.cust_detect+x.dratio+' pipe:4'; - } - }else{ - x.pipe+=' -f singlejpeg -vf fps='+x.detector_fps+x.cust_detect+x.dratio+' pipe:3'; - } - } - //snapshot frames - if(e.details.snap === '1'){ - if(!e.details.snap_fps || e.details.snap_fps === ''){e.details.snap_fps = 1} - if(e.details.snap_vf && e.details.snap_vf !== ''){x.snap_vf=' -vf '+e.details.snap_vf}else{x.snap_vf=''} - if(e.details.snap_scale_x && e.details.snap_scale_x !== '' && e.details.snap_scale_y && e.details.snap_scale_y !== ''){x.snap_ratio = ' -s '+e.details.snap_scale_x+'x'+e.details.snap_scale_y}else{x.snap_ratio=''} - if(e.details.cust_snap && e.details.cust_snap !== ''){x.cust_snap = ' '+e.details.cust_snap}else{x.cust_snap=''} - x.pipe += ' -update 1 -r '+e.details.snap_fps+x.cust_snap+x.snap_ratio+x.snap_vf+' "'+e.sdir+'s.jpg" -y'; - } - x.stdioPipes = []; - var times = config.pipeAddition; - if(e.details.stream_channels){ - times+=e.details.stream_channels.length - } - for(var i=0; i < times; i++){ - x.stdioPipes.push('pipe') - } - var commandString = x.input+x.pipe - if(commandString === x.input){ - return false - } - s.group[e.ke].mon[e.mid].coProcessorCmd = commandString - return spawn(config.ffmpegDir,s.splitForFFPMEG((commandString).replace(/\s+/g,' ').trim()),{detached: true,stdio:x.stdioPipes}) -} -s.coSpawnLauncher = function(e){ - if(s.group[e.ke].mon[e.id].started === 1 && e.coProcessor === true){ - s.coSpawnClose(e) - s.group[e.ke].mon[e.id].coSpawnProcessor = s.ffmpegCoProcessor(e) - if(s.group[e.ke].mon[e.id].coSpawnProcessor === false){ - return - } - s.log(e,{type:lang['coProcessor Started'],msg:{msg:lang.coProcessorTextStarted,cmd:s.group[e.ke].mon[e.id].coProcessorCmd}}); - s.group[e.ke].mon[e.id].coSpawnProcessorExit = function(){ - s.log(e,{type:lang['coProcess Unexpected Exit'],msg:{msg:lang['coProcess Crashed for Monitor']+' : '+e.id,cmd:s.group[e.ke].mon[e.id].coProcessorCmd}}); - setTimeout(function(){ - s.coSpawnLauncher(e) - },2000) - } - s.group[e.ke].mon[e.id].coSpawnProcessor.on('end',s.group[e.ke].mon[e.id].coSpawnProcessorExit) - s.group[e.ke].mon[e.id].coSpawnProcessor.on('exit',s.group[e.ke].mon[e.id].coSpawnProcessorExit) - var checkLog = function(d,x){return d.indexOf(x)>-1;} - s.group[e.ke].mon[e.id].coSpawnProcessor.stderr.on('data',function(d){ - d=d.toString(); - switch(true){ - case checkLog(d,'deprecated pixel format used'): - case checkLog(d,'[hls @'): - case checkLog(d,'Past duration'): - case checkLog(d,'Last message repeated'): - case checkLog(d,'pkt->duration = 0'): - case checkLog(d,'Non-monotonous DTS'): - case checkLog(d,'NULL @'): - return - break; - } - s.log(e,{type:lang.coProcessor,msg:d}); - }) - if(e.frame_to_stream){ - s.group[e.ke].mon[e.id].coSpawnProcessor.stdout.on('data',e.frame_to_stream) - } - if(e.details.detector === '1'){ - if(e.details.detector_pam === '1'){ - s.createPamDiffEngine(e) - s.group[e.ke].mon[e.id].coSpawnProcessor.stdio[3].pipe(s.group[e.ke].mon[e.id].p2p).pipe(s.group[e.ke].mon[e.id].pamDiff) - }else{ - s.group[e.ke].mon[e.id].coSpawnProcessor.stdio[3].on('data',function(d){ - s.ocvTx({f:'frame',mon:s.group[e.ke].mon_conf[e.id].details,ke:e.ke,id:e.id,time:s.formattedTime(),frame:d}); - }) - } - } - } -} -s.coSpawnClose = function(e){ - if(s.group[e.ke].mon[e.id].coSpawnProcessor){ - s.group[e.ke].mon[e.id].coSpawnProcessor.removeListener('end',s.group[e.ke].mon[e.id].coSpawnProcessorExit); - s.group[e.ke].mon[e.id].coSpawnProcessor.removeListener('exit',s.group[e.ke].mon[e.id].coSpawnProcessorExit); - s.group[e.ke].mon[e.id].coSpawnProcessor.stdin.pause() - s.group[e.ke].mon[e.id].coSpawnProcessor.kill() - delete(s.group[e.ke].mon[e.id].coSpawnProcessor) - s.log(e,{type:lang['coProcessor Stopped'],msg:{msg:lang.coProcessorTextStopped+' : '+e.id}}); - } -} -s.ffmpeg = function(e){ - e.coProcessor = false - e.isStreamer = (e.type === 'dashcam'|| e.type === 'socket') - if( - e.details.accelerator === '1' && - e.details.hwaccel !== 'vaapi' && - e.details.hwaccel_vcodec !== 'auto' && - e.isStreamer === false && - (!e.details.input_maps || e.details.input_maps.length === 0) && - (e.details.snap === '1' || e.details.stream_type === 'mjpeg' || e.details.stream_type === 'b64' || e.details.detector === '1') - ){ - e.coProcessor = true - } - //set X for temporary values so we don't break our main monitor object. - var x={tmp:''}; - //set some placeholding values to avoid "undefined" in ffmpeg string. - x.record_string='' - x.cust_input='' - x.cust_detect=' ' - x.record_video_filters=[] - x.stream_video_filters=[] - x.hwaccel='' - x.pipe='' - //input - frame rate (capture rate) - if(e.details.sfps && e.details.sfps!==''){x.input_fps=' -r '+e.details.sfps}else{x.input_fps=''} - //input - analyze duration - if(e.details.aduration&&e.details.aduration!==''){x.cust_input+=' -analyzeduration '+e.details.aduration}; - //input - probe size - if(e.details.probesize&&e.details.probesize!==''){x.cust_input+=' -probesize '+e.details.probesize}; - //input - stream loop (good for static files/lists) - if(e.details.stream_loop === '1' && (e.type === 'mp4' || e.type === 'local')){x.cust_input+=' -stream_loop -1'}; - //input - if(e.details.cust_input.indexOf('-fflags') === -1){x.cust_input+=' -fflags +igndts'} - switch(e.type){ - case'h264': - switch(e.protocol){ - case'rtsp': - if(e.details.rtsp_transport&&e.details.rtsp_transport!==''&&e.details.rtsp_transport!=='no'){x.cust_input+=' -rtsp_transport '+e.details.rtsp_transport;} - break; - } - break; - } - //record - resolution - if(e.width!==''&&e.height!==''&&!isNaN(e.width)&&!isNaN(e.height)){ - x.record_dimensions=' -s '+e.width+'x'+e.height - }else{ - x.record_dimensions='' - } - if(e.details.stream_scale_x&&e.details.stream_scale_x!==''&&e.details.stream_scale_y&&e.details.stream_scale_y!==''){ - x.dimensions = e.details.stream_scale_x+'x'+e.details.stream_scale_y; - } - //record - segmenting - x.segment=' -f segment -segment_atclocktime 1 -reset_timestamps 1 -strftime 1 -segment_list pipe:2 -segment_time '+(60*e.cutoff)+' "'+e.dir+'%Y-%m-%dT%H-%M-%S.'+e.ext+'"'; - //record - set defaults for extension, video quality - switch(e.ext){ - case'mp4': - x.vcodec='libx264';x.acodec='aac'; - if(e.details.crf&&e.details.crf!==''){x.vcodec+=' -crf '+e.details.crf} - break; - case'webm': - x.acodec='libvorbis',x.vcodec='libvpx'; - if(e.details.crf&&e.details.crf!==''){x.vcodec+=' -q:v '+e.details.crf}else{x.vcodec+=' -q:v 1';} - break; - } - if(e.details.vcodec==='h264_vaapi'){ - x.record_video_filters.push('format=nv12,hwupload'); - } - //record - use custom video codec - if(e.details.vcodec&&e.details.vcodec!==''&&e.details.vcodec!=='default'){x.vcodec=e.details.vcodec} - //record - use custom audio codec - if(e.details.acodec&&e.details.acodec!==''&&e.details.acodec!=='default'){x.acodec=e.details.acodec} - if(e.details.cust_record){ - if(x.acodec=='aac'&&e.details.cust_record.indexOf('-strict -2')===-1){e.details.cust_record+=' -strict -2';} - if(e.details.cust_record.indexOf('-threads')===-1){e.details.cust_record+=' -threads 1';} - } -// if(e.details.cust_input&&(e.details.cust_input.indexOf('-use_wallclock_as_timestamps 1')>-1)===false){e.details.cust_input+=' -use_wallclock_as_timestamps 1';} - //record - ready or reset codecs - if(x.acodec!=='no'){ - if(x.acodec.indexOf('none')>-1){x.acodec=''}else{x.acodec=' -acodec '+x.acodec} - }else{ - x.acodec=' -an' - } - if(x.vcodec.indexOf('none')>-1){x.vcodec=''}else{x.vcodec=' -vcodec '+x.vcodec} - //record - frames per second (fps) - if(e.fps&&e.fps!==''&&e.details.vcodec!=='copy'){x.record_fps=' -r '+e.fps}else{x.record_fps=''} - //stream - frames per second (fps) - if(e.details.stream_fps&&e.details.stream_fps!==''){x.stream_fps=' -r '+e.details.stream_fps}else{x.stream_fps=''} - //record - timestamp options for -vf - if(e.details.timestamp&&e.details.timestamp=="1"&&e.details.vcodec!=='copy'){ - //font - if(e.details.timestamp_font&&e.details.timestamp_font!==''){x.time_font=e.details.timestamp_font}else{x.time_font='/usr/share/fonts/truetype/freefont/FreeSans.ttf'} - //position x - if(e.details.timestamp_x&&e.details.timestamp_x!==''){x.timex=e.details.timestamp_x}else{x.timex='(w-tw)/2'} - //position y - if(e.details.timestamp_y&&e.details.timestamp_y!==''){x.timey=e.details.timestamp_y}else{x.timey='0'} - //text color - if(e.details.timestamp_color&&e.details.timestamp_color!==''){x.time_color=e.details.timestamp_color}else{x.time_color='white'} - //box color - if(e.details.timestamp_box_color&&e.details.timestamp_box_color!==''){x.time_box_color=e.details.timestamp_box_color}else{x.time_box_color='0x00000000@1'} - //text size - if(e.details.timestamp_font_size&&e.details.timestamp_font_size!==''){x.time_font_size=e.details.timestamp_font_size}else{x.time_font_size='10'} - - x.record_video_filters.push('drawtext=fontfile='+x.time_font+':text=\'%{localtime}\':x='+x.timex+':y='+x.timey+':fontcolor='+x.time_color+':box=1:boxcolor='+x.time_box_color+':fontsize='+x.time_font_size); - } - //record - watermark for -vf - if(e.details.watermark&&e.details.watermark=="1"&&e.details.watermark_location&&e.details.watermark_location!==''){ - switch(e.details.watermark_position){ - case'tl'://top left - x.watermark_position='10:10' - break; - case'tr'://top right - x.watermark_position='main_w-overlay_w-10:10' - break; - case'bl'://bottom left - x.watermark_position='10:main_h-overlay_h-10' - break; - default://bottom right - x.watermark_position='(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' - break; - } - x.record_video_filters.push('movie='+e.details.watermark_location+'[watermark],[in][watermark]overlay='+x.watermark_position+'[out]'); - } - //record - rotation - if(e.details.rotate_record&&e.details.rotate_record!==""&&e.details.rotate_record!=="no"&&e.details.stream_vcodec!=="copy"){ - x.record_video_filters.push('transpose='+e.details.rotate_record); - } - //check custom record filters for -vf - if(e.details.vf&&e.details.vf!==''){ - x.record_video_filters.push(e.details.vf) - } - //compile filter string for -vf - if(x.record_video_filters.length>0){ - x.record_video_filters=' -vf '+x.record_video_filters.join(',') - }else{ - x.record_video_filters='' - } - //stream - timestamp - if(e.details.stream_timestamp&&e.details.stream_timestamp=="1"&&e.details.vcodec!=='copy'){ - //font - if(e.details.stream_timestamp_font&&e.details.stream_timestamp_font!==''){x.stream_timestamp_font=e.details.stream_timestamp_font}else{x.stream_timestamp_font='/usr/share/fonts/truetype/freefont/FreeSans.ttf'} - //position x - if(e.details.stream_timestamp_x&&e.details.stream_timestamp_x!==''){x.stream_timestamp_x=e.details.stream_timestamp_x}else{x.stream_timestamp_x='(w-tw)/2'} - //position y - if(e.details.stream_timestamp_y&&e.details.stream_timestamp_y!==''){x.stream_timestamp_y=e.details.stream_timestamp_y}else{x.stream_timestamp_y='0'} - //text color - if(e.details.stream_timestamp_color&&e.details.stream_timestamp_color!==''){x.stream_timestamp_color=e.details.stream_timestamp_color}else{x.stream_timestamp_color='white'} - //box color - if(e.details.stream_timestamp_box_color&&e.details.stream_timestamp_box_color!==''){x.stream_timestamp_box_color=e.details.stream_timestamp_box_color}else{x.stream_timestamp_box_color='0x00000000@1'} - //text size - if(e.details.stream_timestamp_font_size&&e.details.stream_timestamp_font_size!==''){x.stream_timestamp_font_size=e.details.stream_timestamp_font_size}else{x.stream_timestamp_font_size='10'} - - x.stream_video_filters.push('drawtext=fontfile='+x.stream_timestamp_font+':text=\'%{localtime}\':x='+x.stream_timestamp_x+':y='+x.stream_timestamp_y+':fontcolor='+x.stream_timestamp_color+':box=1:boxcolor='+x.stream_timestamp_box_color+':fontsize='+x.stream_timestamp_font_size); - } - //stream - watermark for -vf - if(e.details.stream_watermark&&e.details.stream_watermark=="1"&&e.details.stream_watermark_location&&e.details.stream_watermark_location!==''){ - switch(e.details.stream_watermark_position){ - case'tl'://top left - x.stream_watermark_position='10:10' - break; - case'tr'://top right - x.stream_watermark_position='main_w-overlay_w-10:10' - break; - case'bl'://bottom left - x.stream_watermark_position='10:main_h-overlay_h-10' - break; - default://bottom right - x.stream_watermark_position='(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' - break; - } - x.stream_video_filters.push('movie='+e.details.stream_watermark_location+'[watermark],[in][watermark]overlay='+x.stream_watermark_position+'[out]'); - } - //stream - rotation - if(e.details.rotate_stream&&e.details.rotate_stream!==""&&e.details.rotate_stream!=="no"&&e.details.stream_vcodec!=='copy'){ - x.stream_video_filters.push('transpose='+e.details.rotate_stream); - } - //stream - hls vcodec - if(e.details.stream_vcodec&&e.details.stream_vcodec!=='no'){ - if(e.details.stream_vcodec!==''){x.stream_vcodec=' -c:v '+e.details.stream_vcodec}else{x.stream_vcodec=' -c:v libx264'} - }else{ - x.stream_vcodec=''; - } - //stream - hls acodec - if(e.details.stream_acodec!=='no'){ - if(e.details.stream_acodec&&e.details.stream_acodec!==''){x.stream_acodec=' -c:a '+e.details.stream_acodec}else{x.stream_acodec=''} - }else{ - x.stream_acodec=' -an'; - } - //stream - hls segment time - if(e.details.hls_time&&e.details.hls_time!==''){x.hls_time=e.details.hls_time}else{x.hls_time="2"} //hls list size - if(e.details.hls_list_size&&e.details.hls_list_size!==''){x.hls_list_size=e.details.hls_list_size}else{x.hls_list_size=2} - //stream - custom flags - if(e.details.cust_stream&&e.details.cust_stream!==''){x.cust_stream=' '+e.details.cust_stream}else{x.cust_stream=''} - //stream - preset - if(e.details.stream_type !== 'h265' && e.details.preset_stream && e.details.preset_stream !== ''){x.preset_stream=' -preset '+e.details.preset_stream;}else{x.preset_stream=''} - //hardware acceleration - if(e.details.accelerator && e.details.accelerator==='1' && e.isStreamer === false){ - if(e.details.hwaccel&&e.details.hwaccel!==''){ - x.hwaccel+=' -hwaccel '+e.details.hwaccel; - } - if(e.details.hwaccel_vcodec&&e.details.hwaccel_vcodec!==''){ - x.hwaccel+=' -c:v '+e.details.hwaccel_vcodec; - } - if(e.details.hwaccel_device&&e.details.hwaccel_device!==''){ - switch(e.details.hwaccel){ - case'vaapi': - x.hwaccel+=' -vaapi_device '+e.details.hwaccel_device; - break; - default: - x.hwaccel+=' -hwaccel_device '+e.details.hwaccel_device; - break; - } - } -// else{ -// if(e.details.hwaccel==='vaapi'){ -// x.hwaccel+=' -hwaccel_device 0'; -// } -// } - } - if(e.details.stream_vcodec==='h264_vaapi'){ - x.stream_video_filters=[] - x.stream_video_filters.push('format=nv12,hwupload'); - if(e.details.stream_scale_x&&e.details.stream_scale_x!==''&&e.details.stream_scale_y&&e.details.stream_scale_y!==''){ - x.stream_video_filters.push('scale_vaapi=w='+e.details.stream_scale_x+':h='+e.details.stream_scale_y) - } - } - //stream - video filter - if(e.details.svf&&e.details.svf!==''){ - x.stream_video_filters.push(e.details.svf) - } - if(x.stream_video_filters.length>0){ - x.stream_video_filters=' -vf '+x.stream_video_filters.join(',') - }else{ - x.stream_video_filters='' - } - //stream - pipe build - if(e.details.input_map_choices&&e.details.input_map_choices.stream){ - //add input feed map - x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.stream) - } - if(e.details.stream_vcodec !== 'copy' || e.details.stream_type === 'mjpeg' || e.details.stream_type === 'b64'){ - x.cust_stream += x.stream_fps - } - switch(e.details.stream_type){ - case'mp4': - x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream" -reset_timestamps 1' - if(e.details.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; - x.cust_stream+=x.preset_stream - x.cust_stream+=x.stream_video_filters - } - x.pipe+=' -f mp4'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:1'; - break; - case'flv': - if(e.details.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; - x.cust_stream+=x.preset_stream - x.cust_stream+=x.stream_video_filters - } - x.pipe+=' -f flv'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:1'; - break; - case'hls': - if(e.details.stream_vcodec!=='h264_vaapi'&&e.details.stream_vcodec!=='copy'){ - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; - if(x.cust_stream.indexOf('-tune')===-1){x.cust_stream+=' -tune zerolatency'} - if(x.cust_stream.indexOf('-g ')===-1){x.cust_stream+=' -g 1'} - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.cust_stream+=x.stream_video_filters - } - x.pipe+=x.preset_stream+x.stream_acodec+x.stream_vcodec+' -f hls'+x.cust_stream+' -hls_time '+x.hls_time+' -hls_list_size '+x.hls_list_size+' -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'s.m3u8"'; - break; - case'mjpeg': - if(e.coProcessor === false){ - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -q:v '+e.details.stream_quality; - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.pipe+=' -an -c:v mjpeg -f mpjpeg -boundary_tag shinobi'+x.cust_stream+x.stream_video_filters+' pipe:1'; - } - break; - case'h265': - x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1' - if(e.details.stream_vcodec!=='copy'){ - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; - x.cust_stream+=x.preset_stream - x.cust_stream+=x.stream_video_filters - } - x.pipe+=' -f hevc'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:1'; - break; - case'b64':case'':case undefined:case null://base64 - if(e.coProcessor === false){ - if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -q:v '+e.details.stream_quality; - if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} - x.pipe+=' -an -c:v mjpeg -f image2pipe'+x.cust_stream+x.stream_video_filters+' pipe:1'; - } - break; - default: - x.pipe='' - break; - } - if(e.details.stream_channels){ - e.details.stream_channels.forEach(function(v,n){ - if(v.stream_type === 'mjpeg')e.coProcessor = true; - x.pipe += s.createStreamChannel(e,n+config.pipeAddition,v) - }) - } - //detector - plugins, motion - if(e.details.detector==='1' && e.details.detector_send_frames==='1' && e.coProcessor === false){ - if(e.details.input_map_choices&&e.details.input_map_choices.detector){ - //add input feed map - x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) - } - if(!e.details.detector_fps||e.details.detector_fps===''){e.details.detector_fps=2} - if(e.details.detector_scale_x&&e.details.detector_scale_x!==''&&e.details.detector_scale_y&&e.details.detector_scale_y!==''){x.dratio=' -s '+e.details.detector_scale_x+'x'+e.details.detector_scale_y}else{x.dratio=' -s 320x240'} - if(e.details.cust_detect&&e.details.cust_detect!==''){x.cust_detect+=e.details.cust_detect;} - if(e.details.detector_pam==='1'){ - x.pipe+=' -an -c:v pam -pix_fmt gray -f image2pipe -r '+e.details.detector_fps+x.cust_detect+x.dratio+' pipe:3' - if(e.details.detector_use_detect_object === '1'){ - //for object detection - x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) - x.pipe += ' -f singlejpeg -vf fps='+e.details.detector_fps+x.cust_detect+x.dratio+' pipe:4'; - } - }else{ - x.pipe+=' -f singlejpeg -vf fps='+e.details.detector_fps+x.cust_detect+x.dratio+' pipe:3'; - } - } - //api - snapshot bin/ cgi.bin (JPEG Mode) - if(e.details.snap === '1'){ - if(e.details.input_map_choices&&e.details.input_map_choices.snap){ - //add input feed map - x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.snap) - } - if(e.coProcessor === false){ - if(!e.details.snap_fps || e.details.snap_fps === ''){e.details.snap_fps = 1} - if(e.details.snap_vf && e.details.snap_vf !== ''){x.snap_vf=' -vf '+e.details.snap_vf}else{x.snap_vf=''} - if(e.details.snap_scale_x && e.details.snap_scale_x !== '' && e.details.snap_scale_y && e.details.snap_scale_y !== ''){x.snap_ratio = ' -s '+e.details.snap_scale_x+'x'+e.details.snap_scale_y}else{x.snap_ratio=''} - if(e.details.cust_snap && e.details.cust_snap !== ''){x.cust_snap = ' '+e.details.cust_snap}else{x.cust_snap=''} - x.pipe+=' -update 1 -r '+e.details.snap_fps+x.cust_snap+x.snap_ratio+x.snap_vf+' "'+e.sdir+'s.jpg" -y'; - } - } - //Traditional Recording Buffer - if(e.details.detector=='1'&&e.details.detector_trigger=='1'&&e.details.detector_record_method==='sip'){ - if(e.details.input_map_choices&&e.details.input_map_choices.detector_sip_buffer){ - //add input feed map - x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector_sip_buffer) - } - x.detector_buffer_filters=[] - if(!e.details.detector_buffer_vcodec||e.details.detector_buffer_vcodec===''||e.details.detector_buffer_vcodec==='auto'){ - if(e.details.accelerator === '1' && e.details.hwaccel_vcodec === 'cuvid' && e.details.hwaccel_vcodec === ('h264_cuvid' || 'hevc_cuvid' || 'mjpeg_cuvid' || 'mpeg4_cuvid')){ - e.details.detector_buffer_vcodec = 'h264_nvenc' - }else{ - switch(e.type){ - case'h264':case'hls':case'mp4': - e.details.detector_buffer_vcodec = 'copy' - break; - default: - e.details.detector_buffer_vcodec = 'libx264' - break; - } - } - } - if(!e.details.detector_buffer_acodec||e.details.detector_buffer_acodec===''||e.details.detector_buffer_acodec==='auto'){ - switch(e.type){ - case'mjpeg':case'jpeg':case'socket': - e.details.detector_buffer_acodec = 'no' - break; - case'h264':case'hls':case'mp4': - e.details.detector_buffer_acodec = 'copy' - break; - default: - e.details.detector_buffer_acodec = 'aac' - break; - } - } - if(e.details.detector_buffer_acodec === 'no'){ - x.detector_buffer_acodec = ' -an' - }else{ - x.detector_buffer_acodec = ' -c:a '+e.details.detector_buffer_acodec - } - if(!e.details.detector_buffer_tune||e.details.detector_buffer_tune===''){e.details.detector_buffer_tune='zerolatency'} - if(!e.details.detector_buffer_g||e.details.detector_buffer_g===''){e.details.detector_buffer_g='1'} - if(!e.details.detector_buffer_hls_time||e.details.detector_buffer_hls_time===''){e.details.detector_buffer_hls_time='2'} - if(!e.details.detector_buffer_hls_list_size||e.details.detector_buffer_hls_list_size===''){e.details.detector_buffer_hls_list_size='4'} - if(!e.details.detector_buffer_start_number||e.details.detector_buffer_start_number===''){e.details.detector_buffer_start_number='0'} - if(!e.details.detector_buffer_live_start_index||e.details.detector_buffer_live_start_index===''){e.details.detector_buffer_live_start_index='-3'} - - if(e.details.detector_buffer_vcodec.indexOf('_vaapi')>-1){ - if(x.hwaccel.indexOf('-vaapi_device')>-1){ - x.detector_buffer_filters.push('format=nv12') - x.detector_buffer_filters.push('hwupload') - }else{ - e.details.detector_buffer_vcodec='libx264' - } - } - if(e.details.detector_buffer_vcodec!=='copy'){ - if(e.details.detector_buffer_fps&&e.details.detector_buffer_fps!==''){ - x.detector_buffer_fps=' -r '+e.details.detector_buffer_fps - }else{ - x.detector_buffer_fps=' -r 30' - } - }else{ - x.detector_buffer_fps='' - } - if(x.detector_buffer_filters.length>0){ - x.pipe+=' -vf '+x.detector_buffer_filters.join(',') - } - x.pipe+=x.detector_buffer_fps+x.detector_buffer_acodec+' -c:v '+e.details.detector_buffer_vcodec+' -f hls -tune '+e.details.detector_buffer_tune+' -g '+e.details.detector_buffer_g+' -hls_time '+e.details.detector_buffer_hls_time+' -hls_list_size '+e.details.detector_buffer_hls_list_size+' -start_number '+e.details.detector_buffer_start_number+' -live_start_index '+e.details.detector_buffer_live_start_index+' -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'detectorStream.m3u8"' - } - if(e.coProcessor === true){ - // the coProcessor ffmpeg consumes this HLS stream (no audio, frames only) - x.pipe += ' -q:v 1 -an -c:v copy -f hls -tune zerolatency -g 1 -hls_time 2 -hls_list_size 3 -start_number 0 -live_start_index 3 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'cpuOnly.m3u8"' - } - //custom - output - if(e.details.custom_output&&e.details.custom_output!==''){x.pipe+=' '+e.details.custom_output;} - //custom - input flags - if(e.details.cust_input&&e.details.cust_input!==''){x.cust_input+=' '+e.details.cust_input;} - //logging - level - if(e.details.loglevel&&e.details.loglevel!==''){x.loglevel='-loglevel '+e.details.loglevel;}else{x.loglevel='-loglevel error'} - //build record string. - if(e.mode==='record'){ - if(e.details.input_map_choices&&e.details.input_map_choices.record){ - //add input feed map - x.record_string += s.createFFmpegMap(e,e.details.input_map_choices.record) - } - //if h264, hls, mp4, or local add the audio codec flag - switch(e.type){ - case'h264':case'hls':case'mp4':case'local': - x.record_string+=x.acodec; - break; - } - //custom flags - if(e.details.cust_record&&e.details.cust_record!==''){x.record_string+=' '+e.details.cust_record;} - //preset flag - if(e.details.preset_record&&e.details.preset_record!==''){x.record_string+=' -preset '+e.details.preset_record;} - //main string write - x.record_string+=x.vcodec+x.record_fps+x.record_video_filters+x.record_dimensions+x.segment; - } - //create executeable FFMPEG command - x.ffmpegCommandString = x.loglevel+x.input_fps; - //progress pipe - x.ffmpegCommandString += ' -progress pipe:5'; - //add main input - if((e.type === 'mp4' || e.type === 'mjpeg') && x.cust_input.indexOf('-re') === -1){ - x.cust_input += ' -re' - } - switch(e.type){ - case'dashcam': - x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i -'; - break; - case'socket':case'jpeg':case'pipe': - x.ffmpegCommandString += ' -pattern_type glob -f image2pipe'+x.record_fps+' -vcodec mjpeg'+x.cust_input+x.hwaccel+' -i -'; - break; - case'mjpeg': - x.ffmpegCommandString += ' -reconnect 1 -f mjpeg'+x.cust_input+x.hwaccel+' -i "'+e.url+'"'; - break; - case'h264':case'hls':case'mp4': - x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+e.url+'"'; - break; - case'local': - x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+e.path+'"'; - break; - } - //add extra input maps - if(e.details.input_maps){ - e.details.input_maps.forEach(function(v,n){ - x.ffmpegCommandString += s.createInputMap(e,n+1,v) - }) - } - //add recording and stream outputs - x.ffmpegCommandString += x.record_string+x.pipe - //hold ffmpeg command for log stream - s.group[e.ke].mon[e.mid].ffmpeg = x.ffmpegCommandString; - //create additional pipes from ffmpeg - x.stdioPipes = []; - var times = config.pipeAddition; - if(e.details.stream_channels){ - times+=e.details.stream_channels.length - } - for(var i=0; i < times; i++){ - x.stdioPipes.push('pipe') - } - x.ffmpegCommandString = s.splitForFFPMEG(x.ffmpegCommandString.replace(/\s+/g,' ').trim()) - return spawn(config.ffmpegDir,x.ffmpegCommandString,{detached: true,stdio:x.stdioPipes}); -} -s.file=function(x,e){ - if(!e){e={}}; - switch(x){ - case'size': - return fs.statSync(e.filename)["size"]; - break; - case'delete': - if(!e){return false;} - return exec('rm -f '+e,{detached: true}); - break; - case'deleteFolder': - if(!e){return false;} - return exec('rm -rf '+e,{detached: true}); - break; - case'deleteFiles': - if(!e.age_type){e.age_type='min'};if(!e.age){e.age='1'}; - exec('find '+e.path+' -type f -c'+e.age_type+' +'+e.age+' -exec rm -f {} +',{detached: true}); - break; - } -} -s.event = function(x,e,cn){ - switch(x){ - case'trigger': - var d = e; - var filter = { - halt : false, - addToMotionCounter : true, - useLock : true, - save : true, - discord : true, - webhook : true, - command : true, - mail : true, - record : true - } - if(s.group[d.ke].mon[d.id].open){ - d.details.videoTime = s.group[d.ke].mon[d.id].open; - } - var detailString = JSON.stringify(d.details); - if(!s.group[d.ke]||!s.group[d.ke].mon[d.id]){ - return s.systemLog(lang['No Monitor Found, Ignoring Request']) - } - d.mon=s.group[d.ke].mon_conf[d.id]; - var currentConfig = s.group[d.ke].mon[d.id].details - //read filters - if( - currentConfig.use_detector_filters === '1' && - ((currentConfig.use_detector_filters_object === '1' && d.details.matrices) || - currentConfig.use_detector_filters_object !== '1') - ){ - var parseValue = function(key,val){ - var newVal - switch(val){ - case'': - newVal = filter[key] - break; - case'0': - newVal = false - break; - case'1': - newVal = true - break; - default: - newVal = val - break; - } - return newVal - } - var filters = currentConfig.detector_filters - Object.keys(filters).forEach(function(key){ - var conditionChain = {} - var dFilter = filters[key] - dFilter.where.forEach(function(condition,place){ - conditionChain[place] = {ok:false,next:condition.p4,matrixCount:0} - if(d.details.matrices)conditionChain[place].matrixCount = d.details.matrices.length - var modifyFilters = function(toCheck,matrixPosition){ - var param = toCheck[condition.p1] - var pass = function(){ - if(matrixPosition && dFilter.actions.halt === '1'){ - delete(d.details.matrices[matrixPosition]) - }else{ - conditionChain[place].ok = true - } - } - switch(condition.p2){ - case'indexOf': - if(param.indexOf(condition.p3) > -1){ - pass() - } - break; - case'!indexOf': - if(param.indexOf(condition.p3) === -1){ - pass() - } - break; - default: - var cmd = 'param '+condition.p2+' "'+condition.p3.replace(/"/g,'\\"')+'"' - if(eval('param '+condition.p2+' "'+condition.p3.replace(/"/g,'\\"')+'"')){ - pass() - } - break; - } - } - switch(condition.p1){ - case'tag': - case'x': - case'y': - case'height': - case'width': - if(d.details.matrices){ - d.details.matrices.forEach(function(matrix,position){ - modifyFilters(matrix,position) - }) - } - break; - default: - modifyFilters(d.details) - break; - } - }) - var conditionArray = Object.values(conditionChain) - var validationString = '' - conditionArray.forEach(function(condition,number){ - validationString += condition.ok+' ' - if(conditionArray.length-1 !== number){ - validationString += condition.next+' ' - } - }) - if(eval(validationString)){ - 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) - }) - }else{ - filter.halt = true - } - } - }) - if(d.details.matrices && d.details.matrices.length === 0 || filter.halt === true){ - return - }else if(d.details.matrices && d.details.matrices.length > 0){ - var reviewedMatrix = [] - d.details.matrices.forEach(function(matrix){ - if(matrix)reviewedMatrix.push(matrix) - }) - d.details.matrices = reviewedMatrix - } - } - //motion counter - if(filter.addToMotionCounter && filter.record){ - if(!s.group[d.ke].mon[d.id].detector_motion_count){ - s.group[d.ke].mon[d.id].detector_motion_count=0 - } - s.group[d.ke].mon[d.id].detector_motion_count+=1 - } - if(filter.useLock){ - if(s.group[d.ke].mon[d.id].motion_lock){ - return - } - var detector_lock_timeout - if(!currentConfig.detector_lock_timeout||currentConfig.detector_lock_timeout===''){ - detector_lock_timeout = 2000 - } - detector_lock_timeout = parseFloat(currentConfig.detector_lock_timeout); - if(!s.group[d.ke].mon[d.id].detector_lock_timeout){ - s.group[d.ke].mon[d.id].detector_lock_timeout=setTimeout(function(){ - clearTimeout(s.group[d.ke].mon[d.id].detector_lock_timeout) - delete(s.group[d.ke].mon[d.id].detector_lock_timeout) - },detector_lock_timeout) - }else{ - return - } - } - if(d.doObjectDetection !== true){ - //save this detection result in SQL, only coords. not image. - if(filter.save && currentConfig.detector_save==='1'){ - s.sqlQuery('INSERT INTO Events (ke,mid,details) VALUES (?,?,?)',[d.ke,d.id,detailString]) - } - if(currentConfig.detector_notrigger=='1'){ - var detector_notrigger_timeout - if(!currentConfig.detector_notrigger_timeout||currentConfig.detector_notrigger_timeout===''){ - detector_notrigger_timeout = 10 - } - detector_notrigger_timeout = parseFloat(currentConfig.detector_notrigger_timeout)*1000*60; - s.group[e.ke].mon[e.id].detector_notrigger_timeout = detector_notrigger_timeout; - clearInterval(s.group[d.ke].mon[d.id].detector_notrigger_timeout) - s.group[d.ke].mon[d.id].detector_notrigger_timeout = setInterval(s.group[d.ke].mon[d.id].detector_notrigger_timeout_function,detector_notrigger_timeout) - } - if(filter.webhook && currentConfig.detector_webhook=='1'){ - var detector_webhook_url = currentConfig.detector_webhook_url - .replace(/{{TIME}}/g,s.timeObject(new Date).format()) - .replace(/{{REGION_NAME}}/g,d.details.name) - .replace(/{{SNAP_PATH}}/g,s.dir.streams+'/'+d.ke+'/'+d.id+'/s.jpg') - .replace(/{{MONITOR_ID}}/g,d.id) - .replace(/{{GROUP_KEY}}/g,d.ke) - .replace(/{{DETAILS}}/g,detailString) - request({url:detector_webhook_url,method:'GET',encoding:null},function(err,data){ - if(err){ - s.log(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}}) - } - }) - } - var detector_timeout - if(!currentConfig.detector_timeout||currentConfig.detector_timeout===''){ - detector_timeout = 10 - }else{ - detector_timeout = parseFloat(currentConfig.detector_timeout) - } - if(filter.record && d.mon.mode=='start'&¤tConfig.detector_trigger==='1'&¤tConfig.detector_record_method==='sip'){ - //s.group[d.ke].mon[d.id].eventBasedRecording.timeout - // clearTimeout(s.group[d.ke].mon[d.id].eventBasedRecording.timeout) - s.group[d.ke].mon[d.id].eventBasedRecording.timeout = setTimeout(function(){ - s.group[d.ke].mon[d.id].eventBasedRecording.allowEnd=true; - },detector_timeout * 950 * 60) - if(!s.group[d.ke].mon[d.id].eventBasedRecording.process){ - if(!d.auth){ - d.auth=s.gid(); - } - if(!s.group[d.ke].users[d.auth]){ - s.group[d.ke].users[d.auth]={system:1,details:{},lang:lang} - } - s.group[d.ke].mon[d.id].eventBasedRecording.allowEnd = false; - var runRecord = function(){ - var filename = s.formattedTime()+'.mp4' - s.log(d,{type:"Traditional Recording",msg:"Started"}) - //-t 00:'+s.timeObject(new Date(detector_timeout * 1000 * 60)).format('mm:ss')+' - s.group[d.ke].mon[d.id].eventBasedRecording.process = spawn(config.ffmpegDir,s.splitForFFPMEG(('-loglevel warning -analyzeduration 1000000 -probesize 1000000 -re -i http://'+config.ip+':'+config.port+'/'+d.auth+'/hls/'+d.ke+'/'+d.id+'/detectorStream.m3u8 -t 00:'+s.timeObject(new Date(detector_timeout * 1000 * 60)).format('mm:ss')+' -c:v copy -strftime 1 "'+s.video('getDir',d.mon) + filename + '"').replace(/\s+/g,' ').trim())) - var ffmpegError=''; - var error - s.group[d.ke].mon[d.id].eventBasedRecording.process.stderr.on('data',function(data){ - s.log(d,{type:"Traditional Recording",msg:data.toString()}) - }) - s.group[d.ke].mon[d.id].eventBasedRecording.process.on('close',function(){ - if(!s.group[d.ke].mon[d.id].eventBasedRecording.allowEnd){ - s.log(d,{type:"Traditional Recording",msg:"Detector Recording Process Exited Prematurely. Restarting."}) - runRecord() - return - } - s.video('insertCompleted',d.mon,{ - file : filename - }) - s.log(d,{type:"Traditional Recording",msg:"Detector Recording Complete"}) - delete(s.group[d.ke].users[d.auth]) - s.log(d,{type:"Traditional Recording",msg:'Clear Recorder Process'}) - delete(s.group[d.ke].mon[d.id].eventBasedRecording.process) - delete(s.group[d.ke].mon[d.id].eventBasedRecording.timeout) - clearTimeout(s.group[d.ke].mon[d.id].checker) - }) - } - runRecord() - } - }else if(filter.record && d.mon.mode!=='stop'&¤tConfig.detector_trigger=='1'&¤tConfig.detector_record_method==='hot'){ - if(!d.auth){ - d.auth=s.gid(); - } - if(!s.group[d.ke].users[d.auth]){ - s.group[d.ke].users[d.auth]={system:1,details:{},lang:lang} - } - d.urlQuery=[] - d.url='http://'+config.ip+':'+config.port+'/'+d.auth+'/monitor/'+d.ke+'/'+d.id+'/record/'+detector_timeout+'/min'; - if(currentConfig.watchdog_reset!=='0'){ - d.urlQuery.push('reset=1') - } - if(currentConfig.detector_trigger_record_fps&¤tConfig.detector_trigger_record_fps!==''&¤tConfig.detector_trigger_record_fps!=='0'){ - d.urlQuery.push('fps='+currentConfig.detector_trigger_record_fps) - } - if(d.urlQuery.length>0){ - d.url+='?'+d.urlQuery.join('&') - } - http.get(d.url, function(data) { - data.setEncoding('utf8'); - var chunks=''; - data.on('data', (chunk) => { - chunks+=chunk; - }); - data.on('end', () => { - delete(s.group[d.ke].users[d.auth]) - d.cx.f='detector_record_engaged'; - d.cx.msg=JSON.parse(chunks); - s.tx(d.cx,'GRP_'+d.ke); - }); - - }).on('error', function(e) { - - }).end(); - } - var screenshotName = 'Motion_'+(d.mon.name.replace(/[^\w\s]/gi,''))+'_'+d.id+'_'+d.ke+'_'+s.formattedTime() - var screenshotBuffer = null - var detectorStreamBuffers = null - - //discord bot - if(filter.discord && currentConfig.detector_discordbot === '1' && !s.group[d.ke].mon[d.id].detector_discordbot){ - var detector_discordbot_timeout - if(!currentConfig.detector_discordbot_timeout||currentConfig.detector_discordbot_timeout===''){ - detector_discordbot_timeout = 1000*60*10; - }else{ - detector_discordbot_timeout = parseFloat(currentConfig.detector_discordbot_timeout)*1000*60; - } - //lock mailer so you don't get emailed on EVERY trigger event. - s.group[d.ke].mon[d.id].detector_discordbot=setTimeout(function(){ - //unlock so you can mail again. - clearTimeout(s.group[d.ke].mon[d.id].detector_discordbot); - delete(s.group[d.ke].mon[d.id].detector_discordbot); - },detector_discordbot_timeout); - var files = [] - var sendAlert = function(){ - s.discordMsg({ - author: { - name: s.group[d.ke].mon_conf[d.id].name, - icon_url: config.iconURL - }, - title: lang.Event+' - '+screenshotName, - description: lang.EventText1+' '+s.timeObject(new Date).format(), - fields: [], - timestamp: new Date(), - footer: { - icon_url: config.iconURL, - text: "Shinobi Systems" - } - },files,d.ke) - } - if(currentConfig.detector_discordbot_send_video === '1'){ - if(!detectorStreamBuffers){ - detectorStreamBuffers = s.getDetectorStreams(d) - } - detectorStreamBuffers.slice(detectorStreamBuffers.length - 2,detectorStreamBuffers.length).forEach(function(filepath,n){ - files.push({ - attachment: filepath, - name: 'Video Clip '+n+'.ts' - }) - }) - } - if(screenshotBuffer){ - sendAlert() - }else if(currentConfig.snap === '1'){ - fs.readFile(s.dir.streams+'/'+d.ke+'/'+d.id+'/s.jpg',function(err, frame){ - if(err){ - s.systemLog(lang.EventText2+' '+d.ke+' '+d.id,err) - }else{ - screenshotBuffer = frame - files.push({ - attachment: screenshotBuffer, - name: screenshotName+'.jpg' - }) - } - sendAlert() - }) - }else{ - sendAlert() - } - } - //mailer - if(filter.mail && config.mail && !s.group[d.ke].mon[d.id].detector_mail && currentConfig.detector_mail === '1'){ - s.sqlQuery('SELECT mail FROM Users WHERE ke=? AND details NOT LIKE ?',[d.ke,'%"sub"%'],function(err,r){ - r=r[0]; - var detector_mail_timeout - if(!currentConfig.detector_mail_timeout||currentConfig.detector_mail_timeout===''){ - detector_mail_timeout = 1000*60*10; - }else{ - detector_mail_timeout = parseFloat(currentConfig.detector_mail_timeout)*1000*60; - } - //lock mailer so you don't get emailed on EVERY trigger event. - s.group[d.ke].mon[d.id].detector_mail=setTimeout(function(){ - //unlock so you can mail again. - clearTimeout(s.group[d.ke].mon[d.id].detector_mail); - delete(s.group[d.ke].mon[d.id].detector_mail); - },detector_mail_timeout); - var files = [] - var mailOptions = { - from: config.mail.from, // sender address - to: r.mail, // list of receivers - subject: lang.Event+' - '+screenshotName, // Subject line - html: ''+lang.EventText1+' '+s.timeObject(new Date).format()+'.', - attachments: files - } - var sendMail = function(){ - Object.keys(d.details).forEach(function(v,n){ - mailOptions.html+='
'+v+' : '+d.details[v]+'
' - }) - nodemailer.sendMail(mailOptions, (error, info) => { - if (error) { - s.systemLog(lang.MailError,error) - return false; - } - }) - } - if(currentConfig.detector_mail_send_video === '1'){ - if(!detectorStreamBuffers){ - detectorStreamBuffers = s.getDetectorStreams(d) - } - detectorStreamBuffers.slice(detectorStreamBuffers.length - 2,detectorStreamBuffers.length).forEach(function(filepath,n){ - files.push({ - filename: 'Video Clip '+n+'.ts', - content: fs.readFileSync(filepath) - }) - }) - } - if(screenshotBuffer){ - sendMail() - }else if(currentConfig.snap === '1'){ - fs.readFile(s.dir.streams+'/'+d.ke+'/'+d.id+'/s.jpg',function(err, frame){ - if(err){ - s.systemLog(lang.EventText2+' '+d.ke+' '+d.id,err) - }else{ - screenshotBuffer = frame - files.push({ - filename: screenshotName+'.jpg', - content: frame - }) - } - sendMail() - }) - }else{ - sendMail() - } - }); - } - if(filter.command && currentConfig.detector_command_enable==='1'&&!s.group[d.ke].mon[d.id].detector_command){ - var detector_command_timeout - if(!currentConfig.detector_command_timeout||currentConfig.detector_command_timeout===''){ - detector_command_timeout = 1000*60*10; - }else{ - detector_command_timeout = parseFloat(currentConfig.detector_command_timeout)*1000*60; - } - s.group[d.ke].mon[d.id].detector_command=setTimeout(function(){ - clearTimeout(s.group[d.ke].mon[d.id].detector_command); - delete(s.group[d.ke].mon[d.id].detector_command); - - },detector_command_timeout); - var detector_command = currentConfig.detector_command - .replace(/{{TIME}}/g,s.timeObject(new Date).format()) - .replace(/{{REGION_NAME}}/g,d.details.name) - .replace(/{{SNAP_PATH}}/g,s.dir.streams+'/'+d.ke+'/'+d.id+'/s.jpg') - .replace(/{{MONITOR_ID}}/g,d.id) - .replace(/{{GROUP_KEY}}/g,d.ke) - .replace(/{{DETAILS}}/g,detailString) - if(d.details.confidence){ - detector_command = detector_command - .replace(/{{CONFIDENCE}}/g,d.details.confidence) - } - exec(detector_command,{detached: true}) - } - } - //show client machines the event - d.cx={f:'detector_trigger',id:d.id,ke:d.ke,details:d.details,doObjectDetection:d.doObjectDetection}; - s.tx(d.cx,'DETECTOR_'+d.ke+d.id); - break; - } - if(typeof cn==='function'){setTimeout(function(){cn()},1000);} -} -s.camera=function(x,e,cn,tx){ - if(x!=='motion'){ - var ee=s.init('noReference',e); - if(!e){e={}};if(cn&&cn.ke&&!e.ke){e.ke=cn.ke}; - if(!e.mode){e.mode=x;} - if(!e.id&&e.mid){e.id=e.mid} - } - if(e.details&&(e.details instanceof Object)===false){ - try{e.details=JSON.parse(e.details)}catch(err){} - } - //parse Objects - (['detector_cascades','cords','detector_filters','input_map_choices']).forEach(function(v){ - if(e.details&&e.details[v]&&(e.details[v] instanceof Object)===false){ - try{ - if(e.details[v] === '') e.details[v] = '{}' - e.details[v]=JSON.parse(e.details[v]); - if(!e.details[v])e.details[v]={}; - s.group[e.ke].mon[e.id].details = e.details; - }catch(err){ - - } - } - }); - //parse Arrays - (['stream_channels','input_maps']).forEach(function(v){ - if(e.details&&e.details[v]&&(e.details[v] instanceof Array)===false){ - try{ - e.details[v]=JSON.parse(e.details[v]); - if(!e.details[v])e.details[v]=[]; - }catch(err){ - e.details[v]=[]; - } - } - }); - s.init(0,{ke:e.ke,mid:e.id}) - switch(x){ - case'buildOptionsFromUrl': - var monitorConfig = cn - URLobject=URL.parse(e) - if(monitorConfig.details.control_url_method === 'ONVIF' && monitorConfig.details.control_base_url === ''){ - if(monitorConfig.details.onvif_port === ''){ - monitorConfig.details.onvif_port = 8000 - } - URLobject.port = monitorConfig.details.onvif_port - }else if(!URLobject.port){ - URLobject.port = 80 - } - options = { - host: URLobject.hostname, - port: URLobject.port, - method: monitorConfig.details.control_url_method, - path: URLobject.pathname, - }; - if(URLobject.query){ - options.path=options.path+'?'+URLobject.query - } - if(URLobject.username&&URLobject.password){ - options.username = URLobject.username - options.password = URLobject.password - options.auth=URLobject.username+':'+URLobject.password - }else if(URLobject.auth){ - var auth = URLobject.auth.split(':') - options.auth=URLobject.auth - options.username = auth[0] - options.password = auth[1] - } - return options - break; - case'control': - if(!s.group[e.ke]||!s.group[e.ke].mon[e.id]){return} - var monitorConfig = s.group[e.ke].mon_conf[e.id]; - if(monitorConfig.details.control!=="1"){s.log(e,{type:lang['Control Error'],msg:lang.ControlErrorText1});return} - if(!monitorConfig.details.control_base_url||monitorConfig.details.control_base_url===''){ - e.base=s.init('url_no_path',monitorConfig); - }else{ - e.base=monitorConfig.details.control_base_url; - } - if(!monitorConfig.details.control_url_stop_timeout || monitorConfig.details.control_url_stop_timeout === ''){ - monitorConfig.details.control_url_stop_timeout = 1000 - } - if(!monitorConfig.details.control_url_method||monitorConfig.details.control_url_method===''){monitorConfig.details.control_url_method="GET"} - var controlURL = e.base+monitorConfig.details['control_url_'+e.direction] - var controlURLOptions = s.camera('buildOptionsFromUrl',controlURL,monitorConfig) - if(monitorConfig.details.control_url_stop_timeout === '0' && monitorConfig.details.control_stop === '1' && s.group[e.ke].mon[e.id].ptzMoving === true){ - e.direction = 'stopMove' - s.group[e.ke].mon[e.id].ptzMoving = false - }else{ - s.group[e.ke].mon[e.id].ptzMoving = true - } - if(monitorConfig.details.control_url_method === 'ONVIF'){ - try{ - var move = function(device){ - var stopOptions = {ProfileToken : device.current_profile.token,'PanTilt': true,'Zoom': true} - switch(e.direction){ - case'center': -// device.services.ptz.gotoHomePosition() - msg = {type:'Center button inactive'} - s.log(e,msg) - cn(msg) - break; - case'stopMove': - msg = {type:'Control Trigger Ended'} - s.log(e,msg) - cn(msg) - device.services.ptz.stop(stopOptions).then((result) => { -// console.log(JSON.stringify(result['data'], null, ' ')); - }).catch((error) => { -// console.error(error); - }); - break; - default: - var controlOptions = { - ProfileToken : device.current_profile.token, - Velocity : {} - } - var onvifDirections = { - "left" : [-1.0,'x'], - "right" : [1.0,'x'], - "down" : [-1.0,'y'], - "up" : [1.0,'y'], - "zoom_in" : [1.0,'zoom'], - "zoom_out" : [-1.0,'zoom'] - } - var direction = onvifDirections[e.direction] - controlOptions.Velocity[direction[1]] = direction[0]; - (['x','y','z']).forEach(function(axis){ - if(!controlOptions.Velocity[axis]) - controlOptions.Velocity[axis] = 0 - }) - if(monitorConfig.details.control_stop=='1'){ - device.services.ptz.continuousMove(controlOptions).then(function(err){ - s.log(e,{type:'Control Trigger Started'}); - if(monitorConfig.details.control_url_stop_timeout !== '0'){ - setTimeout(function(){ - msg = {type:'Control Trigger Ended'} - s.log(e,msg) - cn(msg) - device.services.ptz.stop(stopOptions).then((result) => { -// console.log(JSON.stringify(result['data'], null, ' ')); - }).catch((error) => { - console.log(error); - }); - },monitorConfig.details.control_url_stop_timeout) - } - }).catch(function(err){ - console.log(err) - }); - }else{ - device.services.ptz.absoluteMove(controlOptions).then(function(err){ - msg = {type:'Control Triggered'} - s.log(e,msg); - cn(msg) - }).catch(function(err){ - console.log(err) - }); - } - break; - } - } - //create onvif connection - if(!s.group[e.ke].mon[e.id].onvifConnection){ - s.group[e.ke].mon[e.id].onvifConnection = new onvif.OnvifDevice({ - xaddr : 'http://' + controlURLOptions.host + ':' + controlURLOptions.port + '/onvif/device_service', - user : controlURLOptions.username, - pass : controlURLOptions.password - }) - s.group[e.ke].mon[e.id].onvifConnection.init().then((info) => { - move(s.group[e.ke].mon[e.id].onvifConnection) - }).catch(function(error){ - console.log(error) - s.log(e,{type:lang['Control Error'],msg:error}) - }) - }else{ - move(s.group[e.ke].mon[e.id].onvifConnection) - } - }catch(err){ - console.log(err) - msg = {type:lang['Control Error'],msg:{msg:lang.ControlErrorText2,error:err,options:controlURLOptions,direction:e.direction}} - s.log(e,msg) - cn(msg) - } - }else{ - var stopCamera = function(){ - var stopURL = e.base+monitorConfig.details['control_url_'+e.direction+'_stop'] - var options = s.camera('buildOptionsFromUrl',stopURL,monitorConfig) - var requestOptions = { - url : stopURL, - method : options.method, - auth : { - user : options.username, - pass : options.password - } - } - if(monitorConfig.details.control_digest_auth === '1'){ - requestOptions.sendImmediately = true - } - request(requestOptions,function(err,data){ - if(err){ - msg = {ok:false,type:'Control Error',msg:err} - }else{ - msg = {ok:true,type:'Control Trigger Ended'} - } - cn(msg) - s.log(e,msg); - }) - } - if(e.direction === 'stopMove'){ - stopCamera() - }else{ - var requestOptions = { - url : controlURL, - method : controlURLOptions.method, - auth : { - user : controlURLOptions.username, - pass : controlURLOptions.password - } - } - if(monitorConfig.details.control_digest_auth === '1'){ - requestOptions.sendImmediately = true - } - request(requestOptions,function(err,data){ - if(err){ - msg = {ok:false,type:'Control Error',msg:err}; - cn(msg) - s.log(e,msg); - return - } - if(monitorConfig.details.control_stop=='1'&&e.direction!=='center'){ - s.log(e,{type:'Control Triggered Started'}); - if(monitorConfig.details.control_url_stop_timeout > 0){ - setTimeout(function(){ - stopCamera() - },monitorConfig.details.control_url_stop_timeout) - } - }else{ - msg = {ok:true,type:'Control Triggered'}; - cn(msg) - s.log(e,msg); - } - }) - } - } - break; - case'snapshot'://get snapshot from monitor URL - if(config.doSnapshot===true){ - if(e.mon.mode!=='stop'){ - if(e.mon.details.snap==='1'){ - fs.readFile(s.dir.streams+e.ke+'/'+e.mid+'/s.jpg',function(err,data){ - if(err){s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke);return}; - s.tx({f:'monitor_snapshot',snapshot:data,snapshot_format:'ab',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - }) - }else{ - e.url=s.init('url',e.mon); - switch(e.mon.type){ - case'mjpeg':case'h264':case'local': - if(e.mon.type==='local'){e.url=e.mon.path;} - e.spawn=spawn(config.ffmpegDir,('-loglevel quiet -i '+e.url+' -s 400x400 -r 25 -ss 1.8 -frames:v 1 -f singlejpeg pipe:1').split(' '),{detached: true}) - e.spawn.stdout.on('data',function(data){ - e.snapshot_sent=true; s.tx({f:'monitor_snapshot',snapshot:data.toString('base64'),snapshot_format:'b64',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - e.spawn.kill(); - }); - e.spawn.on('close',function(data){ - if(!e.snapshot_sent){ - s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - } - delete(e.snapshot_sent); - }); - break; - case'jpeg': - request({url:e.url,method:'GET',encoding:null},function(err,data){ - if(err){s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke);return}; - s.tx({f:'monitor_snapshot',snapshot:data.body,snapshot_format:'ab',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - }) - break; - default: - s.tx({f:'monitor_snapshot',snapshot:'...',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - break; - } - } - }else{ - s.tx({f:'monitor_snapshot',snapshot:'Disabled',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - } - }else{ - s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) - } - break; - case'record_off'://stop recording and start - if(!s.group[e.ke].mon[e.id].record){s.group[e.ke].mon[e.id].record={}} - s.group[e.ke].mon[e.id].record.yes=0; - s.camera('start',e); - break; - case'watch_on'://live streamers - join -// if(s.group[e.ke].mon[e.id].watch[cn.id]){s.camera('watch_off',e,cn,tx);return} - if(!cn.monitor_watching){cn.monitor_watching={}} - if(!cn.monitor_watching[e.id]){cn.monitor_watching[e.id]={ke:e.ke}} - s.group[e.ke].mon[e.id].watch[cn.id]={}; -// if(Object.keys(s.group[e.ke].mon[e.id].watch).length>0){ -// s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[e.ke,e.id],function(err,r) { -// if(r&&r[0]){ -// r=r[0]; -// r.url=s.init('url',r); -// s.group[e.ke].mon.type=r.type; -// } -// }) -// } - break; - case'watch_off'://live streamers - leave - if(cn.monitor_watching){delete(cn.monitor_watching[e.id])} - if(s.group[e.ke].mon[e.id]&&s.group[e.ke].mon[e.id].watch){ - delete(s.group[e.ke].mon[e.id].watch[cn.id]),e.ob=Object.keys(s.group[e.ke].mon[e.id].watch).length - if(e.ob===0){ - delete(s.group[e.ke].mon[e.id].watch) - } - }else{ - e.ob=0; - } - if(tx){tx({f:'monitor_watch_off',ke:e.ke,id:e.id,cnid:cn.id})}; - s.tx({viewers:e.ob,ke:e.ke,id:e.id},'MON_'+e.id); - break; - case'restart'://restart monitor - s.init('monitorStatus',{id:e.id,ke:e.ke,status:'Restarting'}); - s.camera('stop',e) - setTimeout(function(){ - s.camera(e.mode,e) - },1300) - break; - case'idle':case'stop'://stop monitor - if(!s.group[e.ke]||!s.group[e.ke].mon[e.id]){return} - if(config.childNodes.enabled === true && config.childNodes.mode === 'master' && s.group[e.ke].mon[e.id].childNode && s.childNodes[s.group[e.ke].mon[e.id].childNode].activeCameras[e.ke+e.id]){ - s.group[e.ke].mon[e.id].started = 0 - s.cx({ - //function - f : 'cameraStop', - //data, options - d : s.group[e.ke].mon_conf[e.id] - },s.group[e.ke].mon[e.id].childNodeId) - s.cx({f:'sync',sync:s.group[e.ke].mon_conf[e.id],ke:e.ke,mid:e.id},s.group[e.ke].mon[e.id].childNodeId); - }else{ - if(s.group[e.ke].mon[e.id].eventBasedRecording.process){ - clearTimeout(s.group[e.ke].mon[e.id].eventBasedRecording.timeout) - s.group[e.ke].mon[e.id].eventBasedRecording.allowEnd=true; - s.group[e.ke].mon[e.id].eventBasedRecording.process.kill('SIGTERM'); - } - if(s.group[e.ke].mon[e.id].fswatch){s.group[e.ke].mon[e.id].fswatch.close();delete(s.group[e.ke].mon[e.id].fswatch)} - if(s.group[e.ke].mon[e.id].fswatchStream){s.group[e.ke].mon[e.id].fswatchStream.close();delete(s.group[e.ke].mon[e.id].fswatchStream)} - if(s.group[e.ke].mon[e.id].last_frame){delete(s.group[e.ke].mon[e.id].last_frame)} - if(s.group[e.ke].mon[e.id].started!==1){return} - s.kill(s.group[e.ke].mon[e.id].spawn,e); - if(e.neglectTriggerTimer===1){ - delete(e.neglectTriggerTimer); - }else{ - clearTimeout(s.group[e.ke].mon[e.id].trigger_timer) - delete(s.group[e.ke].mon[e.id].trigger_timer) - } - clearInterval(s.group[e.ke].mon[e.id].running); - clearInterval(s.group[e.ke].mon[e.id].detector_notrigger_timeout) - clearTimeout(s.group[e.ke].mon[e.id].err_fatal_timeout); - s.group[e.ke].mon[e.id].started=0; - if(s.group[e.ke].mon[e.id].record){s.group[e.ke].mon[e.id].record.yes=0} - s.tx({f:'monitor_stopping',mid:e.id,ke:e.ke,time:s.formattedTime()},'GRP_'+e.ke); - s.camera('snapshot',{mid:e.id,ke:e.ke,mon:e}) - if(x==='stop'){ - s.log(e,{type:lang['Monitor Stopped'],msg:lang.MonitorStoppedText}); - clearTimeout(s.group[e.ke].mon[e.id].delete) - if(e.delete===1){ - s.group[e.ke].mon[e.id].delete=setTimeout(function(){ - delete(s.group[e.ke].mon[e.id]); - delete(s.group[e.ke].mon_conf[e.id]); - },1000*60); - } - }else{ - s.tx({f:'monitor_idle',mid:e.id,ke:e.ke,time:s.formattedTime()},'GRP_'+e.ke); - s.log(e,{type:lang['Monitor Idling'],msg:lang.MonitorIdlingText}); - } - } - var wantedStatus = lang.Stopped - if(x==='idle'){ - var wantedStatus = lang.Idle - } - s.init('monitorStatus',{id:e.id,ke:e.ke,status:wantedStatus}); - break; - case'start':case'record'://watch or record monitor url - s.init(0,{ke:e.ke,mid:e.id}) - if(!s.group[e.ke].mon_conf[e.id]){s.group[e.ke].mon_conf[e.id]=s.init('noReference',e);} - e.url = s.init('url',e); - if(s.group[e.ke].mon[e.id].started===1){ - //stop action, monitor already started or recording - return - } - //lock this function - s.init('monitorStatus',{id:e.id,ke:e.ke,status:lang.Starting}); - s.group[e.ke].mon[e.id].started = 1; - //create host string without username and password - e.hosty = e.host.split('@'); - if(e.hosty[1]){ - //username and password found - e.hosty = e.hosty[1] - }else{ - //no username or password in `host` string - e.hosty = e.hosty[0] - } - //set recording status - var wantedStatus = lang.Watching - if(x==='record'){ - var wantedStatus = lang.Recording - s.group[e.ke].mon[e.id].record.yes=1; - }else{ - s.group[e.ke].mon[e.mid].record.yes=0; - } - //set the recording directory - if(e.details && e.details.dir && e.details.dir !== '' && config.childNodes.mode !== 'child'){ - //addStorage choice - e.dir=s.checkCorrectPathEnding(e.details.dir)+e.ke+'/'; - if (!fs.existsSync(e.dir)){ - fs.mkdirSync(e.dir); - } - e.dir=e.dir+e.id+'/'; - if (!fs.existsSync(e.dir)){ - fs.mkdirSync(e.dir); - } - }else{ - //MAIN videos dir - e.dir=s.dir.videos+e.ke+'/'; - if (!fs.existsSync(e.dir)){ - fs.mkdirSync(e.dir); - } - e.dir=s.dir.videos+e.ke+'/'+e.id+'/'; - if (!fs.existsSync(e.dir)){ - fs.mkdirSync(e.dir); - } - } - //set the temporary files directory - var setStreamDir = function(){ - //stream dir - e.sdir=s.dir.streams+e.ke+'/'; - if (!fs.existsSync(e.sdir)){ - fs.mkdirSync(e.sdir); - } - e.sdir=s.dir.streams+e.ke+'/'+e.id+'/'; - if (!fs.existsSync(e.sdir)){ - fs.mkdirSync(e.sdir); - }else{ - s.file('deleteFolder',e.sdir+'*') - } - } - setStreamDir() - //try to create HawkEye Onvif Object - if(e.details.is_onvif === '1'){ - console.log('onvifHawk',e.hosty, e.porty.replace(':',''), e.username, e.password) - var doOnvifHawk = true - var errorCount = 0 - var hawkFail = function(msg,callback){ - ++errorCount - if(errorCount > 2){ - callback() - s.log(e,msg); - } - } - var createHawkOnvif = function(){ - if(doOnvifHawk === false){ - return false - } - if(!e.details.onvif_port || e.details.onvif_port === ''){ - e.details.onvif_port = 8000 - } - onvifHawk.connect(e.hosty, e.details.onvif_port, e.username, e.password).then(function(results){ - var camera = results - // if the camera supports events, the module will already be loaded. - if (camera.events) { - camera.events.soap.username = e.username - camera.events.soap.password = e.password - camera.events.on('messages', messages => { - console.log('Messages Received:', (typeof messages)) - }) - camera.events.on('messages:error', error => { - if(error.body.indexOf('anonymous') > -1){ - hawkFail({type:lang.ONVIFEventsNotAvailable,msg:{msg:lang.ONVIFnotCompliantProfileT}},function(){ - camera.events.stopPull() - }) - } - }) - // start a pull event loop - setTimeout(function(){ - camera.events.startPull() - },3000) - // call stopPull() to end the event loop - // camera.events.stopPull() - s.group[e.ke].mon[e.id].HawkEyeOnvifConnection = camera - } - if(s.group[e.ke].mon[e.id].HawkEyeOnvifConnection){ - console.log('Found') - }else{ - console.log('Not Found') - } - }).catch(function(err){ - console.log('Error Connecting') - console.log(err.code) - hawkFail({type:lang.ONVIFEventsNotAvailable,msg:{msg:lang.ONVIFnotCompliantProfileT}},function(){ - doOnvifHawk = false - }) - setTimeout(function(){ - createHawkOnvif() - },3000) - }) - } - createHawkOnvif() - } - //set up fatal error handler - if(e.details.fatal_max===''){ - e.details.fatal_max = 10 - }else{ - e.details.fatal_max = parseFloat(e.details.fatal_max) - } - var errorFatal = function(errorMessage){ - s.debugLog(errorMessage) - clearTimeout(s.group[e.ke].mon[e.id].err_fatal_timeout); - ++errorFatalCount; - if(s.group[e.ke].mon[e.id].started===1){ - s.group[e.ke].mon[e.id].err_fatal_timeout=setTimeout(function(){ - if(e.details.fatal_max!==0&&errorFatalCount>e.details.fatal_max){ - s.camera('stop',{id:e.id,ke:e.ke}) - }else{ - launchMonitorProcesses() - }; - },5000); - }else{ - s.kill(s.group[e.ke].mon[e.id].spawn,e); - } - s.init('monitorStatus',{id:e.id,ke:e.ke,status:lang.Died}); - } - var errorFatalCount = 0; - //cutoff time and recording check interval - if(!e.details.cutoff||e.details.cutoff===''){e.cutoff=15}else{e.cutoff=parseFloat(e.details.cutoff)}; - if(isNaN(e.cutoff)===true){e.cutoff=15} - //set master based process launcher - var launchMonitorProcesses = function(){ - s.group[e.ke].mon[e.id].allowStdinWrite = false - s.txToDashcamUsers({ - f : 'disable_stream', - ke : e.ke, - mid : e.id - },e.ke) - if(e.details.detector_trigger=='1'){ - s.group[e.ke].mon[e.id].motion_lock=setTimeout(function(){ - clearTimeout(s.group[e.ke].mon[e.id].motion_lock); - delete(s.group[e.ke].mon[e.id].motion_lock); - },15000) - } - //start "no motion" checker - if(e.details.detector=='1'&&e.details.detector_notrigger=='1'){ - if(!e.details.detector_notrigger_timeout||e.details.detector_notrigger_timeout===''){ - e.details.detector_notrigger_timeout=10 - } - e.detector_notrigger_timeout=parseFloat(e.details.detector_notrigger_timeout)*1000*60; - s.sqlQuery('SELECT mail FROM Users WHERE ke=? AND details NOT LIKE ?',[e.ke,'%"sub"%'],function(err,r){ - r=r[0]; - s.group[e.ke].mon[e.id].detector_notrigger_timeout_function=function(){ - if(config.mail&&e.details.detector_notrigger_mail=='1'){ - e.mailOptions = { - from: config.mail.from, // sender address - to: r.mail, // list of receivers - subject: lang.NoMotionEmailText1+' '+e.name+' ('+e.id+')', // Subject line - html: ''+lang.NoMotionEmailText2+' '+e.details.detector_notrigger_timeout+' '+lang.minutes+'.', - }; - e.mailOptions.html+='
'+lang['Monitor Name']+' : '+e.name+'
' - e.mailOptions.html+='
'+lang['Monitor ID']+' : '+e.id+'
' - nodemailer.sendMail(e.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); - }); - } - } - clearInterval(s.group[e.ke].mon[e.id].detector_notrigger_timeout) - s.group[e.ke].mon[e.id].detector_notrigger_timeout=setInterval(s.group[e.ke].mon[e.id].detector_notrigger_timeout_function,s.group[e.ke].mon[e.id].detector_notrigger_timeout) - }) - } - var resetRecordingCheck = function(){ - clearTimeout(s.group[e.ke].mon[e.id].checker) - var cutoff = e.cutoff + 0 - if(e.type === 'dashcam'){ - cutoff *= 100 - } - s.group[e.ke].mon[e.id].checker=setTimeout(function(){ - if(s.group[e.ke].mon[e.id].started === 1 && s.group[e.ke].mon_conf[e.id].mode === 'record'){ - launchMonitorProcesses(); - s.init('monitorStatus',{id:e.id,ke:e.ke,status:lang.Restarting}); - s.log(e,{type:lang['Camera is not recording'],msg:{msg:lang['Restarting Process']}}); - } - },60000 * cutoff * 1.1); - } - var resetStreamCheck=function(){ - clearTimeout(s.group[e.ke].mon[e.id].checkStream) - s.group[e.ke].mon[e.id].checkStream = setTimeout(function(){ - if(s.group[e.ke].mon[e.id].started===1){ - launchMonitorProcesses(); - s.log(e,{type:lang['Camera is not streaming'],msg:{msg:lang['Restarting Process']}}); - } - },60000*1); - } - if(e.details.snap === '1'){ - var resetSnapCheck = function(){ - clearTimeout(s.group[e.ke].mon[e.id].checkSnap) - s.group[e.ke].mon[e.id].checkSnap = setTimeout(function(){ - if(s.group[e.ke].mon[e.id].started === 1){ - fs.stat(e.sdir+'s.jpg',function(err,snap){ - var notStreaming = function(){ - if(e.coProcessor === true){ - s.coSpawnLauncher(e) - }else{ - launchMonitorProcesses() - } - s.log(e,{type:lang['Camera is not streaming'],msg:{msg:lang['Restarting Process']}}); - } - if(err){ - notStreaming() - }else{ - if(!e.checkSnapTime)e.checkSnapTime = snap.mtime - if(err || e.checkSnapTime === snap.mtime){ - e.checkSnapTime = snap.mtime - notStreaming() - }else{ - resetSnapCheck() - } - } - }) - } - },60000*1); - } - resetSnapCheck() - } - if(config.childNodes.mode !== 'child' && s.platform!=='darwin' && (x==='record' || (x==='start'&&e.details.detector_record_method==='sip'))){ - //check if ffmpeg is recording - s.group[e.ke].mon[e.id].fswatch = fs.watch(e.dir, {encoding : 'utf8'}, (event, filename) => { - switch(event){ - case'rename': - s.group[e.ke].mon[e.id].open = filename.split('.')[0] - break; - case'change': - resetRecordingCheck() - break; - } - }); - } - if( - //is MacOS - s.platform !== 'darwin' && - //is Watch-Only or Record - (x === 'start' || x === 'record') && - //if JPEG API enabled or Stream Type is HLS - (e.details.stream_type === 'jpeg' || e.details.stream_type === 'hls' || e.details.snap === '1') - ){ - s.group[e.ke].mon[e.id].fswatchStream = fs.watch(e.sdir, {encoding : 'utf8'}, () => { - resetStreamCheck() - }) - } - s.camera('snapshot',{mid:e.id,ke:e.ke,mon:e}) - //check host to see if has password and user in it - setStreamDir() - clearTimeout(s.group[e.ke].mon[e.id].checker) - if(s.group[e.ke].mon[e.id].started===1){ - e.error_count=0; - s.group[e.ke].mon[e.id].error_socket_timeout_count=0; - s.kill(s.group[e.ke].mon[e.id].spawn,e); - startVideoProcessor=function(err,o){ - if(o.success===true){ - e.frames=0; - if(!s.group[e.ke].mon[e.id].record){s.group[e.ke].mon[e.id].record={yes:1}}; - //launch ffmpeg (main) - s.group[e.ke].mon[e.id].spawn = s.ffmpeg(e) - s.group[e.ke].mon[e.id].spawn.stdio[5].on('data',function(data){ - resetStreamCheck() - // var progress = {} - // data.toString().split('\n').forEach(function(v){ - // var split = v.split('=') - // var val = split[1] - // if(val)progress[split[0]] = val - // }) - }) - if(e.type === 'dashcam'){ - setTimeout(function(){ - s.group[e.ke].mon[e.id].allowStdinWrite = true - s.txToDashcamUsers({ - f : 'enable_stream', - ke : e.ke, - mid : e.id - },e.ke) - },30000) - } - s.init('monitorStatus',{id:e.id,ke:e.ke,status:wantedStatus}); - //on unexpected exit restart - s.group[e.ke].mon[e.id].spawn_exit=function(){ - if(s.group[e.ke].mon[e.id].started===1){ - if(e.details.loglevel!=='quiet'){ - s.log(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang['Process Crashed for Monitor'],cmd:s.group[e.ke].mon[e.id].ffmpeg}}); - } - errorFatal('Process Unexpected Exit'); - } - } - s.group[e.ke].mon[e.id].spawn.on('end',s.group[e.ke].mon[e.id].spawn_exit) - s.group[e.ke].mon[e.id].spawn.on('exit',s.group[e.ke].mon[e.id].spawn_exit) - - //emitter for mjpeg - if(!e.details.stream_mjpeg_clients||e.details.stream_mjpeg_clients===''||isNaN(e.details.stream_mjpeg_clients)===false){e.details.stream_mjpeg_clients=20;}else{e.details.stream_mjpeg_clients=parseInt(e.details.stream_mjpeg_clients)} - s.group[e.ke].mon[e.id].emitter = new events.EventEmitter().setMaxListeners(e.details.stream_mjpeg_clients); - s.log(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].mon[e.id].ffmpeg}}); - s.tx({f:'monitor_starting',mode:x,mid:e.id,time:s.formattedTime()},'GRP_'+e.ke); - //start workers - if(e.type==='jpeg'){ - if(!e.details.sfps||e.details.sfps===''){ - var capture_fps=parseFloat(e.details.sfps); - if(isNaN(capture_fps)){capture_fps=1} - } - if(s.group[e.ke].mon[e.id].spawn){ - s.group[e.ke].mon[e.id].spawn.stdin.on('error',function(err){ - if(err&&e.details.loglevel!=='quiet'){ - s.log(e,{type:'STDIN ERROR',msg:err}); - } - }) - }else{ - if(x==='record'){ - s.log(e,{type:lang.FFmpegCantStart,msg:lang.FFmpegCantStartText}); - return - } - } - e.captureOne=function(f){ - s.group[e.ke].mon[e.id].record.request=request({url:e.url,method:'GET',encoding: null,timeout:15000},function(err,data){ - if(err){ - return; - } - }).on('data',function(d){ - if(!e.buffer0){ - e.buffer0=[d] - }else{ - e.buffer0.push(d); - } - if((d[d.length-2] === 0xFF && d[d.length-1] === 0xD9)){ - e.buffer0=Buffer.concat(e.buffer0); - ++e.frames; - if(s.group[e.ke].mon[e.id].spawn&&s.group[e.ke].mon[e.id].spawn.stdin){ - s.group[e.ke].mon[e.id].spawn.stdin.write(e.buffer0); - } - if(s.group[e.ke].mon[e.id].started===1){ - s.group[e.ke].mon[e.id].record.capturing=setTimeout(function(){ - e.captureOne() - },1000/capture_fps); - } - e.buffer0=null; - } - if(!e.timeOut){ - e.timeOut=setTimeout(function(){e.error_count=0;delete(e.timeOut);},3000); - } - - }).on('error', function(err){ - ++e.error_count; - clearTimeout(e.timeOut);delete(e.timeOut); - if(e.details.loglevel!=='quiet'){ - s.log(e,{type:lang['JPEG Error'],msg:{msg:lang.JPEGErrorText,info:err}}); - switch(err.code){ - case'ESOCKETTIMEDOUT': - case'ETIMEDOUT': - ++s.group[e.ke].mon[e.id].error_socket_timeout_count - if(e.details.fatal_max!==0&&s.group[e.ke].mon[e.id].error_socket_timeout_count>e.details.fatal_max){ - s.log(e,{type:lang['Fatal Maximum Reached'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}}); - s.camera('stop',e) - }else{ - s.log(e,{type:lang['Restarting Process'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}}); - s.camera('restart',e) - } - return; - break; - } - } - if(e.details.fatal_max!==0&&e.error_count>e.details.fatal_max){ - clearTimeout(s.group[e.ke].mon[e.id].record.capturing); - launchMonitorProcesses(); - } - }); - } - e.captureOne() - } - if(!s.group[e.ke]||!s.group[e.ke].mon[e.id]){s.init(0,e)} - s.group[e.ke].mon[e.id].spawn.on('error',function(er){ - s.log(e,{type:'Spawn Error',msg:er});errorFatal('Spawn Error') - }); - if(e.details.detector==='1'){ - s.ocvTx({f:'init_monitor',id:e.id,ke:e.ke}) - //frames from motion detect - if(e.details.detector_pam==='1'){ - if(e.coProcessor === false){ - s.createPamDiffEngine(e) - s.group[e.ke].mon[e.id].spawn.stdio[3].pipe(s.group[e.ke].mon[e.id].p2p).pipe(s.group[e.ke].mon[e.id].pamDiff) - if(e.details.detector_use_detect_object === '1'){ - s.group[e.ke].mon[e.id].spawn.stdio[4].on('data',function(d){ - s.group[e.ke].mon[e.id].lastJpegDetectorFrame = d - }) - } - } - }else{ - if(e.coProcessor === false){ - s.group[e.ke].mon[e.id].spawn.stdio[3].on('data',function(d){ - s.ocvTx({f:'frame',mon:s.group[e.ke].mon_conf[e.id].details,ke:e.ke,id:e.id,time:s.formattedTime(),frame:d}); - }) - } - } - } - //frames to stream - switch(e.details.stream_type){ - case'mp4': - s.group[e.ke].mon[e.id].mp4frag['MAIN'] = new Mp4Frag(); - s.group[e.ke].mon[e.id].mp4frag['MAIN'].on('error',function(error){ - s.log(e,{type:lang['Mp4Frag'],msg:{error:error}}) - }) - s.group[e.ke].mon[e.id].spawn.stdio[1].pipe(s.group[e.ke].mon[e.id].mp4frag['MAIN']) - break; - case'flv': - e.frame_to_stream=function(d){ - if(!s.group[e.ke].mon[e.id].firstStreamChunk['MAIN'])s.group[e.ke].mon[e.id].firstStreamChunk['MAIN'] = d; - e.frame_to_stream=function(d){ - resetStreamCheck() - s.group[e.ke].mon[e.id].emitter.emit('data',d); - } - e.frame_to_stream(d) - } - break; - case'mjpeg': - e.frame_to_stream=function(d){ - resetStreamCheck() - s.group[e.ke].mon[e.id].emitter.emit('data',d); - } - break; - case'h265': - e.frame_to_stream=function(d){ - resetStreamCheck() - s.group[e.ke].mon[e.id].emitter.emit('data',d); - } - break; - case'b64':case undefined:case null:case'': - var buffer - e.frame_to_stream=function(d){ - resetStreamCheck() - if(!buffer){ - buffer=[d] - }else{ - buffer.push(d); - } - if((d[d.length-2] === 0xFF && d[d.length-1] === 0xD9)){ - s.group[e.ke].mon[e.id].emitter.emit('data',Buffer.concat(buffer)); - buffer=null; - } - } - break; - } - if(e.frame_to_stream){ - if(e.coProcessor === true && e.details.stream_type === ('b64'||'mjpeg')){ - - }else{ - s.group[e.ke].mon[e.id].spawn.stdout.on('data',e.frame_to_stream) - } - } - if(e.details.stream_channels&&e.details.stream_channels!==''){ - var createStreamEmitter = function(channel,number){ - var pipeNumber = number+config.pipeAddition; - if(!s.group[e.ke].mon[e.id].emitterChannel[pipeNumber]){ - s.group[e.ke].mon[e.id].emitterChannel[pipeNumber] = new events.EventEmitter().setMaxListeners(0); - } - var frame_to_stream - switch(channel.stream_type){ - case'mp4': - s.group[e.ke].mon[e.id].mp4frag[pipeNumber] = new Mp4Frag(); - s.group[e.ke].mon[e.id].spawn.stdio[pipeNumber].pipe(s.group[e.ke].mon[e.id].mp4frag[pipeNumber]) - break; - case'mjpeg': - frame_to_stream=function(d){ - s.group[e.ke].mon[e.id].emitterChannel[pipeNumber].emit('data',d); - } - break; - case'flv': - frame_to_stream=function(d){ - if(!s.group[e.ke].mon[e.id].firstStreamChunk[pipeNumber])s.group[e.ke].mon[e.id].firstStreamChunk[pipeNumber] = d; - frame_to_stream=function(d){ - s.group[e.ke].mon[e.id].emitterChannel[pipeNumber].emit('data',d); - } - frame_to_stream(d) - } - break; - case'h264': - frame_to_stream=function(d){ - s.group[e.ke].mon[e.id].emitterChannel[pipeNumber].emit('data',d); - } - break; - } - if(frame_to_stream){ - s.group[e.ke].mon[e.id].spawn.stdio[pipeNumber].on('data',frame_to_stream); - } - } - e.details.stream_channels.forEach(createStreamEmitter) - } - if(x==='record'||e.type==='mjpeg'||e.type==='h264'||e.type==='local'){ - var checkLog = function(d,x){return d.indexOf(x)>-1;} - s.group[e.ke].mon[e.id].spawn.stderr.on('data',function(d){ - d=d.toString(); - switch(true){ - case checkLog(d,'[hls @'): - case checkLog(d,'Past duration'): - case checkLog(d,'Last message repeated'): - case checkLog(d,'pkt->duration = 0'): - case checkLog(d,'Non-monotonous DTS'): - case checkLog(d,'NULL @'): - case checkLog(d,'RTP: missed'): - case checkLog(d,'deprecated pixel format used'): - return - break; - //mp4 output with webm encoder chosen - case checkLog(d,'Could not find tag for vp8'): - case checkLog(d,'Only VP8 or VP9 Video'): - case checkLog(d,'Could not write header'): -// switch(e.ext){ -// case'mp4': -// e.details.vcodec='libx264' -// e.details.acodec='none' -// break; -// case'webm': -// e.details.vcodec='libvpx' -// e.details.acodec='none' -// break; -// } -// if(e.details.stream_type==='hls'){ -// e.details.stream_vcodec='libx264' -// e.details.stream_acodec='no' -// } -// s.camera('restart',e) - return s.log(e,{type:lang['Incorrect Settings Chosen'],msg:{msg:d}}) - break; -// case checkLog(d,'av_interleaved_write_frame'): - case checkLog(d,'Connection refused'): - case checkLog(d,'Connection timed out'): - //restart - setTimeout(function(){ - s.log(e,{type:lang['Connection timed out'],msg:lang['Retrying...']}); - errorFatal('Connection timed out'); - },1000) - break; -// case checkLog(d,'No such file or directory'): -// case checkLog(d,'Unable to open RTSP for listening'): -// case checkLog(d,'timed out'): -// case checkLog(d,'Invalid data found when processing input'): -// case checkLog(d,'reset by peer'): -// if(e.frames===0&&x==='record'){s.video('delete',e)}; -// setTimeout(function(){ -// if(!s.group[e.ke].mon[e.id].spawn){launchMonitorProcesses()} -// },2000) -// break; - case checkLog(d,'Immediate exit requested'): - case checkLog(d,'mjpeg_decode_dc'): - case checkLog(d,'bad vlc'): - case checkLog(d,'error dc'): - launchMonitorProcesses() - break; - case /T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(d): - var filename = d.split('.')[0]+'.'+e.ext - s.video('insertCompleted',e,{ - file : filename - }) - s.log(e,{type:lang['Video Finished'],msg:{filename:d}}) - if( - e.details.detector==='1'&& - s.group[e.ke].mon[e.id].started===1&& - e.details&& - e.details.detector_record_method==='del'&& - e.details.detector_delete_motionless_videos==='1'&& - s.group[e.ke].mon[e.id].detector_motion_count===0 - ){ - if(e.details.loglevel!=='quiet'){ - s.log(e,{type:lang['Delete Motionless Video'],msg:filename}); - } - s.video('delete',{ - filename : filename, - ke : e.ke, - id : e.id - }) - } - s.group[e.ke].mon[e.id].detector_motion_count = 0 - resetRecordingCheck() - return; - break; - } - s.log(e,{type:"FFMPEG STDERR",msg:d}) - }); - } - if(e.coProcessor === true){ - setTimeout(function(){ - s.coSpawnLauncher(e) - },6000) - } - }else{ - s.log(e,{type:lang["Ping Failed"],msg:lang.skipPingText1}); - errorFatal("Ping Failed");return; - } - } - if(e.type!=='socket'&&e.type!=='dashcam'&&e.protocol!=='udp'&&e.type!=='local'||e.details.skip_ping === '1'){ - connectionTester.test(e.hosty,e.port,2000,startVideoProcessor); - }else{ - startVideoProcessor(null,{success:true}) - } - }else{ - s.kill(s.group[e.ke].mon[e.id].spawn,e); - } - } - //start drawing files - delete(s.group[e.ke].mon[e.id].childNode) - if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){ - var childNodeList = Object.keys(s.childNodes) - if(childNodeList.length>0){ - e.ch_stop = 0; - launchMonitorProcesses = function(){ - startVideoProcessor = function(){ - s.cx({ - //function - f : 'cameraStart', - //mode - mode : x, - //data, options - d : s.group[e.ke].mon_conf[e.id] - },s.group[e.ke].mon[e.id].childNodeId) - } - if(e.type!=='socket'&&e.type!=='dashcam'&&e.protocol!=='udp'&&e.type!=='local' && e.details.skip_ping !== '1'){ - console.log(e.hosty,e.port) - connectionTester.test(e.hosty,e.port,2000,function(err,o){ - if(o.success===true){ - startVideoProcessor() - }else{ - s.log(e,{type:lang["Ping Failed"],msg:lang.skipPingText1}); - errorFatal("Ping Failed");return; - } - }) - }else{ - startVideoProcessor() - } - } - childNodeList.forEach(function(ip){ - if(e.ch_stop===0&&s.childNodes[ip].cpu<80){ - e.ch_stop=1; - s.childNodes[ip].activeCameras[e.ke+e.id] = s.init('noReference',s.group[e.ke].mon_conf[e.id]); - s.group[e.ke].mon[e.id].childNode = ip; - s.group[e.ke].mon[e.id].childNodeId = s.childNodes[ip].cnid; - s.cx({f:'sync',sync:s.group[e.ke].mon_conf[e.id],ke:e.ke,mid:e.id},s.group[e.ke].mon[e.id].childNodeId); - launchMonitorProcesses(); - } - }) - }else{ - launchMonitorProcesses(); - } - }else{ - launchMonitorProcesses(); - } - break; - } - if(typeof cn==='function'){setTimeout(function(){cn()},1000);} -} - -//function for receiving detector data -s.pluginEventController=function(d){ - switch(d.f){ - case'trigger': - s.event('trigger',d) - break; - case's.tx': - s.tx(d.data,d.to) - break; - case'sql': - sql.query(d.query,d.values); - break; - case'log': - s.systemLog('PLUGIN : '+d.plug+' : ',d) - break; - } -} -//multi plugin connections -s.connectedPlugins={} -s.pluginInitiatorSuccess=function(mode,d,cn){ - s.systemLog('pluginInitiatorSuccess',d) - if(mode==='client'){ - //is in client mode (camera.js is client) - cn.pluginEngine=d.plug - if(!s.connectedPlugins[d.plug]){ - s.connectedPlugins[d.plug]={plug:d.plug} - } - s.systemLog('Connected to plugin : Detector - '+d.plug+' - '+d.type) - switch(d.type){ - default:case'detector': - s.ocv={started:s.timeObject(),id:cn.id,plug:d.plug,notice:d.notice,isClientPlugin:true}; - cn.ocv=1; - s.tx({f:'detector_plugged',plug:d.plug,notice:d.notice},'CPU') - break; - } - }else{ - //is in host mode (camera.js is client) - switch(d.type){ - default:case'detector': - s.ocv={started:s.timeObject(),id:"host",plug:d.plug,notice:d.notice,isHostPlugin:true}; - break; - } - } - s.connectedPlugins[d.plug].plugged=true - s.tx({f:'readPlugins',ke:d.ke},'CPU') - s.ocvTx({f:'api_key',key:d.plug}) - s.api[d.plug]={pluginEngine:d.plug,permissions:{},details:{},ip:'0.0.0.0'}; -} -s.pluginInitiatorFail=function(mode,d,cn){ - if(s.connectedPlugins[d.plug])s.connectedPlugins[d.plug].plugged=false - if(mode==='client'){ - //is in client mode (camera.js is client) - cn.disconnect() - }else{ - //is in host mode (camera.js is client) - } -} -if(config.plugins&&config.plugins.length>0){ - config.plugins.forEach(function(v){ - s.connectedPlugins[v.id]={plug:v.id} - if(v.enabled===false){return} - if(v.mode==='host'){ - //is in host mode (camera.js is client) - if(v.https===true){ - v.https='https://' - }else{ - v.https='http://' - } - if(!v.port){ - v.port=80 - } - var socket = socketIOclient(v.https+v.host+':'+v.port) - s.connectedPlugins[v.id].tx = function(x){return socket.emit('f',x)} - socket.on('connect', function(cn){ - s.systemLog('Connected to plugin (host) : '+v.id) - s.connectedPlugins[v.id].tx({f:'init_plugin_as_host',key:v.key}) - }); - socket.on('init',function(d){ - s.systemLog('Initialize Plugin : Host',d) - if(d.ok===true){ - s.pluginInitiatorSuccess("host",d) - }else{ - s.pluginInitiatorFail("host",d) - } - }); - socket.on('ocv',s.pluginEventController); - socket.on('disconnect', function(){ - s.connectedPlugins[v.id].plugged=false - delete(s.api[v.id]) - s.systemLog('Plugin Disconnected : '+v.id) - s.connectedPlugins[v.id].reconnector = setInterval(function(){ - if(socket.connected===true){ - clearInterval(s.connectedPlugins[v.id].reconnector) - }else{ - socket.connect() - } - },1000*2) - }); - s.connectedPlugins[v.id].ws = socket; - } - }) -} -////socket controller -s.cn=function(cn){return{id:cn.id,ke:cn.ke,uid:cn.uid}} -io.on('connection', function (cn) { -var tx; - //set "client" detector plugin event function - cn.on('ocv',function(d){ - if(!cn.pluginEngine&&d.f==='init'){ - if(config.pluginKeys[d.plug]===d.pluginKey){ - s.pluginInitiatorSuccess("client",d,cn) - }else{ - s.pluginInitiatorFail("client",d,cn) - } - }else{ - if(config.pluginKeys[d.plug]===d.pluginKey){ - s.pluginEventController(d) - }else{ - cn.disconnect() - } - } - }) - //unique h265 socket stream - cn.on('h265',function(d){ - if(!s.group[d.ke]||!s.group[d.ke].mon||!s.group[d.ke].mon[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);} - d.failed=function(msg){ - tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); - cn.disconnect(); - } - d.success=function(r){ - r=r[0]; - var Emitter,chunkChannel - if(!d.channel){ - Emitter = s.group[d.ke].mon[d.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[d.ke].mon[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] - chunkChannel = parseInt(d.channel)+config.pipeAddition - } - if(!Emitter){ - cn.disconnect();return; - } - if(!d.channel)d.channel = 'MAIN'; - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - cn.channel=d.channel; - cn.removeListenerOnDisconnect=true; - cn.socketVideoStream=d.id; - 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]){ - d.success(s.group[d.ke].users[d.auth]); - }else{ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed('User not found') - } - }) - }else{ - d.failed('Permissions for this key do not allow authentication with Websocket') - } - }else{ - d.failed('Not an API key') - } - }) - } - }) - } - }) - //unique Base64 socket stream - cn.on('Base64',function(d){ - if(!s.group[d.ke]||!s.group[d.ke].mon||!s.group[d.ke].mon[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);} - d.failed=function(msg){ - tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); - cn.disconnect(); - } - d.success=function(r){ - r=r[0]; - var Emitter,chunkChannel - if(!d.channel){ - Emitter = s.group[d.ke].mon[d.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[d.ke].mon[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] - chunkChannel = parseInt(d.channel)+config.pipeAddition - } - if(!Emitter){ - cn.disconnect();return; - } - if(!d.channel)d.channel = 'MAIN'; - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - cn.channel=d.channel; - cn.removeListenerOnDisconnect=true; - cn.socketVideoStream=d.id; - 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]){ - d.success(s.group[d.ke].users[d.auth]); - }else{ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed('User not found') - } - }) - }else{ - d.failed('Permissions for this key do not allow authentication with Websocket') - } - }else{ - d.failed('Not an API key') - } - }) - } - }) - } - }) - //unique FLV socket stream - cn.on('FLV',function(d){ - if(!s.group[d.ke]||!s.group[d.ke].mon||!s.group[d.ke].mon[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);} - d.failed=function(msg){ - tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); - cn.disconnect(); - } - d.success=function(r){ - r=r[0]; - var Emitter,chunkChannel - if(!d.channel){ - Emitter = s.group[d.ke].mon[d.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[d.ke].mon[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] - chunkChannel = parseInt(d.channel)+config.pipeAddition - } - if(!Emitter){ - cn.disconnect();return; - } - if(!d.channel)d.channel = 'MAIN'; - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - cn.channel=d.channel; - cn.removeListenerOnDisconnect=true; - cn.socketVideoStream=d.id; - var contentWriter - cn.closeSocketVideoStream = function(){ - Emitter.removeListener('data', contentWriter); - } - tx({time:toUTC(),buffer:s.group[d.ke].mon[d.id].firstStreamChunk[chunkChannel]}) - Emitter.on('data',contentWriter = function(buffer){ - tx({time:toUTC(),buffer:buffer}) - }) - } - if(s.group[d.ke]&&s.group[d.ke].users&&s.group[d.ke].users[d.auth]){ - d.success(s.group[d.ke].users[d.auth]); - }else{ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed('User not found') - } - }) - }else{ - d.failed('Permissions for this key do not allow authentication with Websocket') - } - }else{ - d.failed('Not an API key') - } - }) - } - }) - } - }) - //unique MP4 socket stream - cn.on('MP4',function(d){ - if(!s.group[d.ke]||!s.group[d.ke].mon||!s.group[d.ke].mon[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);} - d.failed=function(msg){ - tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); - cn.disconnect(); - } - d.success=function(r){ - r=r[0]; - var Emitter,chunkChannel - if(!d.channel){ - Emitter = s.group[d.ke].mon[d.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[d.ke].mon[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] - chunkChannel = parseInt(d.channel)+config.pipeAddition - } - if(!Emitter){ - cn.disconnect();return; - } - if(!d.channel)d.channel = 'MAIN'; - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - cn.channel=d.channel; - cn.socketVideoStream=d.id; - var mp4frag = s.group[d.ke].mon[d.id].mp4frag[d.channel]; - var onInitialized = () => { - cn.emit('mime', mp4frag.mime); - mp4frag.removeListener('initialized', onInitialized); - }; - //event listener - var onSegment = function(data){ - cn.emit('segment', data); - }; - cn.closeSocketVideoStream = function(){ - mp4frag.removeListener('segment', onSegment) - mp4frag.removeListener('initialized', onInitialized) - } - cn.on('MP4Command',function(msg){ - switch (msg) { - case 'mime' ://client is requesting mime - var mime = mp4frag.mime; - if (mime) { - cn.emit('mime', mime); - } else { - mp4frag.on('initialized', onInitialized); - } - break; - case 'initialization' ://client is requesting initialization segment - cn.emit('initialization', mp4frag.initialization); - break; - case 'segment' ://client is requesting a SINGLE segment - var segment = mp4frag.segment; - if (segment) { - cn.emit('segment', segment); - } else { - mp4frag.once('segment', onSegment); - } - break; - case 'segments' ://client is requesting ALL segments - //send current segment first to start video asap - var segment = mp4frag.segment; - if (segment) { - cn.emit('segment', segment); - } - //add listener for segments being dispatched by mp4frag - mp4frag.on('segment', onSegment); - break; - case 'pause' : - mp4frag.removeListener('segment', onSegment); - break; - case 'resume' : - mp4frag.on('segment', onSegment); - break; - case 'stop' ://client requesting to stop receiving segments - cn.closeSocketVideoStream() - break; - } - }) - } - if(s.group[d.ke]&&s.group[d.ke].users&&s.group[d.ke].users[d.auth]){ - d.success(s.group[d.ke].users[d.auth]); - }else{ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed('User not found') - } - }) - }else{ - d.failed('Permissions for this key do not allow authentication with Websocket') - } - }else{ - d.failed('Not an API key') - } - }) - } - }) - } - }) - //main socket control functions - cn.on('f',function(d){ - if(!cn.ke&&d.f==='init'){//socket login - cn.ip=cn.request.connection.remoteAddress; - tx=function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);} - d.failed=function(){tx({ok:false,msg:'Not Authorized',token_used:d.auth,ke:d.ke});cn.disconnect();} - d.success=function(r){ - r=r[0];cn.join('GRP_'+d.ke);cn.join('CPU'); - cn.ke=d.ke, - cn.uid=d.uid, - cn.auth=d.auth; - if(!s.group[d.ke])s.group[d.ke]={}; -// if(!s.group[d.ke].vid)s.group[d.ke].vid={}; - if(!s.group[d.ke].users)s.group[d.ke].users={}; -// s.group[d.ke].vid[cn.id]={uid:d.uid}; - s.group[d.ke].users[d.auth]={cnid:cn.id,uid:r.uid,mail:r.mail,details:JSON.parse(r.details),logged_in_at:s.timeObject(new Date).format(),login_type:'Dashboard'} - try{s.group[d.ke].users[d.auth].details=JSON.parse(r.details)}catch(er){} - 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.log({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].mon){ - s.group[d.ke].mon={} - if(!s.group[d.ke].mon){s.group[d.ke].mon={}} - } - if(s.ocv){ - tx({f:'detector_plugged',plug:s.ocv.plug,notice:s.ocv.notice}) - s.ocvTx({f:'readPlugins',ke:d.ke}) - } - tx({f:'users_online',users:s.group[d.ke].users}) - s.tx({f:'user_status_change',ke:d.ke,uid:cn.uid,status:1,user:s.group[d.ke].users[d.auth]},'GRP_'+d.ke) - s.init('diskUsedEmit',d) - s.init('apps',d) - s.sqlQuery('SELECT * FROM API WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,rrr) { - tx({ - f:'init_success', - users:s.group[d.ke].vid, - apis:rrr, - os:{ - platform:s.platform, - cpuCount:os.cpus().length, - totalmem:s.totalmem - } - }) - try{ - s.sqlQuery('SELECT * FROM Monitors WHERE ke=?', [d.ke], function(err,r) { - if(r && r[0]){ - r.forEach(function(monitor){ - s.camera('snapshot',{mid:monitor.mid,ke:monitor.ke,mon:monitor}) - }) - } - }) - }catch(err){ - console.log(err) - } - }) - } - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - if(r.details.auth_socket==='1'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { - if(r&&r[0]){ - d.success(r) - }else{ - d.failed() - } - }) - }else{ - d.failed() - } - }else{ - d.failed() - } - }) - } - }) - return; - } - if((d.id||d.uid||d.mid)&&cn.ke){ - try{ - switch(d.f){ - case'ocv_in': - s.ocvTx(d.data) - break; - case'monitorOrder': - if(d.monitorOrder&&d.monitorOrder instanceof Object){ - s.sqlQuery('SELECT details FROM Users WHERE uid=? AND ke=?',[cn.uid,cn.ke],function(err,r){ - if(r&&r[0]){ - r=JSON.parse(r[0].details); - r.monitorOrder=d.monitorOrder; - s.sqlQuery('UPDATE Users SET details=? WHERE uid=? AND ke=?',[JSON.stringify(r),cn.uid,cn.ke]) - } - }) - } - break; - case'update': - if(!config.updateKey){ - tx({error:lang.updateKeyText1}); - return; - } - if(d.key===config.updateKey){ - exec('chmod +x '+__dirname+'/UPDATE.sh&&'+__dirname+'/UPDATE.sh',{detached: true}) - }else{ - tx({error:lang.updateKeyText2}); - } - break; - case'cron': - if(s.group[cn.ke]&&s.group[cn.ke].users[cn.auth].details&&!s.group[cn.ke].users[cn.auth].details.sub){ - s.tx({f:d.ff},s.cron.id) - } - break; - case'api': - switch(d.ff){ - case'delete': - d.set=[],d.ar=[]; - d.form.ke=cn.ke;d.form.uid=cn.uid;delete(d.form.ip); - if(!d.form.code){tx({f:'form_incomplete',form:'APIs'});return} - d.for=Object.keys(d.form); - d.for.forEach(function(v){ - d.set.push(v+'=?'),d.ar.push(d.form[v]); - }); - s.sqlQuery('DELETE FROM API WHERE '+d.set.join(' AND '),d.ar,function(err,r){ - if(!err){ - tx({f:'api_key_deleted',form:d.form}); - delete(s.api[d.form.code]); - }else{ - s.systemLog('API Delete Error : '+e.ke+' : '+' : '+e.mid,err) - } - }) - break; - case'add': - d.set=[],d.qu=[],d.ar=[]; - d.form.ke=cn.ke,d.form.uid=cn.uid,d.form.code=s.gid(30); - d.for=Object.keys(d.form); - d.for.forEach(function(v){ - d.set.push(v),d.qu.push('?'),d.ar.push(d.form[v]); - }); - s.sqlQuery('INSERT INTO API ('+d.set.join(',')+') VALUES ('+d.qu.join(',')+')',d.ar,function(err,r){ - d.form.time=s.formattedTime(new Date,'YYYY-DD-MM HH:mm:ss'); - if(!err){tx({f:'api_key_added',form:d.form});}else{s.systemLog(err)} - }); - break; - } - break; - case'settings': - switch(d.ff){ - case'filters': - switch(d.fff){ - case'save':case'delete': - s.sqlQuery('SELECT details FROM Users WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,r){ - if(r&&r[0]){ - r=r[0]; - d.d=JSON.parse(r.details); - if(d.form.id===''){d.form.id=s.gid(5)} - if(!d.d.filters)d.d.filters={}; - //save/modify or delete - if(d.fff==='save'){ - d.d.filters[d.form.id]=d.form; - }else{ - delete(d.d.filters[d.form.id]); - } - s.sqlQuery('UPDATE Users SET details=? WHERE ke=? AND uid=?',[JSON.stringify(d.d),d.ke,d.uid],function(err,r){ - tx({f:'filters_change',uid:d.uid,ke:d.ke,filters:d.d.filters}); - }); - } - }) - break; - } - break; - case'edit': - s.sqlQuery('SELECT details FROM Users WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,r){ - if(r&&r[0]){ - r=r[0]; - d.d=JSON.parse(r.details); - if(!d.d.sub || d.d.user_change === "1"){ - if(d.d.get_server_log==='1'){ - cn.join('GRPLOG_'+d.ke) - }else{ - cn.leave('GRPLOG_'+d.ke) - } - ///unchangeable from client side, so reset them in case they did. - d.form.details=JSON.parse(d.form.details) - //admin permissions - d.form.details.permissions=d.d.permissions - d.form.details.edit_size=d.d.edit_size - d.form.details.edit_days=d.d.edit_days - d.form.details.use_admin=d.d.use_admin - d.form.details.use_webdav=d.d.use_webdav - d.form.details.use_aws_s3=d.d.use_aws_s3 - d.form.details.use_ldap=d.d.use_ldap - //check - if(d.d.edit_days=="0"){ - d.form.details.days=d.d.days; - } - if(d.d.edit_size=="0"){ - d.form.details.size=d.d.size; - } - if(d.d.sub){ - d.form.details.sub=d.d.sub; - if(d.d.monitors){d.form.details.monitors=d.d.monitors;} - if(d.d.allmonitors){d.form.details.allmonitors=d.d.allmonitors;} - if(d.d.monitor_create){d.form.details.monitor_create=d.d.monitor_create;} - if(d.d.video_delete){d.form.details.video_delete=d.d.video_delete;} - if(d.d.video_view){d.form.details.video_view=d.d.video_view;} - if(d.d.monitor_edit){d.form.details.monitor_edit=d.d.monitor_edit;} - if(d.d.size){d.form.details.size=d.d.size;} - if(d.d.days){d.form.details.days=d.d.days;} - delete(d.form.details.mon_groups) - } - var newSize = d.form.details.size - d.form.details=JSON.stringify(d.form.details) - /// - d.set=[],d.ar=[]; - if(d.form.pass&&d.form.pass!==''){d.form.pass=s.md5(d.form.pass);}else{delete(d.form.pass)}; - delete(d.form.password_again); - d.for=Object.keys(d.form); - d.for.forEach(function(v){ - d.set.push(v+'=?'),d.ar.push(d.form[v]); - }); - d.ar.push(d.ke),d.ar.push(d.uid); - s.sqlQuery('UPDATE Users SET '+d.set.join(',')+' WHERE ke=? AND uid=?',d.ar,function(err,r){ - if(!d.d.sub){ - s.group[d.ke].sizeLimit = parseFloat(newSize) - delete(s.group[d.ke].webdav) - s.group[d.ke].aws = null - s.group[d.ke].aws_s3 = null - if(s.group[d.ke].discordBot && s.group[d.ke].discordBot.destroy){ - s.group[d.ke].discordBot.destroy() - delete(s.group[d.ke].discordBot) - } - s.init('apps',d) - } - tx({f:'user_settings_change',uid:d.uid,ke:d.ke,form:d.form}); - }); - } - } - }) - break; - } - break; - case'monitor': - switch(d.ff){ - case'get': - switch(d.fff){ - case'videos&events': - if(!d.eventLimit){ - d.eventLimit = 500 - }else{ - d.eventLimit = parseInt(d.eventLimit); - } - if(!d.eventStartDate&&d.startDate){ - d.eventStartDate = s.stringToSqlTime(d.startDate) - } - if(!d.eventEndDate&&d.endDate){ - d.eventEndDate = s.stringToSqlTime(d.endDate) - } - var monitorQuery = '' - var monitorValues = [] - var permissions = s.group[d.ke].users[cn.auth].details; - if(!d.mid){ - if(permissions.sub&&permissions.monitors&&permissions.allmonitors!=='1'){ - try{permissions.monitors=JSON.parse(permissions.monitors);}catch(er){} - var or = []; - permissions.monitors.forEach(function(v,n){ - or.push('mid=?'); - monitorValues.push(v) - }) - monitorQuery += ' AND ('+or.join(' OR ')+')' - } - }else if(!permissions.sub||permissions.allmonitors!=='0'||permissions.monitors.indexOf(d.mid)>-1){ - monitorQuery += ' and mid=?'; - monitorValues.push(d.mid) - } - var getEvents = function(callback){ - var eventQuery = 'SELECT * FROM Events WHERE ke=?'; - var eventQueryValues = [cn.ke]; - if(d.eventStartDate&&d.eventStartDate!==''){ - if(d.eventEndDate&&d.eventEndDate!==''){ - eventQuery+=' AND `time` >= ? AND `time` <= ?'; - eventQueryValues.push(d.eventStartDate) - eventQueryValues.push(d.eventEndDate) - }else{ - eventQuery+=' AND `time` >= ?'; - eventQueryValues.push(d.eventStartDate) - } - } - if(monitorValues.length>0){ - eventQuery += monitorQuery; - eventQueryValues = eventQueryValues.concat(monitorValues); - } - eventQuery+=' ORDER BY `time` DESC LIMIT '+d.eventLimit+''; - s.sqlQuery(eventQuery,eventQueryValues,function(err,r){ - if(err){ - console.log(eventQuery) - console.error('LINE 2428',err) - setTimeout(function(){ - getEvents(callback) - },2000) - }else{ - if(!r){r=[]} - r.forEach(function(v,n){ - r[n].details=JSON.parse(v.details); - }) - callback(r) - } - }) - } - if(!d.videoLimit&&d.limit){ - d.videoLimit=d.limit - eventQuery.push() - } - if(!d.videoStartDate&&d.startDate){ - d.videoStartDate = s.stringToSqlTime(d.startDate) - } - if(!d.videoEndDate&&d.endDate){ - d.videoEndDate = s.stringToSqlTime(d.endDate) - } - var getVideos = function(callback){ - var videoQuery='SELECT * FROM Videos WHERE ke=?'; - var videoQueryValues=[cn.ke]; - if(d.videoStartDate||d.videoEndDate){ - if(!d.videoStartDateOperator||d.videoStartDateOperator==''){ - d.videoStartDateOperator='>=' - } - if(!d.videoEndDateOperator||d.videoEndDateOperator==''){ - d.videoEndDateOperator='<=' - } - switch(true){ - case(d.videoStartDate&&d.videoStartDate!==''&&d.videoEndDate&&d.videoEndDate!==''): - videoQuery+=' AND `time` '+d.videoStartDateOperator+' ? AND `end` '+d.videoEndDateOperator+' ?'; - videoQueryValues.push(d.videoStartDate) - videoQueryValues.push(d.videoEndDate) - break; - case(d.videoStartDate&&d.videoStartDate!==''): - videoQuery+=' AND `time` '+d.videoStartDateOperator+' ?'; - videoQueryValues.push(d.videoStartDate) - break; - case(d.videoEndDate&&d.videoEndDate!==''): - videoQuery+=' AND `end` '+d.videoEndDateOperator+' ?'; - videoQueryValues.push(d.videoEndDate) - break; - } - } - if(monitorValues.length>0){ - videoQuery += monitorQuery; - videoQueryValues = videoQueryValues.concat(monitorValues); - } - videoQuery+=' ORDER BY `time` DESC'; - if(!d.videoLimit||d.videoLimit==''){ - d.videoLimit='100' - } - if(d.videoLimit!=='0'){ - videoQuery+=' LIMIT '+d.videoLimit - } - s.sqlQuery(videoQuery,videoQueryValues,function(err,r){ - if(err){ - console.log(videoQuery) - console.error('LINE 2416',err) - setTimeout(function(){ - getVideos(callback) - },2000) - }else{ - s.video('linkBuild',r,cn.auth) - callback({total:r.length,limit:d.videoLimit,videos:r}) - } - }) - } - getVideos(function(videos){ - getEvents(function(events){ - tx({ - f:'drawPowerVideoMainTimeLine', - videos:videos, - events:events - }) - }) - }) - break; - } - break; - case'control': - s.camera('control',d,function(resp){ - tx({f:'control',response:resp}) - }) - break; - case'jpeg_off': - delete(cn.jpeg_on); - if(cn.monitor_watching){ - Object.keys(cn.monitor_watching).forEach(function(n,v){ - v=cn.monitor_watching[n]; - cn.join('MON_STREAM_'+n); - }); - } - tx({f:'mode_jpeg_off'}) - break; - case'jpeg_on': - cn.jpeg_on=true; - if(cn.monitor_watching){ - Object.keys(cn.monitor_watching).forEach(function(n,v){ - v=cn.monitor_watching[n]; - cn.leave('MON_STREAM_'+n); - }); - } - tx({f:'mode_jpeg_on'}) - break; - case'watch_on': - if(!d.ke){d.ke=cn.ke} - s.init(0,{mid:d.id,ke:d.ke}); - if(!s.group[d.ke]||!s.group[d.ke].mon[d.id]||s.group[d.ke].mon[d.id].started===0){return false} - s.camera(d.ff,d,cn,tx) - cn.join('MON_'+d.id); - cn.join('DETECTOR_'+d.ke+d.id); - if(cn.jpeg_on!==true){ - cn.join('MON_STREAM_'+d.id); - } if(s.group[d.ke]&&s.group[d.ke].mon&&s.group[d.ke].mon[d.id]&&s.group[d.ke].mon[d.id].watch){ - - tx({f:'monitor_watch_on',id:d.id,ke:d.ke}) - s.tx({viewers:Object.keys(s.group[d.ke].mon[d.id].watch).length,ke:d.ke,id:d.id},'MON_'+d.id) - } - break; - case'watch_off': - if(!d.ke){d.ke=cn.ke;};cn.leave('MON_'+d.id);s.camera(d.ff,d,cn,tx); - s.tx({viewers:d.ob,ke:d.ke,id:d.id},'MON_'+d.id) - break; - case'start':case'stop': - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[cn.ke,d.id],function(err,r) { - if(r&&r[0]){r=r[0] - s.camera(d.ff,{type:r.type,url:s.init('url',r),id:d.id,mode:d.ff,ke:cn.ke}); - } - }) - break; - } - break; -// case'video': -// switch(d.ff){ -// case'fix': -// s.video('fix',d) -// break; -// } -// break; - case'ffprobe': - if(s.group[cn.ke].users[cn.auth]){ - switch(d.ff){ - case'stop': - exec('kill -9 '+s.group[cn.ke].users[cn.auth].ffprobe.pid,{detatched: true}) - break; - default: - if(s.group[cn.ke].users[cn.auth].ffprobe){ - return - } - s.group[cn.ke].users[cn.auth].ffprobe=1; - tx({f:'ffprobe_start'}) - exec('ffprobe '+('-v quiet -print_format json -show_format -show_streams '+d.query),function(err,data){ - tx({f:'ffprobe_data',data:data.toString('utf8')}) - delete(s.group[cn.ke].users[cn.auth].ffprobe) - tx({f:'ffprobe_stop'}) - }) - //auto kill in 30 seconds - setTimeout(function(){ - exec('kill -9 '+d.pid,{detached: true}) - },30000) - break; - } - } - break; - case'onvif': - d.ip=d.ip.replace(/ /g,''); - d.port=d.port.replace(/ /g,''); - if(d.ip===''){ - var interfaces = os.networkInterfaces(); - var addresses = []; - for (var k in interfaces) { - for (var k2 in interfaces[k]) { - var address = interfaces[k][k2]; - if (address.family === 'IPv4' && !address.internal) { - addresses.push(address.address); - } - } - } - d.arr=[] - addresses.forEach(function(v){ - if(v.indexOf('0.0.0')>-1){return false} - v=v.split('.'); - delete(v[3]); - v=v.join('.'); - d.arr.push(v+'1-'+v+'254') - }) - d.ip=d.arr.join(',') - } - if(d.port===''){ - d.port='80,8080,8000,7575,8081,554' - } - d.ip.split(',').forEach(function(v){ - if(v.indexOf('-')>-1){ - v=v.split('-'); - d.IP_RANGE_START = v[0], - d.IP_RANGE_END = v[1]; - }else{ - d.IP_RANGE_START = v; - d.IP_RANGE_END = v; - } - if(!d.IP_LIST){ - d.IP_LIST = s.ipRange(d.IP_RANGE_START,d.IP_RANGE_END); - }else{ - d.IP_LIST=d.IP_LIST.concat(s.ipRange(d.IP_RANGE_START,d.IP_RANGE_END)) - } - //check port - if(d.port.indexOf('-')>-1){ - d.port=d.port.split('-'); - d.PORT_RANGE_START = d.port[0]; - d.PORT_RANGE_END = d.port[1]; - d.PORT_LIST = s.portRange(d.PORT_RANGE_START,d.PORT_RANGE_END); - }else{ - d.PORT_LIST=d.port.split(',') - } - //check user name and pass - d.USERNAME=''; - if(d.user){ - d.USERNAME = d.user - } - d.PASSWORD=''; - if(d.pass){ - d.PASSWORD = d.pass - } - }) - d.cams=[] - d.IP_LIST.forEach(function(ip_entry,n) { - d.PORT_LIST.forEach(function(port_entry,nn) { - var device = new onvif.OnvifDevice({ - xaddr : 'http://' + ip_entry + ':' + port_entry + '/onvif/device_service', - user : d.USERNAME, - pass : d.PASSWORD - }) - device.init().then((info) => { - var data = { - f : 'onvif', - ip : ip_entry, - port : port_entry, - info : info - } - device.services.device.getSystemDateAndTime().then((date) => { - data.date = date - device.services.media.getStreamUri({ - ProfileToken : device.current_profile.token, - Protocol : 'RTSP' - }).then((stream) => { - data.uri = stream.data.GetStreamUriResponse.MediaUri.Uri - tx(data) - }).catch((error) => { -// console.log(error) - }); - }).catch((error) => { -// console.log(error) - }); - }).catch(function(error){ -// console.log(error) - }) - }); - }); -// tx({f:'onvif_end'}) - break; - } - }catch(er){ - s.systemLog('ERROR CATCH 1',er) - } - }else{ - tx({ok:false,msg:lang.NotAuthorizedText1}); - } - }); - // admin page socket functions - cn.on('super',function(d){ - if(!cn.init&&d.f=='init'){ - d.ok=s.superAuth({mail:d.mail,pass:d.pass},function(data){ - cn.uid=d.mail - cn.join('$'); - cn.ip=cn.request.connection.remoteAddress - s.log({ke:'$',mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.uid,ip:cn.ip}}) - cn.init='super'; - cn.mail=d.mail; - s.tx({f:'init_success',mail:d.mail},cn.id); - }) - if(d.ok===false){ - cn.disconnect(); - } - }else{ - if(cn.mail&&cn.init=='super'){ - switch(d.f){ - case'logs': - switch(d.ff){ - case'delete': - s.sqlQuery('DELETE FROM Logs WHERE ke=?',[d.ke]) - break; - } - break; - case'system': - switch(d.ff){ - case'update': - s.ffmpegKill() - s.systemLog('Shinobi ordered to update',{ - by:cn.mail, - ip:cn.ip - }) - var updateProcess = spawn('sh',(__dirname+'/UPDATE.sh').split(' '),{detached: true}) - updateProcess.stderr.on('data',function(data){ - s.systemLog('Update Info',data.toString()) - }) - updateProcess.stdout.on('data',function(data){ - s.systemLog('Update Info',data.toString()) - }) - break; - case'restart': - d.check=function(x){return d.target.indexOf(x)>-1} - if(d.check('system')){ - s.systemLog('Shinobi ordered to restart',{by:cn.mail,ip:cn.ip}) - s.ffmpegKill() - exec('pm2 restart '+__dirname+'/camera.js') - } - if(d.check('cron')){ - s.systemLog('Shinobi CRON ordered to restart',{by:cn.mail,ip:cn.ip}) - exec('pm2 restart '+__dirname+'/cron.js') - } - if(d.check('logs')){ - s.systemLog('Flush PM2 Logs',{by:cn.mail,ip:cn.ip}) - exec('pm2 flush') - } - break; - case'configure': - s.systemLog('conf.json Modified',{by:cn.mail,ip:cn.ip,old:jsonfile.readFileSync(location.config)}) - jsonfile.writeFile(location.config,d.data,{spaces: 2},function(){ - s.tx({f:'save_configuration'},cn.id) - }) - break; - } - break; - case'accounts': - switch(d.ff){ - case'register': - if(d.form.mail!==''&&d.form.pass!==''){ - if(d.form.pass===d.form.password_again){ - s.sqlQuery('SELECT * FROM Users WHERE mail=?',[d.form.mail],function(err,r) { - if(r&&r[0]){ - //found address already exists - d.msg='Email address is in use.'; - s.tx({f:'error',ff:'account_register',msg:d.msg},cn.id) - }else{ - //create new - //user id - d.form.uid=s.gid(); - //check to see if custom key set - if(!d.form.ke||d.form.ke===''){ - d.form.ke=s.gid() - }else{ - d.form.ke = d.form.ke.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '') - } - //write user to db - s.sqlQuery('INSERT INTO Users (ke,uid,mail,pass,details) VALUES (?,?,?,?,?)',[d.form.ke,d.form.uid,d.form.mail,s.md5(d.form.pass),d.form.details]) - s.tx({f:'add_account',details:d.form.details,ke:d.form.ke,uid:d.form.uid,mail:d.form.mail},'$'); - //init user - s.init('group',d.form) - } - }) - }else{ - d.msg=lang["Passwords Don't Match"]; - } - }else{ - d.msg=lang['Fields cannot be empty']; - } - if(d.msg){ - s.tx({f:'error',ff:'account_register',msg:d.msg},cn.id) - } - break; - case'edit': - s.sqlQuery('SELECT * FROM Users WHERE mail=?',[d.account.mail],function(err,r) { - if(r && r[0]){ - r = r[0] - var details = JSON.parse(r.details) - if(d.form.pass&&d.form.pass!==''){ - if(d.form.pass===d.form.password_again){ - d.form.pass=s.md5(d.form.pass); - }else{ - s.tx({f:'error',ff:'edit_account',msg:lang["Passwords Don't Match"]},cn.id) - return - } - }else{ - delete(d.form.pass); - } - delete(d.form.password_again); - d.keys=Object.keys(d.form); - d.set=[]; - d.values=[]; - d.keys.forEach(function(v,n){ - if(d.set==='ke'||d.set==='password_again'||!d.form[v]){return} - d.set.push(v+'=?') - if(v === 'details'){ - d.form[v] = JSON.stringify(Object.assign(details,JSON.parse(d.form[v]))) - } - d.values.push(d.form[v]) - }) - d.values.push(d.account.mail) - s.sqlQuery('UPDATE Users SET '+d.set.join(',')+' WHERE mail=?',d.values,function(err,r) { - if(err){ - s.tx({f:'error',ff:'edit_account',msg:lang.AccountEditText1},cn.id) - return - } - s.tx({f:'edit_account',form:d.form,ke:d.account.ke,uid:d.account.uid},'$'); - delete(s.group[d.account.ke].init); - s.init('apps',d.account) - }) - } - }) - break; - case'delete': - s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[d.account.uid,d.account.ke,d.account.mail]) - s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[d.account.uid,d.account.ke]) - s.tx({f:'delete_account',ke:d.account.ke,uid:d.account.uid,mail:d.account.mail},'$'); - break; - } - break; - } - } - } - }) - // admin page socket functions - cn.on('a',function(d){ - if(!cn.init&&d.f=='init'){ - s.sqlQuery('SELECT * FROM Users WHERE auth=? AND uid=?',[d.auth,d.uid],function(err,r){ - if(r&&r[0]){ - r=r[0]; - if(!s.group[d.ke]){s.group[d.ke]={users:{}}} - if(!s.group[d.ke].users[d.auth]){s.group[d.ke].users[d.auth]={cnid:cn.id}} - try{s.group[d.ke].users[d.auth].details=JSON.parse(r.details)}catch(er){} - cn.join('ADM_'+d.ke); - cn.ke=d.ke; - cn.uid=d.uid; - cn.auth=d.auth; - cn.init='admin'; - }else{ - cn.disconnect(); - } - }) - }else{ - s.auth({auth:d.auth,ke:d.ke,id:d.id,ip:cn.request.connection.remoteAddress},function(user){ - if(!user.details.sub){ - switch(d.f){ - case'accounts': - switch(d.ff){ - case'edit': - d.keys=Object.keys(d.form); - d.condition=[]; - d.value=[]; - d.keys.forEach(function(v){ - d.condition.push(v+'=?') - d.value.push(d.form[v]) - }) - d.value=d.value.concat([d.ke,d.$uid]) - s.sqlQuery("UPDATE Users SET "+d.condition.join(',')+" WHERE ke=? AND uid=?",d.value) - s.tx({f:'edit_sub_account',ke:d.ke,uid:d.$uid,mail:d.mail,form:d.form},'ADM_'+d.ke); - s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[d.ke,d.$uid],function(err,rows){ - if(rows && rows[0]){ - rows.forEach(function(row){ - delete(s.api[row.code]) - }) - } - }) - break; - case'delete': - s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[d.$uid,d.ke,d.mail]) - s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[d.ke,d.$uid],function(err,rows){ - if(rows && rows[0]){ - rows.forEach(function(row){ - delete(s.api[row.code]) - }) - s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[d.$uid,d.ke]) - } - }) - s.tx({f:'delete_sub_account',ke:d.ke,uid:d.$uid,mail:d.mail},'ADM_'+d.ke); - break; - } - break; - } - } - }) - } - }) - //functions for webcam recorder - cn.on('r',function(d){ - if(!cn.ke&&d.f==='init'){ - s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { - if(r&&r[0]){ - r=r[0] - cn.ke=d.ke,cn.uid=d.uid,cn.auth=d.auth; - if(!s.group[d.ke])s.group[d.ke]={}; - if(!s.group[d.ke].users)s.group[d.ke].users={}; - if(!s.group[d.ke].dashcamUsers)s.group[d.ke].dashcamUsers={}; - s.group[d.ke].users[d.auth]={ - cnid:cn.id, - ke : d.ke, - uid:r.uid, - mail:r.mail, - details:JSON.parse(r.details), - logged_in_at:s.timeObject(new Date).format(), - login_type:'Streamer' - } - s.group[d.ke].dashcamUsers[d.auth] = s.group[d.ke].users[d.auth] - if(s.group[d.ke].mon){ - Object.keys(s.group[d.ke].mon).forEach(function(monitorId){ - var dataToClient = { - f : 'disable_stream', - mid : monitorId, - ke : d.ke - } - var mon = s.group[d.ke].mon[monitorId] - if(s.group[d.ke].mon_conf[monitorId].type === 'dashcam'){ - if(mon.allowStdinWrite === true){ - dataToClient.f = 'enable_stream' - } - s.tx(dataToClient,cn.id) - } - }) - } - } - }) - }else{ - if(s.group[d.ke] && s.group[d.ke].mon[d.mid]){ - if(s.group[d.ke].mon[d.mid].allowStdinWrite === true){ - switch(d.f){ - case'monitor_chunk': - if(s.group[d.ke].mon[d.mid].started!==1 || !s.group[d.ke].mon[d.mid].spawn || !s.group[d.ke].mon[d.mid].spawn.stdin){ - s.tx({error:'Not Started'},cn.id); - return false - }; - s.group[d.ke].mon[d.mid].spawn.stdin.write(new Buffer(d.chunk, "binary")); - break; - case'monitor_frame': - if(s.group[d.ke].mon[d.mid].started!==1){ - s.tx({error:'Not Started'},cn.id); - return false - }; - s.group[d.ke].mon[d.mid].spawn.stdin.write(d.frame); - break; - } - }else{ - s.tx({error:'Cannot Write Yet'},cn.id) - } - }else{ - s.tx({error:'Non Existant Monitor'},cn.id) - } - } - }) - //embed functions - cn.on('e', function (d) { - tx=function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);} - switch(d.f){ - case'init': - if(!s.group[d.ke]||!s.group[d.ke].mon[d.id]||s.group[d.ke].mon[d.id].started===0){return false} - s.auth({auth:d.auth,ke:d.ke,id:d.id,ip:cn.request.connection.remoteAddress},function(user){ - cn.embedded=1; - cn.ke=d.ke; - if(!cn.mid){cn.mid={}} - cn.mid[d.id]={}; -// if(!s.group[d.ke].embed){s.group[d.ke].embed={}} -// if(!s.group[d.ke].embed[d.mid]){s.group[d.ke].embed[d.mid]={}} -// s.group[d.ke].embed[d.mid][cn.id]={} - - s.camera('watch_on',d,cn,tx) - cn.join('MON_'+d.id); - cn.join('MON_STREAM_'+d.id); - cn.join('DETECTOR_'+d.ke+d.id); - cn.join('STR_'+d.ke); - if(s.group[d.ke]&&s.group[d.ke].mon[d.id]&&s.group[d.ke].mon[d.id].watch){ - - tx({f:'monitor_watch_on',id:d.id,ke:d.ke},'MON_'+d.id) - s.tx({viewers:Object.keys(s.group[d.ke].mon[d.id].watch).length,ke:d.ke,id:d.id},'MON_'+d.id) - } - }); - break; - } - }) - //functions for retrieving cron announcements - cn.on('cron',function(d){ - if(d.f==='init'){ - if(config.cron.key){ - if(config.cron.key===d.cronKey){ - s.cron={started:moment(),last_run:moment(),id:cn.id}; - }else{ - cn.disconnect() - } - }else{ - s.cron={started:moment(),last_run:moment(),id:cn.id}; - } - }else{ - if(s.cron&&cn.id===s.cron.id){ - delete(d.cronKey) - switch(d.f){ - case'filters': - s.filterEvents(d.ff,d); - break; - case's.tx': - s.tx(d.data,d.to) - break; - case's.video': - s.video(d.data,d.file) - break; - case'start':case'end': - d.mid='_cron';s.log(d,{type:'cron',msg:d.msg}) - break; - default: - s.systemLog('CRON : ',d) - break; - } - }else{ - cn.disconnect() - } - } - }) - cn.on('disconnect', function () { - if(cn.socketVideoStream){ - cn.closeSocketVideoStream() - return - } - if(cn.ke){ - if(cn.monitor_watching){ - cn.monitor_count=Object.keys(cn.monitor_watching) - if(cn.monitor_count.length>0){ - cn.monitor_count.forEach(function(v){ - s.camera('watch_off',{id:v,ke:cn.monitor_watching[v].ke},s.cn(cn)) - }) - } - }else if(!cn.embedded){ - if(s.group[cn.ke].users[cn.auth].login_type==='Dashboard'){ - s.tx({f:'user_status_change',ke:cn.ke,uid:cn.uid,status:0}) - } - s.log({ke:cn.ke,mid:'$USER'},{type:lang['Websocket Disconnected'],msg:{mail:s.group[cn.ke].users[cn.auth].mail,id:cn.uid,ip:cn.ip}}) - delete(s.group[cn.ke].users[cn.auth]); - if(s.group[cn.ke].dashcamUsers && s.group[cn.ke].dashcamUsers[cn.auth])delete(s.group[cn.ke].dashcamUsers[cn.auth]); - } - } - if(cn.pluginEngine){ - s.connectedPlugins[cn.pluginEngine].plugged=false - s.tx({f:'plugin_engine_unplugged',plug:cn.pluginEngine},'CPU') - delete(s.api[cn.pluginEngine]) - } - if(cn.cron){ - delete(s.cron); - } - if(cn.ocv){ - s.tx({f:'detector_unplugged',plug:s.ocv.plug},'CPU') - delete(s.ocv); - delete(s.api[cn.id]) - } - }) -}); -//Authenticator functions -s.api={}; -//auth handler -//params = parameters -//cb = callback -//res = response, only needed for express (http server) -//request = request, only needed for express (http server) -s.checkChildProxy = function(params,cb,res,req){ - if(s.group[params.ke] && s.group[params.ke].mon[params.id] && s.group[params.ke].mon[params.id].childNode){ - var url = 'http://' + s.group[params.ke].mon[params.id].childNode// + req.originalUrl - proxy.web(req, res, { target: url }) - }else{ - cb() - } -} -//auth handler -//params = parameters -//cb = callback -//res = response, only needed for express (http server) -//request = request, only needed for express (http server) -s.auth = function(params,cb,res,req){ - if(req){ - //express (http server) use of auth function - params.ip=req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress; - var failed=function(){ - if(!req.ret){req.ret={ok:false}} - req.ret.msg=lang['Not Authorized']; - res.end(s.s(req.ret, null, 3)); - } - }else{ - //socket.io use of auth function - var failed=function(){ - //maybe log - } - } - var clearAfterTime=function(){ - //remove temp key from memory - clearTimeout(s.api[params.auth].timeout) - s.api[params.auth].timeout=setTimeout(function(){ - delete(s.api[params.auth]) - },1000*60*5) - } - //check IP address of connecting user - var finish=function(user){ - if(s.api[params.auth].ip.indexOf('0.0.0.0')>-1||s.api[params.auth].ip.indexOf(params.ip)>-1){ - cb(user); - }else{ - failed(); - } - } - //check if auth key is user's temporary session key - if(s.group[params.ke]&&s.group[params.ke].users&&s.group[params.ke].users[params.auth]){ - s.group[params.ke].users[params.auth].permissions={}; - cb(s.group[params.ke].users[params.auth]); - }else{ - //check if key is already in memory to save query time - if(s.api[params.auth]&&s.api[params.auth].details){ - finish(s.api[params.auth]); - if(s.api[params.auth].timeout){ - clearAfterTime() - } - }else{ - //no key in memory, query db to see if key exists - //check if using username and password in plain text or md5 - if(params.username&¶ms.username!==''&¶ms.password&¶ms.password!==''){ - s.sqlQuery('SELECT * FROM Users WHERE mail=? AND (pass=? OR pass=?)',[params.username,params.password,s.md5(params.password)],function(err,r){ - if(r&&r[0]){ - r=r[0]; - r.ip='0.0.0.0'; - r.auth = s.gid(20); - params.auth = r.auth; - r.details=JSON.parse(r.details); - r.permissions = {}; - s.api[r.auth]=r; - clearAfterTime(); - finish(r); - }else{ - failed(); - } - }) - }else{ - //not using plain login - s.sqlQuery('SELECT * FROM API WHERE code=? AND ke=?',[params.auth,params.ke],function(err,r){ - if(r&&r[0]){ - r=r[0]; - s.api[params.auth]={ip:r.ip,uid:r.uid,ke:r.ke,permissions:JSON.parse(r.details),details:{}}; - s.sqlQuery('SELECT details FROM Users WHERE uid=? AND ke=?',[r.uid,r.ke],function(err,rr){ - if(rr&&rr[0]){ - rr=rr[0]; - try{ - s.api[params.auth].mail=rr.mail - s.api[params.auth].details=JSON.parse(rr.details) - s.api[params.auth].lang=s.getLanguageFile(s.api[params.auth].details.lang) - }catch(er){} - } - finish(s.api[params.auth]); - }) - }else{ - s.sqlQuery('SELECT * FROM Users WHERE auth=? AND ke=?',[params.auth,params.ke],function(err,r){ - if(r&&r[0]){ - r=r[0]; - r.ip='0.0.0.0' - s.api[params.auth]=r - s.api[params.auth].details=JSON.parse(r.details) - s.api[params.auth].permissions={} - clearAfterTime() - finish(r) - }else{ - failed(); - } - }) - } - }) - } - } - } -} -//super user authentication handler -s.superAuth=function(x,callback){ - req={}; - req.super=require(location.super); - req.super.forEach(function(v,n){ - if(x.md5===true){ - x.pass=s.md5(x.pass); - } - if(x.mail.toLowerCase()===v.mail.toLowerCase()&&x.pass===v.pass){ - req.found=1; - if(x.users===true){ - s.sqlQuery('SELECT * FROM Users WHERE details NOT LIKE ?',['%"sub"%'],function(err,r) { - callback({$user:v,users:r,config:config,lang:lang}) - }) - }else{ - callback({$user:v,config:config,lang:lang}) - } - } - }) - if(req.found!==1){ - return false; - }else{ - return true; - } -} -//get page URL -if(!config.baseURL){ - config.baseURL = "" -}else if(config.baseURL !== ''){ - config.baseURL = s.checkCorrectPathEnding(config.baseURL) -} -s.getOriginalUrl = function(req){ - var url - if(config.baseURL || config.baseURL === ''){ - url = config.baseURL - }else{ - url = req.protocol + '://' + req.get('host') + '/' - } - return url -} -//Render Configurations - Web Paths -if(config.webPaths === undefined){config.webPaths={}} - //main access URI - if(config.webPaths.home === undefined){config.webPaths.home='/'} - //Super User URI - if(config.webPaths.super === undefined){config.webPaths.super='/super'} - //Admin URI - if(config.webPaths.admin === undefined){config.webPaths.admin='/admin'} - //API Prefix - if(config.webPaths.apiPrefix === undefined){config.webPaths.apiPrefix='/'}else{config.webPaths.apiPrefix = s.checkCorrectPathEnding(config.webPaths.apiPrefix)} -//Render Configurations - Page Render Paths -if(config.renderPaths === undefined){config.renderPaths={}} - //login page - if(config.renderPaths.index === undefined){config.renderPaths.index='pages/index'} - //dashboard page - if(config.renderPaths.home === undefined){config.renderPaths.home='pages/home'} - //sub-account administration page - if(config.renderPaths.admin === undefined){config.renderPaths.admin='pages/admin'} - //superuser page - if(config.renderPaths.super === undefined){config.renderPaths.super='pages/super'} - //2-Factor Auth page - if(config.renderPaths.factorAuth === undefined){config.renderPaths.factorAuth='pages/factor'} - //Streamer v1 (Dashcam Prototype) page - if(config.renderPaths.streamer === undefined){config.renderPaths.streamer='pages/streamer'} - //Streamer v2 (Dashcam) page - if(config.renderPaths.dashcam === undefined){config.renderPaths.dashcam='pages/dashcam'} - //embeddable widget page - if(config.renderPaths.embed === undefined){config.renderPaths.embed='pages/embed'} - //mjpeg full screen page - if(config.renderPaths.mjpeg === undefined){config.renderPaths.mjpeg='pages/mjpeg'} - //gridstack only page - if(config.renderPaths.grid === undefined){config.renderPaths.grid='pages/grid'} - //slick.js (cycle) page - if(config.renderPaths.cycle === undefined){config.renderPaths.cycle='pages/cycle'} -////Pages -app.enable('trust proxy'); -app.use('/libs',express.static(__dirname + '/web/libs')); -app.use(bodyParser.json()); -app.use(bodyParser.urlencoded({extended: true})); -app.set('views', __dirname + '/web'); -app.set('view engine','ejs'); -//add template handler -if(config.renderPaths.handler!==undefined){require(__dirname+'/web/'+config.renderPaths.handler+'.js').addHandlers(s,app,io)} - -//logout -app.get(config.webPaths.apiPrefix+':auth/logout/:ke/:id', function (req,res){ - if(s.group[req.params.ke]&&s.group[req.params.ke].users[req.params.auth]){ - delete(s.api[req.params.auth]); - delete(s.group[req.params.ke].users[req.params.auth]); - s.sqlQuery("UPDATE Users SET auth=? WHERE auth=? AND ke=? AND uid=?",['',req.params.auth,req.params.ke,req.params.id]) - res.end(s.s({ok:true,msg:'You have been logged out, session key is now inactive.'}, null, 3)) - }else{ - res.end(s.s({ok:false,msg:'This group key does not exist or this user is not logged in.'}, null, 3)) - } -}); -//main page -app.get(config.webPaths.home, function (req,res){ - res.render(config.renderPaths.index,{lang:lang,config:config,screen:'dashboard',originalURL:s.getOriginalUrl(req)},function(err,html){ - if(err){ - s.systemLog(err) - } - res.end(html) - }) -}); -//admin page -app.get(config.webPaths.admin, function (req,res){ - res.render(config.renderPaths.index,{lang:lang,config:config,screen:'admin',originalURL:s.getOriginalUrl(req)},function(err,html){ - if(err){ - s.systemLog(err) - } - res.end(html) - }) -}); -//super page -app.get(config.webPaths.super, function (req,res){ - - res.render(config.renderPaths.index,{lang:lang,config:config,screen:'super',originalURL:s.getOriginalUrl(req)},function(err,html){ - if(err){ - s.systemLog(err) - } - res.end(html) - }) -}); -//update server -app.get(config.webPaths.apiPrefix+':auth/update/:key', function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - req.fn=function(user){ - if(!config.updateKey){ - req.ret.msg=user.lang.updateKeyText1; - return; - } - if(req.params.key===config.updateKey){ - req.ret.ok=true; - exec('chmod +x '+__dirname+'/UPDATE.sh&&'+__dirname+'/UPDATE.sh',{detached: true}) - }else{ - req.ret.msg=user.lang.updateKeyText2; - } - res.end(s.s(req.ret, null, 3)); - } - s.auth(req.params,req.fn,res,req); -}); -//get user details by API key -app.get(config.webPaths.apiPrefix+':auth/userInfo/:ke',function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - req.ret.ok=true - req.ret.user=user - res.end(s.s(req.ret, null, 3)); - },res,req); -}) -//register function -app.post(config.webPaths.apiPrefix+':auth/register/:ke/:uid',function (req,res){ - req.resp={ok:false}; - res.setHeader('Content-Type', 'application/json'); - s.auth(req.params,function(user){ - if(user.details.sub){ - res.end(user.lang['Not Permitted']) - return - } - s.sqlQuery('SELECT * FROM Users WHERE uid=? AND ke=? AND details NOT LIKE ? LIMIT 1',[req.params.uid,req.params.ke,'%"sub"%'],function(err,u) { - if(u&&u[0]){ - if(req.body.mail!==''&&req.body.pass!==''){ - if(req.body.pass===req.body.password_again){ - s.sqlQuery('SELECT * FROM Users WHERE mail=?',[req.body.mail],function(err,r) { - if(r&&r[0]){//found one exist - req.resp.msg='Email address is in use.'; - }else{//create new - req.resp.msg='New Account Created';req.resp.ok=true; - req.gid=s.gid(); - req.body.details='{"sub":"1","allmonitors":"1"}'; - s.sqlQuery('INSERT INTO Users (ke,uid,mail,pass,details) VALUES (?,?,?,?,?)',[req.params.ke,req.gid,req.body.mail,s.md5(req.body.pass),req.body.details]) - s.tx({f:'add_sub_account',details:req.body.details,ke:req.params.ke,uid:req.gid,mail:req.body.mail},'ADM_'+req.params.ke); - } - res.end(s.s(req.resp,null,3)); - }) - }else{ - req.resp.msg=user.lang['Passwords Don\'t Match']; - } - }else{ - req.resp.msg=user.lang['Fields cannot be empty']; - } - }else{ - req.resp.msg=user.lang['Not an Administrator Account']; - } - if(req.resp.msg){ - res.end(s.s(req.resp,null,3)); - } - }) - },res,req); -}) -//login function -s.deleteFactorAuth=function(r){ - delete(s.factorAuth[r.ke][r.uid]) - if(Object.keys(s.factorAuth[r.ke]).length===0){ - delete(s.factorAuth[r.ke]) - } -} -app.post([config.webPaths.home,s.checkCorrectPathEnding(config.webPaths.home)+':screen'],function (req,res){ - req.ip=req.headers['cf-connecting-ip']||req.headers["CF-Connecting-IP"]||req.headers["'x-forwarded-for"]||req.connection.remoteAddress; - if(req.query.json=='true'){ - res.header("Access-Control-Allow-Origin",req.headers.origin); - } - req.renderFunction=function(focus,data){ - if(req.query.json=='true'){ - delete(data.config) - data.ok=true; - res.setHeader('Content-Type', 'application/json'); - res.end(s.s(data, null, 3)) - }else{ - data.originalURL = s.getOriginalUrl(req) - data.screen=req.params.screen - res.render(focus,data,function(err,html){ - if(err){ - s.systemLog(err) - } - res.end(html) - }); - } - } - req.failed=function(board){ - if(req.query.json=='true'){ - res.setHeader('Content-Type', 'application/json'); - res.end(s.s({ok:false}, null, 3)) - }else{ - res.render(config.renderPaths.index,{failedLogin:true,lang:lang,config:config,screen:req.params.screen,originalURL:s.getOriginalUrl(req)},function(err,html){ - if(err){ - s.systemLog(err) - } - res.end(html); - }); - } - req.logTo={ke:'$',mid:'$USER'} - req.logData={type:lang['Authentication Failed'],msg:{for:board,mail:req.body.mail,ip:req.ip}} - if(board==='super'){ - s.log(req.logTo,req.logData) - }else{ - s.sqlQuery('SELECT ke,uid,details FROM Users WHERE mail=?',[req.body.mail],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details); - r.lang=s.getLanguageFile(r.details.lang) - req.logData.id=r.uid - req.logData.type=r.lang['Authentication Failed'] - req.logTo.ke=r.ke - } - s.log(req.logTo,req.logData) - }) - } - } - req.fn=function(r){ - switch(req.body.function){ - case'cam': - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND type=?',[r.ke,"dashcam"],function(err,rr){ - req.resp.mons=rr; - req.renderFunction(config.renderPaths.dashcam,{$user:req.resp,lang:r.lang,define:s.getDefinitonFile(r.details.lang)}); - }) - break; - case'streamer': - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND type=?',[r.ke,"socket"],function(err,rr){ - req.resp.mons=rr; - req.renderFunction(config.renderPaths.streamer,{$user:req.resp,lang:r.lang,define:s.getDefinitonFile(r.details.lang)}); - }) - break; - case'admin': - if(!r.details.sub){ - s.sqlQuery('SELECT uid,mail,details FROM Users WHERE ke=? AND details LIKE \'%"sub"%\'',[r.ke],function(err,rr) { - s.sqlQuery('SELECT * FROM Monitors WHERE ke=?',[r.ke],function(err,rrr) { - req.renderFunction(config.renderPaths.admin,{$user:req.resp,$subs:rr,$mons:rrr,lang:r.lang,define:s.getDefinitonFile(r.details.lang)}); - }) - }) - }else{ - //not admin user - req.renderFunction(config.renderPaths.home,{$user:req.resp,config:config,lang:r.lang,define:s.getDefinitonFile(r.details.lang),addStorage:s.dir.addStorage,fs:fs,__dirname:__dirname}); - } - break; - default: - req.renderFunction(config.renderPaths.home,{$user:req.resp,config:config,lang:r.lang,define:s.getDefinitonFile(r.details.lang),addStorage:s.dir.addStorage,fs:fs,__dirname:__dirname}); - break; - } - s.log({ke:r.ke,mid:'$USER'},{type:r.lang['New Authentication Token'],msg:{for:req.body.function,mail:r.mail,id:r.uid,ip:req.ip}}) - // res.end(); - } - if(req.body.mail&&req.body.pass){ - req.default=function(){ - s.sqlQuery('SELECT * FROM Users WHERE mail=? AND pass=?',[req.body.mail,s.md5(req.body.pass)],function(err,r) { - req.resp={ok:false}; - if(!err&&r&&r[0]){ - r=r[0];r.auth=s.md5(s.gid()); - s.sqlQuery("UPDATE Users SET auth=? WHERE ke=? AND uid=?",[r.auth,r.ke,r.uid]) - req.resp={ok:true,auth_token:r.auth,ke:r.ke,uid:r.uid,mail:r.mail,details:r.details}; - r.details=JSON.parse(r.details); - r.lang=s.getLanguageFile(r.details.lang) - req.factorAuth=function(cb){ - if(r.details.factorAuth==="1"){ - if(!r.details.acceptedMachines||!(r.details.acceptedMachines instanceof Object)){ - r.details.acceptedMachines={} - } - if(!r.details.acceptedMachines[req.body.machineID]){ - req.complete=function(){ - s.factorAuth[r.ke][r.uid].function = req.body.function - s.factorAuth[r.ke][r.uid].info = req.resp - clearTimeout(s.factorAuth[r.ke][r.uid].expireAuth) - s.factorAuth[r.ke][r.uid].expireAuth=setTimeout(function(){ - s.deleteFactorAuth(r) - },1000*60*15) - req.renderFunction(config.renderPaths.factorAuth,{$user:req.resp,lang:r.lang}) - } - if(!s.factorAuth[r.ke]){s.factorAuth[r.ke]={}} - if(!s.factorAuth[r.ke][r.uid]){ - s.factorAuth[r.ke][r.uid]={key:s.nid(),user:r} - if(r.details.factor_mail !== '0'){ - var mailOptions = { - from: config.mail.from, - to: r.mail, - subject: r.lang['2-Factor Authentication'], - html: r.lang['Enter this code to proceed']+' '+s.factorAuth[r.ke][r.uid].key+'. '+r.lang.FactorAuthText1, - }; - nodemailer.sendMail(mailOptions, (error, info) => { - if (error) { - s.systemLog(r.lang.MailError,error) - return - } - }); - } - if(r.details.factor_discord === '1'){ - s.discordMsg({ - author: { - name: r.lang['2-Factor Authentication'], - icon_url: config.iconURL - }, - title: r.lang['Enter this code to proceed'], - description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+r.lang.FactorAuthText1, - fields: [], - timestamp: new Date(), - footer: { - icon_url: config.iconURL, - text: "Shinobi Systems" - } - },[],r.ke) - } - req.complete() - }else{ - req.complete() - } - }else{ - req.fn(r) - } - }else{ - req.fn(r) - } - } - if(r.details.sub){ - s.sqlQuery('SELECT details FROM Users WHERE ke=? AND details NOT LIKE ?',[r.ke,'%"sub"%'],function(err,rr) { - rr=rr[0]; - rr.details=JSON.parse(rr.details); - r.details.mon_groups=rr.details.mon_groups; - req.resp.details=JSON.stringify(r.details); - req.factorAuth() - }) - }else{ - req.factorAuth() - } - }else{ - req.failed(req.body.function) - } - }) - } - if(LdapAuth&&req.body.function==='ldap'&&req.body.key!==''){ - s.sqlQuery('SELECT * FROM Users WHERE ke=? AND details NOT LIKE ?',[req.body.key,'%"sub"%'],function(err,r) { - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - r.lang=s.getLanguageFile(r.details.lang) - if(r.details.use_ldap!=='0'&&r.details.ldap_enable==='1'&&r.details.ldap_url&&r.details.ldap_url!==''){ - req.mailArray={} - req.body.mail.split(',').forEach(function(v){ - v=v.split('=') - req.mailArray[v[0]]=v[1] - }) - if(!r.details.ldap_bindDN||r.details.ldap_bindDN===''){ - r.details.ldap_bindDN=req.body.mail - } - if(!r.details.ldap_bindCredentials||r.details.ldap_bindCredentials===''){ - r.details.ldap_bindCredentials=req.body.pass - } - if(!r.details.ldap_searchFilter||r.details.ldap_searchFilter===''){ - r.details.ldap_searchFilter=req.body.mail - if(req.mailArray.cn){ - r.details.ldap_searchFilter='cn='+req.mailArray.cn - } - if(req.mailArray.uid){ - r.details.ldap_searchFilter='uid='+req.mailArray.uid - } - }else{ - r.details.ldap_searchFilter=r.details.ldap_searchFilter.replace('{{username}}',req.body.mail) - } - if(!r.details.ldap_searchBase||r.details.ldap_searchBase===''){ - r.details.ldap_searchBase='dc=test,dc=com' - } - req.auth = new LdapAuth({ - url:r.details.ldap_url, - bindDN:r.details.ldap_bindDN, - bindCredentials:r.details.ldap_bindCredentials, - searchBase:r.details.ldap_searchBase, - searchFilter:'('+r.details.ldap_searchFilter+')', - reconnect:true - }); - req.auth.on('error', function (err) { - console.error('LdapAuth: ', err); - }); - - req.auth.authenticate(req.body.mail, req.body.pass, function(err, user) { - if(user){ - //found user - if(!user.uid){ - user.uid=s.gid() - } - req.resp={ - ke:req.body.key, - uid:user.uid, - auth:s.md5(s.gid()), - mail:user.mail, - pass:s.md5(req.body.pass), - details:JSON.stringify({ - sub:'1', - ldap:'1', - allmonitors:'1', - filter: {} - }) - } - user.post=[] - Object.keys(req.resp).forEach(function(v){ - user.post.push(req.resp[v]) - }) - s.log({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP Success'],msg:{user:user}}) - s.sqlQuery('SELECT * FROM Users WHERE ke=? AND mail=?',[req.body.key,user.cn],function(err,rr){ - if(rr&&rr[0]){ - //already registered - rr=rr[0] - req.resp=rr; - rr.details=JSON.parse(rr.details) - req.resp.lang=s.getLanguageFile(rr.details.lang) - s.log({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP User Authenticated'],msg:{user:user,shinobiUID:rr.uid}}) - s.sqlQuery("UPDATE Users SET auth=? WHERE ke=? AND uid=?",[req.resp.auth,req.resp.ke,rr.uid]) - }else{ - //new ldap login - s.log({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP User is New'],msg:{info:r.lang['Creating New Account'],user:user}}) - req.resp.lang=r.lang - s.sqlQuery('INSERT INTO Users (ke,uid,auth,mail,pass,details) VALUES (?,?,?,?,?,?)',user.post) - } - req.resp.details=JSON.stringify(req.resp.details) - req.resp.auth_token=req.resp.auth - req.resp.ok=true - req.fn(req.resp) - }) - return - } - s.log({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP Failed'],msg:{err:err}}) - //no user - req.default() - }); - - req.auth.close(function(err) { - - }) - }else{ - req.default() - } - }else{ - req.default() - } - }) - }else{ - if(req.body.function==='super'){ - if(!fs.existsSync(location.super)){ - res.end(lang.superAdminText) - return - } - req.ok=s.superAuth({mail:req.body.mail,pass:req.body.pass,users:true,md5:true},function(data){ - s.sqlQuery('SELECT * FROM Logs WHERE ke=? ORDER BY `time` DESC LIMIT 30',['$'],function(err,r) { - if(!r){ - r=[] - } - data.Logs=r; - fs.readFile(location.config,'utf8',function(err,file){ - data.plainConfig=JSON.parse(file) - req.renderFunction(config.renderPaths.super,data); - }) - }) - }) - if(req.ok===false){ - req.failed(req.body.function) - } - }else{ - req.default() - } - } - }else{ - if(req.body.machineID&&req.body.factorAuthKey){ - if(s.factorAuth[req.body.ke]&&s.factorAuth[req.body.ke][req.body.id]&&s.factorAuth[req.body.ke][req.body.id].key===req.body.factorAuthKey){ - if(s.factorAuth[req.body.ke][req.body.id].key===req.body.factorAuthKey){ - if(req.body.remember==="1"){ - req.details=JSON.parse(s.factorAuth[req.body.ke][req.body.id].info.details) - req.lang=s.getLanguageFile(req.details.lang) - if(!req.details.acceptedMachines||!(req.details.acceptedMachines instanceof Object)){ - req.details.acceptedMachines={} - } - if(!req.details.acceptedMachines[req.body.machineID]){ - req.details.acceptedMachines[req.body.machineID]={} - s.sqlQuery("UPDATE Users SET details=? WHERE ke=? AND uid=?",[s.s(req.details),req.body.ke,req.body.id]) - } - } - req.body.function = s.factorAuth[req.body.ke][req.body.id].function - req.resp = s.factorAuth[req.body.ke][req.body.id].info - req.fn(s.factorAuth[req.body.ke][req.body.id].user) - }else{ - req.renderFunction(config.renderPaths.factorAuth,{$user:s.factorAuth[req.body.ke][req.body.id].info,lang:req.lang}); - res.end(); - } - }else{ - req.failed(lang['2-Factor Authentication']) - } - }else{ - req.failed(lang['2-Factor Authentication']) - } - } -}); -// Get HLS stream (m3u8) -app.get([config.webPaths.apiPrefix+':auth/hls/:ke/:id/:file',config.webPaths.apiPrefix+':auth/hls/:ke/:id/:channel/:file'], function (req,res){ - res.header("Access-Control-Allow-Origin",req.headers.origin); - req.fn=function(user){ - s.checkChildProxy(req.params,function(){ - 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; - }else{ - req.dir+=req.params.file; - } - res.on('finish',function(){res.end();}); - if (fs.existsSync(req.dir)){ - fs.createReadStream(req.dir).pipe(res); - }else{ - res.end(lang['File Not Found']) - } - },res,req) - } - s.auth(req.params,req.fn,res,req); -}); -//Get JPEG snap -app.get(config.webPaths.apiPrefix+':auth/jpeg/:ke/:id/s.jpg', function(req,res){ - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - s.checkChildProxy(req.params,function(){ - if(user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors&&user.details.monitors.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) - return - } - req.dir=s.dir.streams+req.params.ke+'/'+req.params.id+'/s.jpg'; - res.writeHead(200, { - 'Content-Type': 'image/jpeg', - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache' - }); - res.on('finish',function(){res.end();}); - if (fs.existsSync(req.dir)){ - fs.createReadStream(req.dir).pipe(res); - }else{ - fs.createReadStream(config.defaultMjpeg).pipe(res); - } - },res,req); - },res,req); -}); -//Get FLV stream -app.get([config.webPaths.apiPrefix+':auth/flv/:ke/:id/s.flv',config.webPaths.apiPrefix+':auth/flv/:ke/:id/:channel/s.flv'], function(req,res) { - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - s.checkChildProxy(req.params,function(){ - var Emitter,chunkChannel - if(!req.params.channel){ - Emitter = s.group[req.params.ke].mon[req.params.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[req.params.ke].mon[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition] - chunkChannel = parseInt(req.params.channel)+config.pipeAddition - } - if(s.group[req.params.ke].mon[req.params.id].firstStreamChunk[chunkChannel]){ - //variable name of contentWriter - var contentWriter - //set headers - res.setHeader('Content-Type', 'video/x-flv'); - res.setHeader('Access-Control-Allow-Origin','*'); - //write first frame on stream - res.write(s.group[req.params.ke].mon[req.params.id].firstStreamChunk[chunkChannel]) - //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) - }) - }else{ - res.setHeader('Content-Type', 'application/json'); - res.end(s.s({ok:false,msg:'FLV not started or not ready'},null,3)) - } - },res,req) - },res,req) -}) -//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) { - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - s.checkChildProxy(req.params,function(){ - var Emitter,chunkChannel - if(!req.params.channel){ - Emitter = s.group[req.params.ke].mon[req.params.id].emitter - chunkChannel = 'MAIN' - }else{ - Emitter = s.group[req.params.ke].mon[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','*'); - //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) - }) - },res,req) - },res,req) -}) -//montage - stand alone squished view with gridstackjs -app.get([ - config.webPaths.apiPrefix+':auth/grid/:ke', - config.webPaths.apiPrefix+':auth/grid/:ke/:group', - config.webPaths.apiPrefix+':auth/cycle/:ke', - config.webPaths.apiPrefix+':auth/cycle/:ke/:group' -], function(req,res) { - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - if(user.permissions.get_monitors==="0"){ - res.end(user.lang['Not Permitted']) - return - } - - req.params.protocol=req.protocol; - req.sql='SELECT * FROM Monitors WHERE mode!=? AND mode!=? AND ke=?';req.ar=['stop','idle',req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end(user.lang['There are no monitors that you can view with this account.']); - return; - } - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(req.params.group){ - var filteredByGroupCheck = {}; - var filteredByGroup = []; - r.forEach(function(v,n){ - var details = JSON.parse(r[n].details); - try{ - req.params.group.split('|').forEach(function(group){ - var groups = JSON.parse(details.groups); - if(groups.indexOf(group) > -1 && !filteredByGroupCheck[v.mid]){ - filteredByGroupCheck[v.mid] = true; - filteredByGroup.push(v) - } - }) - }catch(err){ - - } - }) - r = filteredByGroup; - } - r.forEach(function(v,n){ - if(s.group[v.ke]&&s.group[v.ke].mon[v.mid]&&s.group[v.ke].mon[v.mid].watch){ - r[n].currentlyWatching=Object.keys(s.group[v.ke].mon[v.mid].watch).length - } - r[n].subStream={} - var details = JSON.parse(r[n].details) - if(details.snap==='1'){ - r[n].subStream.jpeg = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg' - } - if(details.stream_channels&&details.stream_channels!==''){ - try{ - details.stream_channels=JSON.parse(details.stream_channels) - r[n].channels=[] - details.stream_channels.forEach(function(b,m){ - var streamURL - switch(b.stream_type){ - case'mjpeg': - streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+'/'+m - break; - case'hls': - streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+'/'+m+'/s.m3u8' - break; - case'h264': - streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+'/'+m - break; - case'flv': - streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+'/'+m+'/s.flv' - break; - case'mp4': - streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+'/'+m+'/s.mp4' - break; - } - r[n].channels.push(streamURL) - }) - }catch(err){ - s.log(req.params,{type:'Broken Monitor Object',msg:'Stream Channels Field is damaged. Skipping.'}) - } - } - }) - var page = config.renderPaths.grid - if(req.path.indexOf('/cycle/') > -1){ - page = config.renderPaths.cycle - } - res.render(page,{ - data:Object.assign(req.params,req.query), - baseUrl:req.protocol+'://'+req.hostname, - config:config, - lang:user.lang, - $user:user, - monitors:r, - originalURL:s.getOriginalUrl(req), - query:req.query - }); - }) - },res,req) -}); -//MJPEG feed -// if query string `full=true` is not present then it will load the MJPEG data directly and not the iframe ready page. -app.get([config.webPaths.apiPrefix+':auth/mjpeg/:ke/:id',config.webPaths.apiPrefix+':auth/mjpeg/:ke/:id/:channel'], function(req,res) { - res.header("Access-Control-Allow-Origin",req.headers.origin); - if(req.query.full=='true'){ - res.render(config.renderPaths.mjpeg,{url:'/'+req.params.auth+'/mjpeg/'+req.params.ke+'/'+req.params.id,originalURL:s.getOriginalUrl(req)}); - res.end() - }else{ - s.auth(req.params,function(user){ - s.checkChildProxy(req.params,function(){ - if(s.group[req.params.ke]&&s.group[req.params.ke].mon[req.params.id]){ - 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 - } - - var Emitter - if(!req.params.channel){ - Emitter = s.group[req.params.ke].mon[req.params.id].emitter - }else{ - Emitter = s.group[req.params.ke].mon[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition] - } - res.writeHead(200, { - 'Content-Type': 'multipart/x-mixed-replace; boundary=shinobi', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive', - 'Pragma': 'no-cache' - }); - var contentWriter,content = fs.readFileSync(config.defaultMjpeg,'binary'); - res.write("--shinobi\r\n"); - res.write("Content-Type: image/jpeg\r\n"); - res.write("Content-Length: " + content.length + "\r\n"); - res.write("\r\n"); - res.write(content,'binary'); - res.write("\r\n"); - Emitter.on('data',contentWriter=function(d){ - content = d; - res.write(content,'binary'); - }) - res.on('close', function () { - Emitter.removeListener('data',contentWriter) - }); - }else{ - res.end(); - } - },res,req); - },res,req); - } -}); -//embed monitor -app.get([config.webPaths.apiPrefix+':auth/embed/:ke/:id',config.webPaths.apiPrefix+':auth/embed/:ke/:id/:addon'], function (req,res){ - res.header("Access-Control-Allow-Origin",req.headers.origin); - req.params.protocol=req.protocol; - s.auth(req.params,function(user){ - 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 - } - if(s.group[req.params.ke]&&s.group[req.params.ke].mon[req.params.id]){ - if(s.group[req.params.ke].mon[req.params.id].started===1){ - req.params.uid=user.uid; - res.render(config.renderPaths.embed,{data:req.params,baseUrl:req.protocol+'://'+req.hostname,config:config,lang:user.lang,mon:CircularJSON.parse(CircularJSON.stringify(s.group[req.params.ke].mon_conf[req.params.id])),originalURL:s.getOriginalUrl(req)}); - res.end() - }else{ - res.end(user.lang['Cannot watch a monitor that isn\'t running.']) - } - }else{ - res.end(user.lang['No Monitor Exists with this ID.']) - } - },res,req); -}); -// Get TV Channels (Monitor Streams) json -app.get([config.webPaths.apiPrefix+':auth/tvChannels/:ke',config.webPaths.apiPrefix+':auth/tvChannels/:ke/:id','/get.php'], function (req,res){ - req.ret={ok:false}; - if(req.query.username&&req.query.password){ - req.params.username = req.query.username - req.params.password = req.query.password - } - var output = ['h264','hls','mp4'] - if(req.query.output&&req.query.output!==''){ - output = req.query.output.split(',') - output.forEach(function(type,n){ - if(type==='ts'){ - output[n]='h264' - if(output.indexOf('hls')===-1){ - output.push('hls') - } - } - }) - } - var isM3u8 = false; - if(req.query.type==='m3u8'||req.query.type==='m3u_plus'){ - //is m3u8 - isM3u8 = true; - }else{ - res.setHeader('Content-Type', 'application/json'); - } - res.header("Access-Control-Allow-Origin",req.headers.origin); - req.fn=function(user){ - if(user.permissions.get_monitors==="0"){ - res.end(s.s([])) - return - } - if(!req.params.ke){ - req.params.ke = user.ke; - } - if(req.query.id&&!req.params.id){ - req.params.id = req.query.id; - } - req.sql='SELECT * FROM Monitors WHERE mode!=? AND ke=?';req.ar=['stop',req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - var tvChannelMonitors = []; - r.forEach(function(v,n){ - var buildStreamURL = function(channelRow,type,channelNumber){ - var streamURL - if(channelNumber){channelNumber = '/'+channelNumber}else{channelNumber=''} - switch(type){ - case'mjpeg': - streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+channelNumber - break; - case'hls': - streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+channelNumber+'/s.m3u8' - break; - case'h264': - streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+channelNumber - break; - case'flv': - streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+channelNumber+'/s.flv' - break; - case'mp4': - streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+channelNumber+'/s.ts' - break; - } - if(streamURL){ - if(!channelRow.streamsSortedByType[type]){ - channelRow.streamsSortedByType[type]=[] - } - channelRow.streamsSortedByType[type].push(streamURL) - channelRow.streams.push(streamURL) - } - return streamURL - } - var details = JSON.parse(r[n].details); - if(!details.tv_channel_id||details.tv_channel_id==='')details.tv_channel_id = 'temp_'+s.gid(5) - var channelRow = { - ke:v.ke, - mid:v.mid, - type:v.type, - groupTitle:details.tv_channel_group_title, - channel:details.tv_channel_id, - }; - if(details.snap==='1'){ - channelRow.snapshot = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg' - } - channelRow.streams=[] - channelRow.streamsSortedByType={} - buildStreamURL(channelRow,details.stream_type) - if(details.stream_channels&&details.stream_channels!==''){ - details.stream_channels=JSON.parse(details.stream_channels) - details.stream_channels.forEach(function(b,m){ - buildStreamURL(channelRow,b.stream_type,m.toString()) - }) - } - if(details.tv_channel==='1'){ - tvChannelMonitors.push(channelRow) - } - }) - if(isM3u8){ - var m3u8 = '#EXTM3U'+'\n' - tvChannelMonitors.forEach(function(channelRow,n){ - output.forEach(function(type){ - if(channelRow.streamsSortedByType[type]){ - if(req.query.type==='m3u_plus'){ - m3u8 +='#EXTINF-1 tvg-id="'+channelRow.mid+'" tvg-name="'+channelRow.channel+'" tvg-logo="'+req.protocol+'://'+req.headers.host+channelRow.snapshot+'" group-title="'+channelRow.groupTitle+'",'+channelRow.channel+'\n' - }else{ - m3u8 +='#EXTINF:-1,'+channelRow.channel+' ('+type.toUpperCase()+') \n' - } - m3u8 += req.protocol+'://'+req.headers.host+channelRow.streamsSortedByType[type][0]+'\n' - } - }) - }) - res.end(m3u8) - }else{ - if(tvChannelMonitors.length===1){tvChannelMonitors=tvChannelMonitors[0];} - res.end(s.s(tvChannelMonitors, null, 3)); - } - }) - } - s.auth(req.params,req.fn,res,req); -}); -// Get monitors json -app.get([config.webPaths.apiPrefix+':auth/monitor/:ke',config.webPaths.apiPrefix+':auth/monitor/:ke/:id'], function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - req.fn=function(user){ - if(user.permissions.get_monitors==="0"){ - res.end(s.s([])) - return - } - req.sql='SELECT * FROM Monitors WHERE ke=?';req.ar=[req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - r.forEach(function(v,n){ - if(s.group[v.ke]&&s.group[v.ke].mon[v.mid]&&s.group[v.ke].mon[v.mid].watch){ - r[n].currentlyWatching=Object.keys(s.group[v.ke].mon[v.mid].watch).length - } - if(s.group[v.ke]&&s.group[v.ke].mon[v.mid]&&s.group[v.ke].mon[v.mid].watch){ - r[n].status = s.group[v.ke].mon[v.mid].monitorStatus - } - var buildStreamURL = function(type,channelNumber){ - var streamURL - if(channelNumber){channelNumber = '/'+channelNumber}else{channelNumber=''} - switch(type){ - case'mjpeg': - streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+channelNumber - break; - case'hls': - streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+channelNumber+'/s.m3u8' - break; - case'h264': - streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+channelNumber - break; - case'flv': - streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+channelNumber+'/s.flv' - break; - case'mp4': - streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+channelNumber+'/s.mp4' - break; - } - if(streamURL){ - if(!r[n].streamsSortedByType[type]){ - r[n].streamsSortedByType[type]=[] - } - r[n].streamsSortedByType[type].push(streamURL) - r[n].streams.push(streamURL) - } - return streamURL - } - var details = JSON.parse(r[n].details); - if(!details.tv_channel_id||details.tv_channel_id==='')details.tv_channel_id = 'temp_'+s.gid(5) - if(details.snap==='1'){ - r[n].snapshot = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg' - } - r[n].streams=[] - r[n].streamsSortedByType={} - buildStreamURL(details.stream_type) - if(details.stream_channels&&details.stream_channels!==''){ - details.stream_channels=JSON.parse(details.stream_channels) - details.stream_channels.forEach(function(b,m){ - buildStreamURL(b.stream_type,m.toString()) - }) - } - }) - if(r.length===1){r=r[0];} - res.end(s.s(r, null, 3)); - }) - } - s.auth(req.params,req.fn,res,req); -}); -// Get videos json -app.get([config.webPaths.apiPrefix+':auth/videos/:ke',config.webPaths.apiPrefix+':auth/videos/:ke/:id'], function (req,res){ - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' - if( - user.permissions.watch_videos==="0" || - hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1) - ){ - res.end(s.s([])) - return - } - req.sql='SELECT * FROM Videos WHERE ke=?';req.ar=[req.params.ke]; - req.count_sql='SELECT COUNT(*) FROM Videos WHERE ke=?';req.count_ar=[req.params.ke]; - if(req.query.archived=='1'){ - req.sql+=' AND details LIKE \'%"archived":"1"\'' - req.count_sql+=' AND details LIKE \'%"archived":"1"\'' - } - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - req.count_sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - req.count_sql+=' and mid=?';req.count_ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - if(req.query.start||req.query.end){ - if(req.query.start && req.query.start !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - } - if(req.query.end && req.query.end !== ''){ - req.query.end = s.stringToSqlTime(req.query.end) - } - if(!req.query.startOperator||req.query.startOperator==''){ - req.query.startOperator='>=' - } - if(!req.query.endOperator||req.query.endOperator==''){ - req.query.endOperator='<=' - } - switch(true){ - case(req.query.start&&req.query.start!==''&&req.query.end&&req.query.end!==''): - req.sql+=' AND `time` '+req.query.startOperator+' ? AND `end` '+req.query.endOperator+' ?'; - req.count_sql+=' AND `time` '+req.query.startOperator+' ? AND `end` '+req.query.endOperator+' ?'; - req.ar.push(req.query.start) - req.ar.push(req.query.end) - req.count_ar.push(req.query.start) - req.count_ar.push(req.query.end) - break; - case(req.query.start&&req.query.start!==''): - req.sql+=' AND `time` '+req.query.startOperator+' ?'; - req.count_sql+=' AND `time` '+req.query.startOperator+' ?'; - req.ar.push(req.query.start) - req.count_ar.push(req.query.start) - break; - case(req.query.end&&req.query.end!==''): - req.sql+=' AND `end` '+req.query.endOperator+' ?'; - req.count_sql+=' AND `end` '+req.query.endOperator+' ?'; - req.ar.push(req.query.end) - req.count_ar.push(req.query.end) - break; - } - } - req.sql+=' ORDER BY `time` DESC'; - if(!req.query.limit||req.query.limit==''){ - req.query.limit='100' - } - if(req.query.limit!=='0'){ - req.sql+=' LIMIT '+req.query.limit - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(!r){ - res.end(s.s({total:0,limit:req.query.limit,skip:0,videos:[]}, null, 3)); - return - } - s.sqlQuery(req.count_sql,req.count_ar,function(err,count){ - s.video('linkBuild',r,req.params.auth) - if(req.query.limit.indexOf(',')>-1){ - req.skip=parseInt(req.query.limit.split(',')[0]) - req.query.limit=parseInt(req.query.limit.split(',')[0]) - }else{ - req.skip=0 - req.query.limit=parseInt(req.query.limit) - } - res.end(s.s({isUTC:config.useUTC,total:count[0]['COUNT(*)'],limit:req.query.limit,skip:req.skip,videos:r}, null, 3)); - }) - }) - },res,req); -}); -// Get events json (motion logs) -app.get([config.webPaths.apiPrefix+':auth/events/:ke',config.webPaths.apiPrefix+':auth/events/:ke/:id',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit/:start',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit/:start/:end'], function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_view.indexOf(req.params.id)===-1){ - res.end(s.s([])) - return - } - req.sql='SELECT * FROM Events WHERE ke=?';req.ar=[req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - if(req.params.start&&req.params.start!==''){ - req.params.start = s.stringToSqlTime(req.params.start) - if(req.params.end&&req.params.end!==''){ - req.params.end = s.stringToSqlTime(req.params.end) - req.sql+=' AND `time` >= ? AND `time` <= ?'; - req.ar.push(decodeURIComponent(req.params.start)) - req.ar.push(decodeURIComponent(req.params.end)) - }else{ - req.sql+=' AND `time` >= ?'; - req.ar.push(decodeURIComponent(req.params.start)) - } - } - if(!req.params.limit||req.params.limit==''){req.params.limit=100} - req.sql+=' ORDER BY `time` DESC LIMIT '+req.params.limit+''; - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(err){ - err.sql=req.sql; - res.end(s.s(err, null, 3)); - return - } - if(!r){r=[]} - r.forEach(function(v,n){ - r[n].details=JSON.parse(v.details); - }) - res.end(s.s(r, null, 3)); - }) - },res,req); -}); -// Get logs json -app.get([config.webPaths.apiPrefix+':auth/logs/:ke',config.webPaths.apiPrefix+':auth/logs/:ke/:id'], function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - if(user.permissions.get_logs==="0" || user.details.sub && user.details.view_logs !== '1'){ - res.end(s.s([])) - return - } - req.sql='SELECT * FROM Logs WHERE ke=?';req.ar=[req.params.ke]; - if(!req.params.id){ - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - }else{ - if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1||req.params.id.indexOf('$')>-1){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - }else{ - res.end('[]'); - return; - } - } - if(req.query.start||req.query.end){ - if(!req.query.startOperator||req.query.startOperator==''){ - req.query.startOperator='>=' - } - if(!req.query.endOperator||req.query.endOperator==''){ - req.query.endOperator='<=' - } - if(req.query.start && req.query.start !== '' && req.query.end && req.query.end !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - req.query.end = s.stringToSqlTime(req.query.end) - req.sql+=' AND `time` '+req.query.startOperator+' ? AND `time` '+req.query.endOperator+' ?'; - req.ar.push(req.query.start) - req.ar.push(req.query.end) - }else if(req.query.start && req.query.start !== ''){ - req.query.start = s.stringToSqlTime(req.query.start) - req.sql+=' AND `time` '+req.query.startOperator+' ?'; - req.ar.push(req.query.start) - } - } - if(!req.query.limit||req.query.limit==''){req.query.limit=50} - req.sql+=' ORDER BY `time` DESC LIMIT '+req.query.limit+''; - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(err){ - err.sql=req.sql; - res.end(s.s(err, null, 3)); - return - } - if(!r){r=[]} - r.forEach(function(v,n){ - r[n].info=JSON.parse(v.info) - }) - res.end(s.s(r, null, 3)); - }) - },res,req); -}); -// Get monitors online json -app.get(config.webPaths.apiPrefix+':auth/smonitor/:ke', function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - req.fn=function(user){ - if(user.permissions.get_monitors==="0"){ - res.end(s.s([])) - return - } - req.sql='SELECT * FROM Monitors WHERE ke=?';req.ar=[req.params.ke]; - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(r&&r[0]){ - req.ar=[]; - r.forEach(function(v){ - if(s.group[req.params.ke]&&s.group[req.params.ke].mon[v.mid]&&s.group[req.params.ke].mon[v.mid].started===1){ - req.ar.push(v) - } - }) - }else{ - req.ar=[]; - } - res.end(s.s(req.ar, null, 3)); - }) - } - s.auth(req.params,req.fn,res,req); -}); -// Monitor Add,Edit,Delete -app.all([config.webPaths.apiPrefix+':auth/configureMonitor/:ke/:id',config.webPaths.apiPrefix+':auth/configureMonitor/:ke/:id/:f'], function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' - if(req.params.f !== 'delete'){ - if(!req.body.data&&!req.query.data){ - req.ret.msg='No Monitor Data found.' - res.end(s.s(req.ret, null, 3)) - return - } - try{ - if(req.query.data){ - req.monitor=JSON.parse(req.query.data) - }else{ - req.monitor=JSON.parse(req.body.data) - } - }catch(er){ - if(!req.monitor){ - req.ret.msg=user.lang.monitorEditText1; - res.end(s.s(req.ret, null, 3)) - } - return - } - if(!user.details.sub || - user.details.allmonitors === '1' || - hasRestrictions && user.details.monitor_edit.indexOf(req.monitor.mid) >- 1 || - hasRestrictions && user.details.monitor_create === '1'){ - if(req.monitor&&req.monitor.mid&&req.monitor.name){ - req.set=[],req.ar=[]; - req.monitor.mid=req.params.id.replace(/[^\w\s]/gi,'').replace(/ /g,''); - try{ - JSON.parse(req.monitor.details) - }catch(er){ - if(!req.monitor.details||!req.monitor.details.stream_type){ - req.ret.msg=user.lang.monitorEditText2; - res.end(s.s(req.ret, null, 3)) - return - }else{ - req.monitor.details=JSON.stringify(req.monitor.details) - } - } - req.monitor.ke=req.params.ke - req.logObject={details:JSON.parse(req.monitor.details),ke:req.params.ke,mid:req.params.id} - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[req.monitor.ke,req.monitor.mid],function(er,r){ - req.tx={f:'monitor_edit',mid:req.monitor.mid,ke:req.monitor.ke,mon:req.monitor}; - if(r&&r[0]){ - req.tx.new=false; - Object.keys(req.monitor).forEach(function(v){ - if(req.monitor[v]&&req.monitor[v]!==''){ - req.set.push(v+'=?'),req.ar.push(req.monitor[v]); - } - }) - req.set=req.set.join(','); - req.ar.push(req.monitor.ke),req.ar.push(req.monitor.mid); - s.log(req.monitor,{type:'Monitor Updated',msg:'by user : '+user.uid}); - req.ret.msg=user.lang['Monitor Updated by user']+' : '+user.uid; - s.sqlQuery('UPDATE Monitors SET '+req.set+' WHERE ke=? AND mid=?',req.ar) - req.finish=1; - }else{ - if(!s.group[req.monitor.ke].init.max_camera||s.group[req.monitor.ke].init.max_camera==''||Object.keys(s.group[req.monitor.ke].mon).length <= parseInt(s.group[req.monitor.ke].init.max_camera)){ - req.tx.new=true; - req.st=[]; - Object.keys(req.monitor).forEach(function(v){ - if(req.monitor[v]&&req.monitor[v]!==''){ - req.set.push(v),req.st.push('?'),req.ar.push(req.monitor[v]); - } - }) - // req.set.push('ke'),req.st.push('?'),req.ar.push(req.monitor.ke); - req.set=req.set.join(','),req.st=req.st.join(','); - s.log(req.monitor,{type:'Monitor Added',msg:'by user : '+user.uid}); - req.ret.msg=user.lang['Monitor Added by user']+' : '+user.uid; - s.sqlQuery('INSERT INTO Monitors ('+req.set+') VALUES ('+req.st+')',req.ar) - req.finish=1; - }else{ - req.tx.f='monitor_edit_failed'; - req.tx.ff='max_reached'; - req.ret.msg=user.lang.monitorEditFailedMaxReached; - } - } - if(req.finish===1){ - req.monitor.details=JSON.parse(req.monitor.details) - req.ret.ok=true; - s.init(0,{mid:req.monitor.mid,ke:req.monitor.ke}); - s.group[req.monitor.ke].mon_conf[req.monitor.mid]=s.init('noReference',req.monitor); - if(req.monitor.mode==='stop'){ - s.camera('stop',req.monitor); - }else{ - s.camera('stop',req.monitor);setTimeout(function(){s.camera(req.monitor.mode,req.monitor);},5000) - }; - s.tx(req.tx,'STR_'+req.monitor.ke); - }; - s.tx(req.tx,'GRP_'+req.monitor.ke); - res.end(s.s(req.ret, null, 3)) - }) - }else{ - req.ret.msg=user.lang.monitorEditText1; - res.end(s.s(req.ret, null, 3)) - } - }else{ - req.ret.msg=user.lang['Not Permitted']; - res.end(s.s(req.ret, null, 3)) - } - }else{ - if(!user.details.sub || user.details.allmonitors === '1' || user.details.monitor_edit.indexOf(req.params.id) > -1 || hasRestrictions && user.details.monitor_create === '1'){ - s.log(s.group[req.params.ke].mon_conf[req.params.id],{type:'Monitor Deleted',msg:'by user : '+user.uid}); - req.params.delete=1;s.camera('stop',req.params); - s.tx({f:'monitor_delete',uid:user.uid,mid:req.params.id,ke:req.params.ke},'GRP_'+req.params.ke); - s.sqlQuery('DELETE FROM Monitors WHERE ke=? AND mid=?',[req.params.ke,req.params.id]) -// s.sqlQuery('DELETE FROM Files WHERE ke=? AND mid=?',[req.params.ke,req.params.id]) - if(req.query.deleteFiles === 'true'){ - //videos - s.dir.addStorage.forEach(function(v,n){ - var videosDir = v.path+req.params.ke+'/'+req.params.id+'/' - fs.stat(videosDir,function(err,stat){ - if(!err){ - s.file('deleteFolder',videosDir) - } - }) - }) - var videosDir = s.dir.videos+req.params.ke+'/'+req.params.id+'/' - fs.stat(videosDir,function(err,stat){ - if(!err){ - s.file('deleteFolder',videosDir) - } - }) - //fileBin - var binDir = s.dir.fileBin+req.params.ke+'/'+req.params.id+'/' - fs.stat(binDir,function(err,stat){ - if(!err){ - s.file('deleteFolder',binDir) - } - }) - } - req.ret.ok=true; - req.ret.msg='Monitor Deleted by user : '+user.uid - res.end(s.s(req.ret, null, 3)) - }else{ - req.ret.msg=user.lang['Not Permitted']; - res.end(s.s(req.ret, null, 3)) - } - } - }) -}) -app.get([config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f',config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f/:ff',config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f/:ff/:fff'], function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - if(user.permissions.control_monitors==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitor_edit.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) - return - } - if(req.params.f===''){req.ret.msg=user.lang.monitorGetText1;res.end(s.s(req.ret, null, 3));return} - if(req.params.f!=='stop'&&req.params.f!=='start'&&req.params.f!=='record'){ - req.ret.msg='Mode not recognized.'; - res.end(s.s(req.ret, null, 3)); - return; - } - s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[req.params.ke,req.params.id],function(err,r){ - if(r&&r[0]){ - r=r[0]; - if(req.query.reset==='1'||(s.group[r.ke]&&s.group[r.ke].mon_conf[r.mid].mode!==req.params.f)||req.query.fps&&(!s.group[r.ke].mon[r.mid].currentState||!s.group[r.ke].mon[r.mid].currentState.trigger_on)){ - if(req.query.reset!=='1'||!s.group[r.ke].mon[r.mid].trigger_timer){ - if(!s.group[r.ke].mon[r.mid].currentState)s.group[r.ke].mon[r.mid].currentState={} - s.group[r.ke].mon[r.mid].currentState.mode=r.mode.toString() - s.group[r.ke].mon[r.mid].currentState.fps=r.fps.toString() - if(!s.group[r.ke].mon[r.mid].currentState.trigger_on){ - s.group[r.ke].mon[r.mid].currentState.trigger_on=true - }else{ - s.group[r.ke].mon[r.mid].currentState.trigger_on=false - } - r.mode=req.params.f; - try{r.details=JSON.parse(r.details);}catch(er){} - if(req.query.fps){ - r.fps=parseFloat(r.details.detector_trigger_record_fps) - s.group[r.ke].mon[r.mid].currentState.detector_trigger_record_fps=r.fps - } - r.id=r.mid; - s.sqlQuery('UPDATE Monitors SET mode=? WHERE ke=? AND mid=?',[r.mode,r.ke,r.mid]); - s.group[r.ke].mon_conf[r.mid]=r; - s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'GRP_'+r.ke); - s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'STR_'+r.ke); - s.camera('stop',s.init('noReference',r)); - if(req.params.f!=='stop'){ - s.camera(req.params.f,s.init('noReference',r)); - } - req.ret.msg=user.lang['Monitor mode changed']+' : '+req.params.f; - }else{ - req.ret.msg=user.lang['Reset Timer']; - } - req.ret.cmd_at=s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss'); - req.ret.ok=true; - if(req.params.ff&&req.params.f!=='stop'){ - req.params.ff=parseFloat(req.params.ff); - clearTimeout(s.group[r.ke].mon[r.mid].trigger_timer) - switch(req.params.fff){ - case'day':case'days': - req.timeout=req.params.ff*1000*60*60*24 - break; - case'hr':case'hour':case'hours': - req.timeout=req.params.ff*1000*60*60 - break; - case'min':case'minute':case'minutes': - req.timeout=req.params.ff*1000*60 - break; - default://seconds - req.timeout=req.params.ff*1000 - break; - } - s.group[r.ke].mon[r.mid].trigger_timer=setTimeout(function(){ - delete(s.group[r.ke].mon[r.mid].trigger_timer) - s.sqlQuery('UPDATE Monitors SET mode=? WHERE ke=? AND mid=?',[s.group[r.ke].mon[r.mid].currentState.mode,r.ke,r.mid]); - r.neglectTriggerTimer=1; - r.mode=s.group[r.ke].mon[r.mid].currentState.mode; - r.fps=s.group[r.ke].mon[r.mid].currentState.fps; - s.camera('stop',s.init('noReference',r),function(){ - if(s.group[r.ke].mon[r.mid].currentState.mode!=='stop'){ - s.camera(s.group[r.ke].mon[r.mid].currentState.mode,s.init('noReference',r)); - } - s.group[r.ke].mon_conf[r.mid]=r; - }); - s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'GRP_'+r.ke); - s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'STR_'+r.ke); - },req.timeout); -// req.ret.end_at=s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss').add(req.timeout,'milliseconds'); - } - }else{ - req.ret.msg=user.lang['Monitor mode is already']+' : '+req.params.f; - } - }else{ - req.ret.msg=user.lang['Monitor or Key does not exist.']; - } - res.end(s.s(req.ret, null, 3)); - }) - },res,req); -}) -//get file from fileBin bin -app.get([config.webPaths.apiPrefix+':auth/fileBin/:ke',config.webPaths.apiPrefix+':auth/fileBin/:ke/:id'],function (req,res){ - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - req.fn=function(user){ - req.sql='SELECT * FROM Files WHERE ke=?';req.ar=[req.params.ke]; - if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ - try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} - req.or=[]; - user.details.monitors.forEach(function(v,n){ - req.or.push('mid=?');req.ar.push(v) - }) - req.sql+=' AND ('+req.or.join(' OR ')+')' - }else{ - if(req.params.id&&(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1)){ - req.sql+=' and mid=?';req.ar.push(req.params.id) - } - } - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(!r){ - r=[] - }else{ - r.forEach(function(v){ - v.details=JSON.parse(v.details) - v.href='/'+req.params.auth+'/fileBin/'+req.params.ke+'/'+req.params.id+'/'+v.details.year+'/'+v.details.month+'/'+v.details.day+'/'+v.name; - }) - } - res.end(s.s(r, null, 3)); - }) - } - s.auth(req.params,req.fn,res,req); -}); -//get file from fileBin bin -app.get(config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:year/:month/:day/:file', function (req,res){ - res.header("Access-Control-Allow-Origin",req.headers.origin); - req.fn=function(user){ - req.failed=function(){ - res.end(user.lang['File Not Found']) - } - if (!s.group[req.params.ke].fileBin[req.params.id+'/'+req.params.file]){ - s.sqlQuery('SELECT * FROM Files WHERE ke=? AND mid=? AND name=?',[req.params.ke,req.params.id,req.params.file],function(err,r){ - if(r&&r[0]){ - r=r[0] - r.details=JSON.parse(r.details) - req.dir=s.dir.fileBin+req.params.ke+'/'+req.params.id+'/'+r.details.year+'/'+r.details.month+'/'+r.details.day+'/'+req.params.file; - if(fs.existsSync(req.dir)){ - res.on('finish',function(){res.end();}); - fs.createReadStream(req.dir).pipe(res); - }else{ - req.failed() - } - }else{ - req.failed() - } - }) - }else{ - res.end(user.lang['Please Wait for Completion']) - } - } - s.auth(req.params,req.fn,res,req); -}); -//zip videos and get link from fileBin -app.get(config.webPaths.apiPrefix+':auth/zipVideos/:ke', function (req,res){ - res.header("Access-Control-Allow-Origin",req.headers.origin); - var failed = function(resp){ - res.setHeader('Content-Type', 'application/json'); - res.end(s.s(resp)) - } - if(req.query.videos && req.query.videos !== ''){ - s.auth(req.params,function(user){ - var videosSelected = JSON.parse(req.query.videos) - var where = [] - var values = [] - videosSelected.forEach(function(video){ - where.push("(ke=? AND mid=? AND `time`=?)") - if(!video.ke)video.ke = req.params.ke - values.push(video.ke) - values.push(video.mid) - var time = s.nameToTime(video.filename) - if(req.query.isUTC === 'true'){ - time = s.utcToLocal(time) - } - time = new Date(time) - values.push(time) - }) - s.sqlQuery('SELECT * FROM Videos WHERE '+where.join(' OR '),values,function(err,r){ - var resp = {ok:false} - if(r && r[0]){ - resp.ok = true - var zipDownload = null - var tempFiles = [] - var fileId = s.gid() - var fileBinDir = s.dir.fileBin+req.params.ke+'/' - var tempScript = s.dir.streams+req.params.ke+'/'+fileId+'.sh' - var zippedFilename = s.formattedTime()+'-'+fileId+'-Shinobi_Recordings.zip' - var zippedFile = fileBinDir+zippedFilename - var script = 'cd '+fileBinDir+' && zip -9 -r '+zippedFile - res.on('close', () => { - if(zipDownload && zipDownload.destroy){ - zipDownload.destroy() - } - fs.unlink(zippedFile); - }) - if(!fs.existsSync(fileBinDir)){ - fs.mkdirSync(fileBinDir); - } - r.forEach(function(video){ - timeFormatted = s.formattedTime(video.time) - video.filename = timeFormatted+'.'+video.ext - var dir = s.video('getDir',video)+video.filename - var tempVideoFile = timeFormatted+' - '+video.mid+'.'+video.ext - fs.writeFileSync(fileBinDir+tempVideoFile, fs.readFileSync(dir)) - tempFiles.push(fileBinDir+tempVideoFile) - script += ' "'+tempVideoFile+'"' - }) - fs.writeFileSync(tempScript,script,'utf8') - var zipCreate = spawn('sh',(tempScript).split(' '),{detached: true}) - zipCreate.stderr.on('data',function(data){ - s.log({ke:req.params.ke,mid:'$USER'},{title:'Zip Create Error',msg:data.toString()}) - }) - zipCreate.on('exit',function(data){ - fs.unlinkSync(tempScript) - tempFiles.forEach(function(file){ - fs.unlink(file,function(){}) - }) - res.setHeader('Content-Disposition', 'attachment; filename="'+zippedFilename+'"') - var zipDownload = fs.createReadStream(zippedFile) - zipDownload.pipe(res) - zipDownload.on('error', function (error) { - s.log({ke:req.params.ke,mid:'$USER'},{title:'Zip Download Error',msg:error.toString()}) - if(zipDownload && zipDownload.destroy){ - zipDownload.destroy() - } - }); - zipDownload.on('close', function () { - res.end() - zipDownload.destroy(); - fs.unlinkSync(zippedFile); - }); - }) - }else{ - failed({ok:false,msg:'No Videos Found'}) - } - }) - },res,req); - }else{ - failed({ok:false,msg:'"videos" query variable is missing from request.'}) - } -}); -// Get video file -app.get(config.webPaths.apiPrefix+':auth/videos/:ke/:id/:file', function (req,res){ - s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) - return - } - var time = s.nameToTime(req.params.file) - if(req.query.isUTC === 'true'){ - time = s.utcToLocal(time) - } - time = new Date(time) - s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND mid=? AND `time`=?',[req.params.ke,req.params.id,time],function(err,r){ - if(r&&r[0]){ - req.dir=s.video('getDir',r[0])+req.params.file - if (fs.existsSync(req.dir)){ - req.ext=req.params.file.split('.')[1]; - var total = fs.statSync(req.dir).size; - if (req.headers['range']) { - var range = req.headers.range; - var parts = range.replace(/bytes=/, "").split("-"); - var partialstart = parts[0]; - var partialend = parts[1]; - - var start = parseInt(partialstart, 10); - var end = partialend ? parseInt(partialend, 10) : total-1; - var chunksize = (end-start)+1; - var file = fs.createReadStream(req.dir, {start: start, end: end}); - req.headerWrite={ 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/'+req.ext } - req.writeCode=206 - } else { - req.headerWrite={ 'Content-Length': total, 'Content-Type': 'video/'+req.ext}; - var file=fs.createReadStream(req.dir) - req.writeCode=200 - } - if(req.query.downloadName){ - req.headerWrite['content-disposition']='attachment; filename="'+req.query.downloadName+'"'; - } - res.writeHead(req.writeCode,req.headerWrite); - file.on('close',function(){ - res.end(); - }) - file.pipe(res); - }else{ - res.end(user.lang['File Not Found in Filesystem']) - } - }else{ - res.end(user.lang['File Not Found in Database']) - } - }) - },res,req); -}); -//motion trigger -app.get(config.webPaths.apiPrefix+':auth/motion/:ke/:id', function (req,res){ - s.auth(req.params,function(user){ - if(req.query.data){ - try{ - var d={id:req.params.id,ke:req.params.ke,details:JSON.parse(req.query.data)}; - }catch(err){ - res.end('Data Broken',err); - return; - } - }else{ - res.end('No Data'); - return; - } - if(!d.ke||!d.id||!s.group[d.ke]){ - res.end(user.lang['No Group with this key exists']); - return; - } - s.event('trigger',d,function(){ - res.end(user.lang['Trigger Successful']) - }); - },res,req); -}) -//hookTester trigger -app.get(config.webPaths.apiPrefix+':auth/hookTester/:ke/:id', function (req,res){ - res.setHeader('Content-Type', 'application/json'); - s.auth(req.params,function(user){ - s.log(req.params,{type:'Test',msg:'Hook Test'}) - res.end(s.s({ok:true},null,3)) - },res,req); -}) -//control trigger -app.get(config.webPaths.apiPrefix+':auth/control/:ke/:id/:direction', function (req,res){ - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - s.camera('control',req.params,function(resp){ - res.end(s.s(resp,null,3)) - }); - },res,req); -}) -//modify video file -app.get([config.webPaths.apiPrefix+':auth/videos/:ke/:id/:file/:mode',config.webPaths.apiPrefix+':auth/videos/:ke/:id/:file/:mode/:f'], function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_delete.indexOf(req.params.id)===-1){ - res.end(user.lang['Not Permitted']) - return - } - var time = s.nameToTime(req.params.file) - if(req.query.isUTC === 'true'){ - time = s.utcToLocal(time) - } - time = new Date(time) - req.sql='SELECT * FROM Videos WHERE ke=? AND mid=? AND `time`=?'; - req.ar=[req.params.ke,req.params.id,time]; - s.sqlQuery(req.sql,req.ar,function(err,r){ - if(r&&r[0]){ - r=r[0];r.filename=s.formattedTime(r.time)+'.'+r.ext; - switch(req.params.mode){ - case'fix': - req.ret.ok=true; - s.video('fix',r) - break; - case'status': - r.f = 'video_edit' - r.status = parseInt(req.params.f) - if(isNaN(req.params.f)||req.params.f===0){ - req.ret.msg='Not a valid value.'; - }else{ - req.ret.ok=true; - s.sqlQuery('UPDATE Videos SET status=? WHERE ke=? AND mid=? AND `time`=?',[req.params.f,req.params.ke,req.params.id,time]) - s.tx(r,'GRP_'+r.ke); - } - break; - case'delete': - req.ret.ok=true; - s.video('delete',r) - break; - default: - req.ret.msg=user.lang.modifyVideoText1; - break; - } - }else{ - req.ret.msg=user.lang['No such file']; - } - res.end(s.s(req.ret, null, 3)); - }) - },res,req); -}) -//ffmpeg pushed stream in here to make a pipe -app.all(['/streamIn/:ke/:id','/streamIn/:ke/:id/:feed'], function (req, res) { - var checkOrigin = function(search){return req.headers.host.indexOf(search)>-1} - if(checkOrigin('127.0.0.1')){ - if(!req.params.feed){req.params.feed='1'} - if(!s.group[req.params.ke].mon[req.params.id].streamIn[req.params.feed]){ - s.group[req.params.ke].mon[req.params.id].streamIn[req.params.feed] = new events.EventEmitter().setMaxListeners(0) - } - //req.params.feed = Feed Number - res.connection.setTimeout(0); - req.on('data', function(buffer){ - s.group[req.params.ke].mon[req.params.id].streamIn[req.params.feed].emit('data',buffer) - }); - req.on('end',function(){ -// console.log('streamIn closed',req.params); - }); - }else{ - res.end('Local connection is only allowed.') - } -}) -//MP4 Stream -app.get([config.webPaths.apiPrefix+':auth/mp4/:ke/:id/:channel/s.mp4',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/s.mp4',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/:channel/s.ts',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/s.ts'], function (req, res) { - s.auth(req.params,function(user){ - if(!s.group[req.params.ke] || !s.group[req.params.ke].mon[req.params.id]){ - res.status(404); - res.end('404 : Monitor not found'); - return - } - s.checkChildProxy(req.params,function(){ - var Channel = 'MAIN' - if(req.params.channel){ - Channel = parseInt(req.params.channel)+config.pipeAddition - } - var mp4frag = s.group[req.params.ke].mon[req.params.id].mp4frag[Channel]; - var errorMessage = 'MP4 Stream is not enabled' - if(!mp4frag){ - res.status(503); - res.end('503 : initialization : '+errorMessage); - }else{ - var init = mp4frag.initialization; - if (!init) { - res.status(503); - res.end('404 : Not Found : '+errorMessage); - } else { - res.locals.mp4frag = mp4frag - res.set('Access-Control-Allow-Origin', '*') - res.set('Connection', 'close') - res.set('Cache-Control', 'private, no-cache, no-store, must-revalidate') - res.set('Expires', '-1') - res.set('Pragma', 'no-cache') - res.set('Content-Type', 'video/mp4') - res.status(200); - res.write(init); - mp4frag.pipe(res); - res.on('close', () => { - mp4frag.unpipe(res); - }); - } - } - },res,req); - },res,req); -}); -//simulate RTSP over HTTP -app.get([ - config.webPaths.apiPrefix+':auth/mpegts/:ke/:id/:feed/:file', - config.webPaths.apiPrefix+':auth/mpegts/:ke/:id/:feed/', - config.webPaths.apiPrefix+':auth/h264/:ke/:id/:feed/:file', - config.webPaths.apiPrefix+':auth/h264/:ke/:id/:feed', - config.webPaths.apiPrefix+':auth/h264/:ke/:id' -], function (req, res) { - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - s.checkChildProxy(req.params,function(){ - if(!req.query.feed){req.query.feed='1'} - var Emitter - if(!req.params.feed){ - Emitter = s.group[req.params.ke].mon[req.params.id].streamIn[req.query.feed] - }else{ - Emitter = s.group[req.params.ke].mon[req.params.id].emitterChannel[parseInt(req.params.feed)+config.pipeAddition] - } - s.init('streamIn',req.params) - var contentWriter - var date = new Date(); - res.writeHead(200, { - 'Date': date.toUTCString(), - 'Connection': 'keep-alive', - 'Cache-Control': 'no-cache', - 'Pragma': 'no-cache', - 'Content-Type': 'video/mp4', - 'Server': 'Shinobi H.264 Test Stream', - }); - Emitter.on('data',contentWriter=function(buffer){ - res.write(buffer) - }) - res.on('close', function () { - Emitter.removeListener('data',contentWriter) - }) - },res,req); - },res,req); -}); -//FFprobe by API -app.get(config.webPaths.apiPrefix+':auth/probe/:ke',function (req,res){ - req.ret={ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - switch(req.query.action){ -// case'stop': -// exec('kill -9 '+user.ffprobe.pid,{detatched: true}) -// break; - default: - if(!req.query.url){ - req.ret.error = 'Missing URL' - res.end(s.s(req.ret, null, 3)); - return - } - if(user.ffprobe){ - req.ret.error = 'Account is already probing' - res.end(s.s(req.ret, null, 3)); - return - } - user.ffprobe=1; - if(req.query.flags==='default'){ - req.query.flags = '-v quiet -print_format json -show_format -show_streams' - }else{ - if(!req.query.flags){ - req.query.flags = '' - } - } - req.probeCommand = s.splitForFFPMEG(req.query.flags+' -i '+req.query.url).join(' ') - exec('ffprobe '+req.probeCommand+' | echo ',function(err,stdout,stderr){ - delete(user.ffprobe) - if(err){ - req.ret.error=(err) - }else{ - req.ret.ok=true - req.ret.result = stdout+stderr - } - req.ret.probe = req.probeCommand - res.end(s.s(req.ret, null, 3)); - }) - break; - } - },res,req); -}) -//ONVIF requesting with Shinobi API structure -app.all([config.webPaths.apiPrefix+':auth/onvif/:ke/:id/:action',config.webPaths.apiPrefix+':auth/onvif/:ke/:id/:service/:action'],function (req,res){ - var response = {ok:false}; - res.setHeader('Content-Type', 'application/json'); - res.header("Access-Control-Allow-Origin",req.headers.origin); - s.auth(req.params,function(user){ - var errorMessage = function(msg,error){ - response.ok = false - response.msg = msg - response.error = error - res.end(s.s(response,null,3)) - } - var actionCallback = function(onvifActionResponse){ - response.ok = true - if(onvifActionResponse.data){ - response.responseFromDevice = onvifActionResponse.data - }else{ - response.responseFromDevice = onvifActionResponse - } - if(onvifActionResponse.soap)response.soap = onvifActionResponse.soap - res.end(s.s(response,null,3)) - } - var isEmpty = function(obj) { - for(var key in obj) { - if(obj.hasOwnProperty(key)) - return false; - } - return true; - } - var doAction = function(Camera){ - var completeAction = function(command){ - if(command.then){ - command.then(actionCallback).catch(function(error){ - errorMessage('Device responded with an error',error) - }) - }else if(command){ - response.ok = true - response.repsonseFromDevice = command - res.end(s.s(response,null,3)) - }else{ - response.error = 'Big Errors, Please report it to Shinobi Development' - res.end(s.s(response,null,3)) - } - } - var action - if(req.params.service){ - if(Camera.services[req.params.service] === undefined){ - return errorMessage('This is not an available service. Please use one of the following : '+Object.keys(Camera.services).join(', ')) - } - if(Camera.services[req.params.service] === null){ - return errorMessage('This service is not activated. Maybe you are not connected through ONVIF. You can test by attempting to use the "Control" feature with ONVIF in Shinobi.') - } - action = Camera.services[req.params.service][req.params.action] - }else{ - action = Camera[req.params.action] - } - if(!action || typeof action !== 'function'){ - errorMessage(req.params.action+' is not an available ONVIF function. See https://github.com/futomi/node-onvif for functions.') - }else{ - var argNames = s.getFunctionParamNames(action) - var options - var command - if(argNames[0] === 'options' || argNames[0] === 'params'){ - options = {} - if(req.query.options){ - var jsonRevokedText = 'JSON not formated correctly' - try{ - options = JSON.parse(req.query.options) - }catch(err){ - return errorMessage(jsonRevokedText,err) - } - }else if(req.body.options){ - try{ - options = JSON.parse(req.body.options) - }catch(err){ - return errorMessage(jsonRevokedText,err) - } - }else if(req.query.params){ - try{ - options = JSON.parse(req.query.params) - }catch(err){ - return errorMessage(jsonRevokedText,err) - } - }else if(req.body.params){ - try{ - options = JSON.parse(req.body.params) - }catch(err){ - return errorMessage(jsonRevokedText,err) - } - } - } - if(req.params.service){ - command = Camera.services[req.params.service][req.params.action](options) - }else{ - command = Camera[req.params.action](options) - } - completeAction(command) - } - } - if(!s.group[req.params.ke].mon[req.params.id].onvifConnection){ - //prepeare onvif connection - var controlURL - var monitorConfig = s.group[req.params.ke].mon_conf[req.params.id] - if(!monitorConfig.details.control_base_url||monitorConfig.details.control_base_url===''){ - controlURL = s.init('url_no_path',monitorConfig) - }else{ - controlURL = monitorConfig.details.control_base_url - } - var controlURLOptions = s.camera('buildOptionsFromUrl',controlURL,monitorConfig) - //create onvif connection - s.group[req.params.ke].mon[req.params.id].onvifConnection = new onvif.OnvifDevice({ - xaddr : 'http://' + controlURLOptions.host + ':' + controlURLOptions.port + '/onvif/device_service', - user : controlURLOptions.username, - pass : controlURLOptions.password - }) - var device = s.group[req.params.ke].mon[req.params.id].onvifConnection - device.init().then((info) => { - if(info)doAction(device) - }).catch(function(error){ - return errorMessage('Device responded with an error',error) - }) - }else{ - doAction(s.group[req.params.ke].mon[req.params.id].onvifConnection) - } - },res,req); -}) -s.cpuUsage=function(e){ - k={} - switch(s.platform){ - case'win32': - k.cmd="@for /f \"skip=1\" %p in ('wmic cpu get loadpercentage') do @echo %p%" - break; - case'darwin': - k.cmd="ps -A -o %cpu | awk '{s+=$1} END {print s}'"; - break; - case'linux': - k.cmd='LANG=C top -b -n 2 | grep "^'+config.cpuUsageMarker+'" | awk \'{print $2}\' | tail -n1'; - break; - } - if(config.customCpuCommand){ - exec(config.customCpuCommand,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true) { - d = d.replace(/(\r\n|\n|\r)/gm, "").replace(/%/g, "") - } - e(d) - }); - } else if(k.cmd){ - exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true){ - d=d.replace(/(\r\n|\n|\r)/gm,"").replace(/%/g,"") - } - e(d) - }); - } else{ - e(0) - } -} -s.ramUsage=function(e){ - k={} - switch(s.platform){ - case'win32': - k.cmd = "wmic OS get FreePhysicalMemory /Value" - break; - case'darwin': - k.cmd = "vm_stat | awk '/^Pages free: /{f=substr($3,1,length($3)-1)} /^Pages active: /{a=substr($3,1,length($3-1))} /^Pages inactive: /{i=substr($3,1,length($3-1))} /^Pages speculative: /{s=substr($3,1,length($3-1))} /^Pages wired down: /{w=substr($4,1,length($4-1))} /^Pages occupied by compressor: /{c=substr($5,1,length($5-1)); print ((a+w)/(f+a+i+w+s+c))*100;}'" - break; - default: - k.cmd = "LANG=C free | grep Mem | awk '{print $4/$2 * 100.0}'"; - break; - } - if(k.cmd){ - exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ - if(s.isWin===true){ - d=(parseInt(d.split('=')[1])/(s.totalmem/1000))*100 - } - e(d) - }); - }else{ - e(0) - } -} -//check disk space every 20 minutes -if(config.autoDropCache===true){ - setInterval(function(){ - exec('echo 3 > /proc/sys/vm/drop_caches',{detached: true}) - },60000*20); -} -s.beat=function(){ - setTimeout(s.beat, 8000); - io.sockets.emit('ping',{beat:1}); -} -s.beat(); -s.processReady = function(){ - s.systemLog(lang.startUpText5) - process.send('ready') -} -//setup Master for childNodes -if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){ - 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); - }); - childNodeWebsocket.attach(childNodeServer); - //send data to child node function (experimental) - s.cx = function(z,y,x){if(!z.mid && !z.d){ - var err = new Error(); - console.log(err.stack); - };if(x){return x.broadcast.to(y).emit('c',z)};childNodeWebsocket.to(y).emit('c',z);} - //child Node Websocket - childNodeWebsocket.on('connection', function (cn) { - //functions for dispersing work to child servers; - cn.on('c',function(d){ - if(config.childNodes.key.indexOf(d.socketKey) > -1){ - if(!cn.shinobi_child&&d.f=='init'){ - cn.ip = cn.request.connection.remoteAddress.replace('::ffff:','')+':'+d.port - cn.shinobi_child = 1 - tx = function(z){ - cn.emit('c',z) - } - if(!s.childNodes[cn.ip]){ - s.childNodes[cn.ip] = {} - }; - s.childNodes[cn.ip].cnid = cn.id - s.childNodes[cn.ip].cpu = 0 - s.childNodes[cn.ip].activeCameras = {} - tx({ - f : 'init_success', - childNodes : s.childNodes - }); - }else{ - switch(d.f){ - case'cpu': - s.childNodes[cn.ip].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'camera': - s.camera(d.mode,d.data) - break; - case's.tx': - s.tx(d.data,d.to) - break; - case's.log': - if(!d.mon || !d.data)return console.log('LOG DROPPED',d.mon,d.data); - s.log(d.mon,d.data) - break; - case'created_file_chunk': - if(!s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename]){ - d.dir = s.video('getDir',s.group[d.ke].mon_conf[d.mid]) - s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename] = fs.createWriteStream(d.dir+d.filename) - } - s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename].write(d.chunk) - break; - case'created_file': - if(!s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename]){ - return console.log('FILE NOT EXIST') - } - s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename].end(); - 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.startTime, - size:d.filesize, - end:d.endTime - },'GRP_'+d.ke,'video_view'); - clearTimeout(s.group[d.ke].mon[d.mid].checker) - clearTimeout(s.group[d.ke].mon[d.mid].checkStream) - break; - } - } - } - }) - cn.on('disconnect',function(){ - if(s.childNodes[cn.ip]){ - var activeCameraKeys = Object.keys(s.childNodes[cn.ip].activeCameras) - activeCameraKeys.forEach(function(key){ - var monitor = s.childNodes[cn.ip].activeCameras[key] - s.camera('stop',s.init('noReference',monitor)) - delete(s.group[monitor.ke].mon[monitor.mid].childNode) - delete(s.group[monitor.ke].mon[monitor.mid].childNodeId) - setTimeout(function(){ - s.camera(monitor.mode,s.init('noReference',monitor)) - },1300) - }) - delete(s.childNodes[cn.ip]); - } - }) - }) -}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); - 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.log = function(x,y){s.cx({f:'s.log',mon:x,data:y})} - s.queuedSqlCallbacks = {} - s.sqlQuery = function(query,values,onMoveOn){ - var callbackId = s.gid() - if(!values){values=[]} - if(typeof values === 'function'){ - 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}); - } - setInterval(function(){ - s.cpuUsage(function(cpu){ - io.emit('c',{f:'cpu',cpu:parseFloat(cpu)}); - }) - },2000); - childIO.on('connect', function(d){ - console.log('CHILD CONNECTION SUCCESS') - s.cx({ - f : 'init', - port : config.port - }) - }) - 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.init(0,d.d); - s.kill(s.group[d.d.ke].mon[d.d.id].spawn,d.d) - break; - case'sync': - s.init(0,d.sync); - Object.keys(d.sync).forEach(function(v){ - s.group[d.sync.ke].mon[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'insertCompleted'://close video - s.video('insertCompleted',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; - }) -} -if(config.childNodes.mode === 'child'){ - //child node - startup functions -// fs.readdir(s.dir.videos, function(err,groupKeys) { -// groupKeys.forEach(function(groupKey){ -// fs.readdir(s.dir.videos+groupKey, function(err,monitorIds) { -// monitorIds.forEach(function(monitorId){ -// fs.readdir(s.dir.videos+groupKey+'/'+monitorId, function(err,files) { -// files.forEach(function(file){ -// if(/T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(file)){ -// var filePath = s.dir.videos+groupKey+'/'+monitorId+'/'+file -// var stat = fs.statSync(filePath) -// var filesize = stat.size -// var filesizeMB = parseFloat((filesize/1000000).toFixed(2)) -// var startTime = s.nameToTime(file) -// var endTime = s.formattedTime(stat.mtime,'YYYY-MM-DD HH:mm:ss') -// fs.createReadStream(filePath) -// .on('data',function(data){ -// s.cx({ -// f:'created_file_chunk', -// mid:monitorId, -// ke:groupKey, -// chunk:data, -// filename:file, -// filesize:filesize, -// time:s.timeObject(startTime).format(), -// end:s.timeObject(endTime).format() -// }) -// }) -// .on('close',function(){ -// s.cx({ -// f:'created_file', -// mid:monitorId, -// ke:groupKey, -// filename:file, -// filesize:filesize, -// time:s.timeObject(startTime).format(), -// end:s.timeObject(endTime).format() -// }) -// }) -// .on('error',function(){ -// console.log('File Read Error',file) -// }); -// }else{ -// console.log('Not Video',file) -// } -// }) -// }) -// }) -// }) -// }) -// }) -}else{ - //add Cloud Videos table, will remove in future - s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Videos` (`mid` varchar(50) NOT NULL,`ke` varchar(50) DEFAULT NULL,`href` text NOT NULL,`size` float DEFAULT NULL,`time` timestamp NULL DEFAULT NULL,`end` timestamp NULL DEFAULT NULL,`status` int(1) DEFAULT \'0\' COMMENT \'0:Complete,1:Read,2:Archive\',`details` text) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;',[],function(err){ - if(err)console.log(err) - }) - //master node - startup functions - setInterval(function(){ - s.cpuUsage(function(cpu){ - s.ramUsage(function(ram){ - s.tx({f:'os',cpu:cpu,ram:ram},'CPU'); - }) - }) - },10000); - setTimeout(function(){ - //get current disk used for each isolated account (admin user) on startup - s.sqlQuery('SELECT * FROM Users WHERE details NOT LIKE ?',['%"sub"%'],function(err,r){ - if(r&&r[0]){ - var count = r.length - var countFinished = 0 - r.forEach(function(v,n){ - v.size=0; - v.limit=JSON.parse(v.details).size - s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND status!=?',[v.ke,0],function(err,rr){ - ++countFinished - if(r&&r[0]){ - rr.forEach(function(b){ - v.size+=b.size - }) - } - s.systemLog(v.mail+' : '+lang.startUpText0+' : '+rr.length,v.size) - s.init('group',v) - s.init('apps',v) - s.systemLog(v.mail+' : '+lang.startUpText1,countFinished+'/'+count) - if(countFinished===count){ - s.systemLog(lang.startUpText4) - //preliminary monitor start - s.sqlQuery('SELECT * FROM Monitors', function(err,r) { - if(err){s.systemLog(err)} - if(r&&r[0]){ - r.forEach(function(v){ - s.init(0,v); - r.ar={}; - r.ar.id=v.mid; - Object.keys(v).forEach(function(b){ - r.ar[b]=v[b]; - }) - if(!s.group[v.ke]){ - s.group[v.ke]={} - s.group[v.ke].mon_conf={} - } - v.details=JSON.parse(v.details); - s.group[v.ke].mon_conf[v.mid]=v; - s.camera(v.mode,r.ar); - }); - } - s.processReady() - }); - } - }) - }) - }else{ - s.processReady() - } - }) - },1500) -} diff --git a/conf.sample.json b/conf.sample.json index 3600e77..9a74575 100644 --- a/conf.sample.json +++ b/conf.sample.json @@ -1,5 +1,6 @@ { "port": 8080, + "passwordType": "sha256", "addStorage": [ {"name":"second","path":"__DIR__/videos2"} ], diff --git a/cron.js b/cron.js index c8ad615..234ee9a 100644 --- a/cron.js +++ b/cron.js @@ -57,59 +57,70 @@ s.sqlDate = function(value){ }else{ value = value.toUpperCase() if (value.slice(-1) === 'S') { - value = value.slice(0, -1); + value = value.slice(0, -1); } dateQueryFunction = "DATE_SUB(NOW(), INTERVAL "+value+")" } return dateQueryFunction } -s.sqlQuery = function(query,values,onMoveOn,hideLog){ - s.debugLog(query,values) +s.mergeQueryValues = function(query,values){ if(!values){values=[]} var valuesNotFunction = true; if(typeof values === 'function'){ - var onMoveOn = values; var values = []; valuesNotFunction = false; } - if(!onMoveOn){onMoveOn=function(){}} if(values&&valuesNotFunction){ var splitQuery = query.split('?') var newQuery = '' splitQuery.forEach(function(v,n){ newQuery += v - if(values[n]){ - if(isNaN(values[n])){ - newQuery += "'"+values[n]+"'" + var value = values[n] + if(value){ + if(isNaN(value) || value instanceof Date){ + newQuery += "'"+value+"'" }else{ - newQuery += values[n] + newQuery += value } } }) }else{ newQuery = query } - return s.databaseEngine.raw(newQuery) - .asCallback(function(err,r){ - if(err&&config.databaseLogs){ - s.systemLog('s.sqlQuery QUERY',query) - s.systemLog('s.sqlQuery ERROR',err) + return newQuery +} +s.stringToSqlTime = function(value){ + newValue = new Date(value.replace('T',' ')) + return newValue +} +s.sqlQuery = function(query,values,onMoveOn){ + if(!values){values=[]} + if(typeof values === 'function'){ + var onMoveOn = values; + var values = []; + } + if(!onMoveOn){onMoveOn=function(){}} + var mergedQuery = s.mergeQueryValues(query,values) + s.debugLog('s.sqlQuery QUERY',mergedQuery) + return s.databaseEngine + .raw(query,values) + .asCallback(function(err,r){ + if(err){ + console.log('s.sqlQuery QUERY ERRORED',query) + console.log('s.sqlQuery ERROR',err) + } + if(onMoveOn && typeof onMoveOn === 'function'){ + switch(databaseOptions.client){ + case'sqlite3': + if(!r)r=[] + break; + default: + if(r)r=r[0] + break; } - if(onMoveOn) - if(typeof onMoveOn === 'function'){ - switch(databaseOptions.client){ - case'sqlite3': - if(!r)r=[] - break; - default: - if(r)r=r[0] - break; - } - onMoveOn(err,r) - }else{ - s.debugLog('onMoveOn',onMoveOn) - } - }) + onMoveOn(err,r) + } + }) } s.debugLog = function(arg1,arg2){ @@ -145,15 +156,15 @@ s.utcToLocal = function(time){ s.localToUtc = function(time){ return moment(time).utc() } -s.nameToTime=function(x){x=x.replace('.webm','').replace('.mp4','').split('T'),x[1]=x[1].replace(/-/g,':');x=x.join(' ');return x;} +s.nameToTime = function(x){x=x.replace('.webm','').replace('.mp4','').split('T'),x[1]=x[1].replace(/-/g,':');x=x.join(' ');return x;} io = require('socket.io-client')('ws://'+config.ip+':'+config.port);//connect to master -s.cx=function(x){x.cronKey=config.cron.key;return io.emit('cron',x)} +s.cx = function(x){x.cronKey=config.cron.key;return io.emit('cron',x)} //emulate master socket emitter -s.tx=function(x,y){s.cx({f:'s.tx',data:x,to:y})} -s.video=function(x,y){s.cx({f:'s.video',data:x,file:y})} +s.tx = function(x,y){s.cx({f:'s.tx',data:x,to:y})} +s.deleteVideo = function(x){s.cx({f:'s.deleteVideo',file:x})} //Cron Job s.cx({f:'init',time:moment()}) -s.getVideoDirectory=function(e){ +s.getVideoDirectory = function(e){ if(e.mid&&!e.id){e.id=e.mid}; if(e.details&&(e.details instanceof Object)===false){ try{e.details=JSON.parse(e.details)}catch(err){} @@ -164,19 +175,33 @@ s.getVideoDirectory=function(e){ return s.dir.videos+e.ke+'/'+e.id+'/'; } } -s.getFileBinDirectory=function(e){ +s.getFileBinDirectory = function(e){ if(e.mid&&!e.id){e.id=e.mid}; return s.dir.fileBin+e.ke+'/'+e.id+'/'; } //filters set by the user in their dashboard //deleting old videos is part of the filter - config.cron.deleteOld -s.checkFilterRules=function(v,callback){ +s.checkFilterRules = function(v,callback){ //filters if(!v.d.filters||v.d.filters==''){ v.d.filters={}; } //delete old videos with filter if(config.cron.deleteOld===true){ + var where = [{ + "p1":"end", + "p2":"<", + "p3":s.sqlDate(v.d.days+" DAYS"), + "p3_type":"function", + }] + //exclude monitors with their own max days + v.monitorsWithMaxKeepDays.forEach(function(mid){ + where.push({ + "p1":"mid", + "p2":"!=", + "p3":mid, + }) + }) v.d.filters.deleteOldVideosByCron={ "id":"deleteOldVideosByCron", "name":"deleteOldVideosByCron", @@ -188,12 +213,7 @@ s.checkFilterRules=function(v,callback){ "email":"0", "delete":"1", "execute":"", - "where":[{ - "p1":"end", - "p2":"<", - "p3":s.sqlDate(v.d.days+" DAYS"), - "p3_type":"function", - }] + "where":where }; } s.debugLog('Filters') @@ -269,7 +289,7 @@ s.checkFilterRules=function(v,callback){ } } //database rows with no videos in the filesystem -s.deleteRowsWithNoVideo=function(v,callback){ +s.deleteRowsWithNoVideo = function(v,callback){ if( config.cron.deleteNoVideo===true&&( config.cron.deleteNoVideoRecursion===true|| @@ -301,7 +321,7 @@ s.deleteRowsWithNoVideo=function(v,callback){ } fileExists = fs.existsSync(dir+filename) if(fileExists !== true){ - s.video('delete',ev) + s.deleteVideo(ev) s.tx({f:'video_delete',filename:filename+'.'+ev.ext,mid:ev.mid,ke:ev.ke,time:ev.time,end:s.moment(new Date,'YYYY-MM-DD HH:mm:ss')},'GRP_'+ev.ke); } }); @@ -318,7 +338,7 @@ s.deleteRowsWithNoVideo=function(v,callback){ } } //info about what the application is doing -s.deleteOldLogs=function(v,callback){ +s.deleteOldLogs = function(v,callback){ if(!v.d.log_days||v.d.log_days==''){v.d.log_days=10}else{v.d.log_days=parseFloat(v.d.log_days)}; if(config.cron.deleteLogs===true&&v.d.log_days!==0){ s.sqlQuery("DELETE FROM Logs WHERE ke=? AND `time` < "+s.sqlDate('? DAYS'),[v.ke,v.d.log_days],function(err,rrr){ @@ -333,7 +353,7 @@ s.deleteOldLogs=function(v,callback){ } } //events - motion, object, etc. detections -s.deleteOldEvents=function(v,callback){ +s.deleteOldEvents = function(v,callback){ if(!v.d.event_days||v.d.event_days==''){v.d.event_days=10}else{v.d.event_days=parseFloat(v.d.event_days)}; if(config.cron.deleteEvents===true&&v.d.event_days!==0){ s.sqlQuery("DELETE FROM Events WHERE ke=? AND `time` < "+s.sqlDate('? DAYS'),[v.ke,v.d.event_days],function(err,rrr){ @@ -348,10 +368,10 @@ s.deleteOldEvents=function(v,callback){ } } //check for temporary files (special archive) -s.deleteOldFileBins=function(v,callback){ +s.deleteOldFileBins = function(v,callback){ if(!v.d.fileBin_days||v.d.fileBin_days==''){v.d.fileBin_days=10}else{v.d.fileBin_days=parseFloat(v.d.fileBin_days)}; if(config.cron.deleteFileBins===true&&v.d.fileBin_days!==0){ - var fileBinQuery = " FROM Files WHERE ke=? AND `date` < "+s.sqlDate('? DAYS'); + var fileBinQuery = " FROM Files WHERE ke=? AND `time` < "+s.sqlDate('? DAYS'); s.sqlQuery("SELECT *"+fileBinQuery,[v.ke,v.d.fileBin_days],function(err,files){ if(files&&files[0]){ //delete the files @@ -377,60 +397,12 @@ s.deleteOldFileBins=function(v,callback){ } } //check for files with no database row -s.checkForOrphanedFiles=function(v,callback){ - if(config.cron.deleteOrphans===true){ - var finish=function(count){ - if(count>0 || config.debugLog === true){ - s.cx({f:'deleteOrphanedFiles',msg:count+' SQL rows with no database row deleted',ke:v.ke,time:moment()}) - } - callback() - } - e={}; - var numberOfItems = 0; - s.sqlQuery('SELECT * FROM Monitors WHERE ke=?',[v.ke],function(arr,b) { - if(b&&b[0]){ - b.forEach(function(mon,m){ - fs.readdir(s.getVideoDirectory(mon), function(err, items) { - e.query=[]; - e.filesFound=[mon.ke,mon.mid]; - numberOfItems+=items.length; - if(items&&items.length>0){ - items.forEach(function(v,n){ - e.query.push('time=?') - e.filesFound.push(s.nameToTime(v)) - }) - s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND mid=? AND ('+e.query.join(' OR ')+')',e.filesFound,function(arr,r) { - if(!r){r=[]}; - e.foundSQLrows=[]; - r.forEach(function(v,n){ - v.index=e.filesFound.indexOf(s.moment(v.time,'YYYY-MM-DD HH:mm:ss')); - if(v.index>-1){ - delete(items[v.index-2]); - } - }); - items.forEach(function(v,n){ - if(v&&v!==null){ - exec('rm '+s.getVideoDirectory(mon)+v); - } - if(m===b.length-1&&n===items.length-1){ - finish(numberOfItems) - } - }) - }) - }else{ - if(m===b.length-1){ - finish(numberOfItems) - } - } - }) - }); - }else{ - finish(numberOfItems) - } - }); - }else{ - callback() +s.checkForOrphanedFiles = function(v,callback){ + if(config.cron.deleteOrphans === true){ + console.log('"config.cron.deleteOrphans" has been removed. It has been replace by a one-time-run at startup with "config.insertOrphans". As the variable name suggests, instead of deleting, it will insert videos found without a database row.') + console.log('By default "config.orphanedVideoCheckMax" will only check up to 20 video. You can raise this value to any number you choose but be careful as it will check that number of videos on every start.') } + callback() } //user processing function s.processUser = function(number,rows){ @@ -456,9 +428,11 @@ s.processUser = function(number,rows){ if(!v.d.filters||v.d.filters==''){ v.d.filters={}; } + v.monitorsWithMaxKeepDays = [] rr.forEach(function(b,m){ b.details=JSON.parse(b.details); if(b.details.max_keep_days&&b.details.max_keep_days!==''){ + v.monitorsWithMaxKeepDays.push(b.mid) v.d.filters['deleteOldVideosByCron'+b.mid]={ "id":'deleteOldVideosByCron'+b.mid, "name":'deleteOldVideosByCron'+b.mid, @@ -471,7 +445,7 @@ s.processUser = function(number,rows){ "delete":"1", "execute":"", "where":[{ - "p1":"ke", + "p1":"mid", "p2":"=", "p3":b.mid },{ @@ -494,7 +468,6 @@ s.processUser = function(number,rows){ s.deleteRowsWithNoVideo(v,function(){ s.debugLog('--- deleteRowsWithNoVideo Complete') s.checkForOrphanedFiles(v,function(){ - s.debugLog('--- checkForOrphanedFiles Complete') //done user, unlock current, and do next s.overlapLock[v.ke]=false; s.processUser(number+1,rows) @@ -538,4 +511,4 @@ io.on('f',function(d){ break; } }) -console.log('Shinobi : cron.js started') \ No newline at end of file +console.log('Shinobi : cron.js started') diff --git a/definitions/en_CA.json b/definitions/en_CA.json index aa76b27..ab0e5af 100644 --- a/definitions/en_CA.json +++ b/definitions/en_CA.json @@ -289,8 +289,8 @@ }, { "name": "detail=stream_flv_type", - "field": "FLV Stream Type", - "description": "The method you will view your FLV stream. Both methods are always active for each stream regardless of your viewing choice for the Shinobi Dashboard.", + "field": "Connection Type", + "description": "The method you will view your stream. Both methods are always active regardless of your viewing choice for the Dashboard.", "default": "10", "example": "", "possible": "" @@ -737,7 +737,7 @@ { "name": "detail=detector", "field": "Enabled", - "description": "This will add another output in the FFMPEG command for the motion detector. A detector plugin must be connected for this to have any effect.", + "description": "This will add another output in the FFMPEG command for the motion detector.", "default": "No", "example": "", "possible": "" @@ -876,11 +876,35 @@ { "name": "detail=detector_sensitivity", "field": "Indifference", - "description": "How much Shinobi doesn't care about motion before doing something. The opposite of sensitivity; a lower number means it will trigger sooner. The value ranges up to 15(+) decimal places. 10 is default, 0.005 is pretty sensitive to motion changes. Note: If using Region Editor, leave this blank, and set indifference in the Region Editor (above).", - "default": "10", + "description": "This can mean multiple things depending on the detector used. Built-In Motion Detection defines this as \"Percentage Changed in View or Region\"", + "default": "0.005", "example": "10", "possible": "" }, + { + "name": "detail=detector_max_sensitivity", + "field": "Max Indifference", + "description": "An upperbound to indifference. Any value over this amount will be ignored.", + "default": "", + "example": "75", + "possible": "Any number." + }, + { + "name": "detail=detector_threshold", + "field": "Trigger Threshold", + "description": "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.", + "default": "1", + "example": "3", + "possible": "Any non-negative integer." + }, + { + "name": "detail=detector_color_threshold", + "field": "Color Threshold", + "description": "The amount of difference allowed in a pixel before it is considered motion.", + "default": "9", + "example": "9", + "possible": "Any non-negative integer." + }, { "name": "detail=detector_webhook", "field": "Webhook", diff --git a/languages/en_CA.json b/languages/en_CA.json index f8bef10..24a4f60 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -1,6 +1,9 @@ { "Shinobi": "Shinobi", "superAdminTitle": "Shinobi : Super Admin", + "failedLoginText1": "You have failed to login too many times. You must wait 15 minutes before trying again.", + "failedLoginText2": "Please check your login credentials.", + "Time Left": "Time Left", "Login": "Login", "Authenticate": "Authenticate", "Dashboard": "Dashboard", @@ -27,6 +30,7 @@ "Input Selector": "Input Selector", "Input Settings": "Input Settings", "Connection": "Connection", + "Video Set": "Video Set", "API": "API", "ONVIF": "ONVIF", "FFprobe": "Probe", @@ -121,6 +125,7 @@ "Sort By": "Sort By", "Start Time": "Start Time", "End Time": "End Time", + "Time": "Time", "Monitor ID": "Monitor ID", "File Type": "File Type", "Filesize": "Filesize", @@ -179,26 +184,36 @@ "RegionNote": "Points are only saved when you press Save on the Monitor Settings window.", "Points": "Points When adding points click on the edge of the polygon.", "Indifference": "Indifference", + "Max Indifference": "Max Indifference", + "Trigger Threshold": "Trigger Threshold", + "Color Threshold": "Color Threshold", "Region Name": "Region Name", "Regions": "Regions", "Again": "Again", "Account Info": "Account Info", "blankPassword": "Leave blank to keep same password", "2-Factor Authentication": "2-Factor Authentication", + "Use Max Storage Amount": "Use Max Storage Amount", "Max Storage Amount": "Max Storage Amount in Megabytes", "Number of Days to keep": "Number of Days to keep", "Monitor Groups": "Monitor Groups", "Group Name": "Group Name", "WebDAV": "WebDAV", + "Backblaze B2": "Backblaze B2", + "Backblaze Error": "Backblaze Error", + "Could not create Bucket.": "Could not create Bucket.", "Amazon S3": "Amazon S3", "Save Links to Database": "Save Links to Database", "Bucket": "Bucket", "Region": "Region", "Amazon S3 Upload Error": "Amazon S3 Upload Error", + "accountId": "Account ID", + "applicationKey": "Application Key", "aws_accessKeyId": "Access Key Id", "aws_secretAccessKey": "Secret Access Key", "Discord Bot": "Discord Bot", "URL": "URL", + "Operating Hours": "Operating Hours", "Autosave": "Autosave", "Save Directory": "Save Directory", "CSS": "CSS Style your dashboard.", @@ -267,6 +282,7 @@ "Invalid JSON": "Invalid JSON", "InvalidJSONText": "Please ensure this is a valid JSON string for Shinobi monitor configuration.", "Passwords don't match": "Passwords don't match", + "Email address is in use.": "Email address is in use.", "No Events found for this video": "No Events found for this video", "Video and Time Span (Minutes)": "Video and Time Span (Minutes)", "Video Length (minutes) and Motion Count per video": "Video Length (minutes) and Motion Count per video", @@ -444,6 +460,7 @@ "Log Level": "Log Level", "Save Log in SQL": "Save Log in SQL This can fill up quickly.", "JPEG": "JPEG", + "Web Page": "Web Page", "MJPEG": "MJPEG", "H.264 / H.265 / H.265+": "H.264 / H.265 / H.265+", "HLS (.m3u8)": "HLS (.m3u8)", @@ -526,7 +543,8 @@ "Flush PM2 Logs": "Flush PM2 Logs", "Filter ID": "Filter ID", "Webdav Error": "Webdav Error", - "WebdavErrorText": "Cannot save. Did you make the camera folders inside your chosen save directory?", + "WebdavErrorTextTryCreatingDir": "Cannot save. Trying to create directory.", + "WebdavErrorTextCreatingDir": "Cannot create directory.", "File Not Exist": "File Not Exist", "No Videos Found": "No Videos Found", "FileNotExistText": "Cannot save non existant file. Something went wrong.", @@ -578,6 +596,7 @@ "monSavedButNotCopied": "Your monitor was saved but not copied to any other monitor.", "No Monitor Found, Ignoring Request": "No Monitor Found, Ignoring Request", "Event": "Event", + "CPU used by this stream": "CPU used by this stream", "Detector Buffer": "Detector Buffer", "EventText1": "Triggered a motion event at", "EventText2": "Could not email image, file was not accessible", @@ -585,10 +604,14 @@ "updateKeyText1": "\"updateKey\" is missing from \"conf.json\", cannot do updates this way until you add it.", "updateKeyText2": "\"updateKey\" is incorrect.", "Control Error": "Control Error", + "Database row does not exist": "Database row does not exist", + "File Delete Error": "File Delete Error", + "postDataBroken": "Check the format of the JSON. Ensure it is stringified and defined under 'data'", "ControlErrorText1": "Control is not enabled", "ControlErrorText2": "Check your connection details. You may need to point the Base URL at port 8000 or 80. Check your authentication info.", "NotAuthorizedText1": "Not Authorized, Submit init command with \"auth\",\"ke\", and \"uid\"", "Fields cannot be empty": "Fields cannot be empty", + "Email and Password fields cannot be empty": "Email and Password fields cannot be empty", "AccountEditText1": "Could not edit. Refresh page if problem continues.", "Not an Administrator Account": "Not an Administrator Account", "superAdminText": "\"super.json\" does not exist. Please rename \"super.sample.json\" to \"super.json\".", @@ -606,15 +629,18 @@ "No Group with this key exists": "No Group with this key exists", "Trigger Successful": "Trigger Successful", "No such file": "No such file", + "h265BrowserText1": "If you are trying to play an H.265 file, you may need to download it and open it in another application like VLC.", "modifyVideoText1": "Method doesn't exist. Check to make sure that the last value of the URL is not blank.", "CPU indicator will not work. Continuing...": "CPU indicator will not work. Continuing...", - "startUpText0": "size check for videos", - "startUpText1": "end of size check for videos", + "startUpText0": "Checking Disk Used..", + "startUpText1": "Completed Checking Disk Used.", "startUpText2": "all users checked, wait to close open files and remove files over user limit", "startUpText3": "waiting to give unfinished video check some time. 3 seconds.", - "startUpText4": "starting all monitors set to watch and record", + "startUpText4": "Starting Monitors... Please Wait...", "startUpText5": "Shinobi is ready.", + "startUpText6": "Orphaned Videos Found and Inserted", "Migrator": "Migrator", + "Thumbnail": "Thumbnail", "Host Type": "Host Type", "Edit": "Edit", "Dashboard Language": "Dashboard Language", @@ -692,6 +718,7 @@ "Get Logs to Client": "Get Logs to Client", "Hardware Accelerated": "Hardware Accelerated", "Accelerator": "Accelerator", + "drm": "DRM object sharing", "qsv": "qsv", "dxva2": "dxva2 (DirectX Video, Windows)", "vdpau": "vdpau", diff --git a/libs/auth.js b/libs/auth.js new file mode 100644 index 0000000..bbc39a9 --- /dev/null +++ b/libs/auth.js @@ -0,0 +1,191 @@ +var fs = require('fs'); +module.exports = function(s,config,lang){ + //Authenticator functions + s.api = {} + s.superUsersApi = {} + s.factorAuth = {} + s.failedLoginAttempts = {} + //auth handler + //params = parameters + //cb = callback + //res = response, only needed for express (http server) + //request = request, only needed for express (http server) + s.auth = function(params,cb,res,req){ + if(req){ + //express (http server) use of auth function + params.ip=req.headers['cf-connecting-ip'] || req.headers['x-forwarded-for'] || req.connection.remoteAddress; + var failed=function(){ + if(!req.ret){req.ret={ok:false}} + req.ret.msg=lang['Not Authorized']; + res.end(s.s(req.ret)); + } + }else{ + //socket.io use of auth function + var failed = function(){ + //maybe log + } + } + var clearAfterTime=function(){ + //remove temp key from memory + clearTimeout(s.api[params.auth].timeout) + s.api[params.auth].timeout=setTimeout(function(){ + delete(s.api[params.auth]) + },1000*60*5) + } + //check IP address of connecting user + var finish=function(user){ + if(s.api[params.auth].ip.indexOf('0.0.0.0')>-1||s.api[params.auth].ip.indexOf(params.ip)>-1){ + cb(user); + }else{ + failed(); + } + } + //check if auth key is user's temporary session key + if(s.group[params.ke]&&s.group[params.ke].users&&s.group[params.ke].users[params.auth]){ + s.group[params.ke].users[params.auth].permissions={}; + cb(s.group[params.ke].users[params.auth]); + }else{ + //check if key is already in memory to save query time + if(s.api[params.auth]&&s.api[params.auth].details){ + finish(s.api[params.auth]); + if(s.api[params.auth].timeout){ + clearAfterTime() + } + }else{ + //no key in memory, query db to see if key exists + //check if using username and password in plain text or md5 + if(params.username&¶ms.username!==''&¶ms.password&¶ms.password!==''){ + s.sqlQuery('SELECT * FROM Users WHERE mail=? AND (pass=? OR pass=?)',[params.username,params.password,s.createHash(params.password)],function(err,r){ + if(r&&r[0]){ + r=r[0]; + r.ip='0.0.0.0'; + r.auth = s.gid(20); + params.auth = r.auth; + r.details=JSON.parse(r.details); + r.permissions = {}; + s.api[r.auth]=r; + clearAfterTime(); + finish(r); + }else{ + failed(); + } + }) + }else{ + //not using plain login + s.sqlQuery('SELECT * FROM API WHERE code=? AND ke=?',[params.auth,params.ke],function(err,r){ + if(r&&r[0]){ + r=r[0]; + s.api[params.auth]={ip:r.ip,uid:r.uid,ke:r.ke,permissions:JSON.parse(r.details),details:{}}; + s.sqlQuery('SELECT mail,details FROM Users WHERE uid=? AND ke=?',[r.uid,r.ke],function(err,rr){ + if(rr&&rr[0]){ + rr=rr[0]; + try{ + s.api[params.auth].mail=rr.mail + s.api[params.auth].details=JSON.parse(rr.details) + s.api[params.auth].lang=s.getLanguageFile(s.api[params.auth].details.lang) + }catch(er){} + } + finish(s.api[params.auth]); + }) + }else{ + s.sqlQuery('SELECT * FROM Users WHERE auth=? AND ke=?',[params.auth,params.ke],function(err,r){ + if(r&&r[0]){ + r=r[0]; + r.ip='0.0.0.0' + s.api[params.auth]=r + s.api[params.auth].details=JSON.parse(r.details) + s.api[params.auth].permissions={} + clearAfterTime() + finish(r) + }else{ + failed(); + } + }) + } + }) + } + } + } + } + //super user authentication handler + s.superAuth = function(params,callback,res,req){ + var userFound = false + var userSelected = false + var adminUsersSelected = null + try{ + var success = function(){ + if(req && res){ + res.setHeader('Content-Type', 'application/json'); + var ip = req.headers['cf-connecting-ip']||req.headers["CF-Connecting-IP"]||req.headers["'x-forwarded-for"]||req.connection.remoteAddress; + var resp = { + ok: userFound, + ip: ip + } + if(userFound === false){ + resp.msg = lang['Not Authorized'] + res.end(s.prettyPrint(resp)) + } + if(userSelected){ + resp.$user = userSelected + } + if(adminUsersSelected){ + resp.users = adminUsersSelected + } + } + callback({ + ip : ip, + $user:userSelected, + users:adminUsersSelected, + config:config, + lang:lang + }) + } + var foundUser = function(){ + if(params.users === true){ + s.sqlQuery('SELECT * FROM Users WHERE details NOT LIKE ?',['%"sub"%'],function(err,r) { + adminUsersSelected = r + success() + }) + }else{ + success() + } + } + if(params.auth && s.superUsersApi[params.auth]){ + userFound = true + userSelected = s.superUsersApi[params.auth].$user + foundUser() + }else{ + var superUserList = JSON.parse(fs.readFileSync(s.location.super)) + superUserList.forEach(function(superUser,n){ + if( + userFound === false && + ( + params.auth && superUser.tokens && superUser.tokens[params.auth] || //using API key (object) + params.auth && superUser.tokens && superUser.tokens.indexOf && superUser.tokens.indexOf(params.auth) > -1 || //using API key (array) + ( + params.mail && params.mail.toLowerCase() === superUser.mail.toLowerCase() && //email matches + ( + params.pass === superUser.pass || //user give it already hashed + superUser.pass === s.createHash(params.pass) || //hash and check it + superUser.pass.toLowerCase() === s.md5(params.pass).toLowerCase() //check if still using md5 + ) + ) + ) + ){ + userFound = true + userSelected = superUser + foundUser() + } + }) + } + }catch(err){ + console.log('The following error may mean your super.json is not formatted correctly.') + console.log(err) + } + if(userFound === true){ + return true + }else{ + return false + } + } +} diff --git a/libs/basic.js b/libs/basic.js new file mode 100644 index 0000000..58c558c --- /dev/null +++ b/libs/basic.js @@ -0,0 +1,227 @@ +var moment = require('moment'); +var crypto = require('crypto'); +var exec = require('child_process').exec; +var spawn = require('child_process').spawn; +var events = require('events'); +var http = require('http'); +var https = require('https'); +module.exports = function(s,config){ + //kill any ffmpeg running + s.ffmpegKill=function(){ + var cmd='' + if(s.isWin===true){ + cmd = "Taskkill /IM ffmpeg.exe /F" + }else{ + cmd = "ps aux | grep -ie ffmpeg | awk '{print $2}' | xargs kill -9" + } + exec(cmd,{detached: true}) + }; + process.on('exit',s.ffmpegKill.bind(null,{cleanup:true})); + process.on('SIGINT',s.ffmpegKill.bind(null, {exit:true})); + s.checkRelativePath = function(x){ + if(x.charAt(0)!=='/'){ + x=s.mainDirectory+'/'+x + } + return x + } + s.checkDetails = function(e){ + if(!e.id && e.mid){e.id = e.mid} + if(e.details&&(e.details instanceof Object)===false){ + try{e.details=JSON.parse(e.details)}catch(err){} + } + } + s.parseJSON = function(string){ + try{ + string = JSON.parse(string) + }catch(err){ + + } + return string + } + s.stringJSON = function(json){ + try{ + if(json instanceof Object){ + json = JSON.stringify(json) + } + }catch(err){ + + } + return json + } + s.addUserPassToUrl = function(url,user,pass){ + var splitted = url.split('://') + splitted[1] = user + ':' + pass + '@' + splitted[1] + return splitted.join('://') + } + s.checkCorrectPathEnding = function(x){ + var length=x.length + if(x.charAt(length-1)!=='/'){ + x=x+'/' + } + return x.replace('__DIR__',s.mainDirectory) + } + s.md5 = function(x){return crypto.createHash('md5').update(x).digest("hex")} + s.createHash = s.md5 + switch(config.passwordType){ + case'sha512': + if(config.passwordSalt){ + s.createHash = function(x){return crypto.pbkdf2Sync(x, config.passwordSalt, 100000, 64, 'sha512').toString('hex')} + } + break; + case'sha256': + s.createHash = function(x){return crypto.createHash('sha256').update(x).digest("hex")} + break; + } + //load camera controller vars + s.nameToTime=function(x){x=x.split('.')[0].split('T'),x[1]=x[1].replace(/-/g,':');x=x.join(' ');return x;} + s.ratio=function(width,height,ratio){ratio = width / height;return ( Math.abs( ratio - 4 / 3 ) < Math.abs( ratio - 16 / 9 ) ) ? '4:3' : '16:9';} + s.randomNumber=function(x){ + if(!x){x=10}; + return Math.floor((Math.random() * x) + 1); + }; + s.gid=function(x){ + if(!x){x=10};var t = "";var p = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + for( var i=0; i < x; i++ ) + t += p.charAt(Math.floor(Math.random() * p.length)); + return t; + }; + s.nid=function(x){ + if(!x){x=6};var t = "";var p = "0123456789"; + for( var i=0; i < x; i++ ) + t += p.charAt(Math.floor(Math.random() * p.length)); + return t; + }; + s.formattedTime_withOffset=function(e,x){ + if(!e){e=new Date};if(!x){x='YYYY-MM-DDTHH-mm-ss'}; + e=s.timeObject(e);if(config.utcOffset){e=e.utcOffset(config.utcOffset)} + return e.format(x); + } + s.formattedTime=function(e,x){ + if(!e){e=new Date};if(!x){x='YYYY-MM-DDTHH-mm-ss'}; + return s.timeObject(e).format(x); + } + s.utcToLocal = function(time){ + return moment.utc(time).utcOffset(s.utcOffset).format() + } + s.localTimeObject = function(e,x){ + return moment(e) + } + if(config.useUTC === true){ + s.timeObject = function(time){ + return moment(time).utc() + } + }else{ + s.timeObject = moment + } + s.ipRange=function(start_ip, end_ip) { + var start_long = s.toLong(start_ip); + var end_long = s.toLong(end_ip); + if (start_long > end_long) { + var tmp=start_long; + start_long=end_long + end_long=tmp; + } + var range_array = []; + var i; + for (i=start_long; i<=end_long;i++) { + range_array.push(s.fromLong(i)); + } + return range_array; + } + s.portRange=function(lowEnd,highEnd){ + var list = []; + for (var i = lowEnd; i <= highEnd; i++) { + list.push(i); + } + return list; + } + //toLong taken from NPM package 'ip' + s.toLong=function(ip) { + var ipl = 0; + ip.split('.').forEach(function(octet) { + ipl <<= 8; + ipl += parseInt(octet); + }); + return(ipl >>> 0); + } + //fromLong taken from NPM package 'ip' + s.fromLong=function(ipl) { + return ((ipl >>> 24) + '.' + + (ipl >> 16 & 255) + '.' + + (ipl >> 8 & 255) + '.' + + (ipl & 255) ); + } + s.getFunctionParamNames = function(func) { + var fnStr = func.toString().replace(/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg, ''); + var result = fnStr.slice(fnStr.indexOf('(')+1, fnStr.indexOf(')')).match(/([^\s,]+)/g); + if(result === null) + result = []; + return result; + } + s.getRequest = function(url,callback){ + return http.get(url, function(res){ + var body = ''; + res.on('data', function(chunk){ + body += chunk; + }); + res.on('end',function(){ + try{body = JSON.parse(body)}catch(err){} + callback(body) + }); + }).on('error', function(e){ + // s.systemLog("Get Snapshot Error", e); + }); + } + //system log + s.systemLog = function(q,w,e){ + if(!w){w=''} + if(!e){e=''} + if(config.systemLog===true){ + if(typeof q==='string'&&s.databaseEngine){ + s.sqlQuery('INSERT INTO Logs (ke,mid,info) VALUES (?,?,?)',['$','$SYSTEM',s.s({type:q,msg:w})]); + s.tx({f:'log',log:{time:s.timeObject(),ke:'$',mid:'$SYSTEM',time:s.timeObject(),info:s.s({type:q,msg:w})}},'$'); + } + return console.log(s.timeObject().format(),q,w,e) + } + } + //system log + s.debugLog = function(q,w,e){ + if(config.debugLog === true){ + if(!w){w = ''} + if(!e){e = ''} + console.log(s.timeObject().format(),q,w,e) + if(config.debugLogVerbose === true){ + console.log(new Error()) + } + } + } + s.getOriginalUrl = function(req){ + var url + if(config.baseURL || config.baseURL === ''){ + url = config.baseURL + }else{ + url = req.protocol + '://' + req.get('host') + '/' + } + return url + } + s.file=function(x,e){ + if(!e){e={}}; + switch(x){ + case'size': + return fs.statSync(e.filename)["size"]; + break; + case'delete': + if(!e){return false;} + return exec('rm -f '+e,{detached: true}); + break; + case'deleteFolder': + if(!e){return false;} + return exec('rm -rf '+e,{detached: true}); + break; + case'deleteFiles': + if(!e.age_type){e.age_type='min'};if(!e.age){e.age='1'}; + exec('find '+e.path+' -type f -c'+e.age_type+' +'+e.age+' -exec rm -f {} +',{detached: true}); + break; + } + } +} diff --git a/libs/childNode.js b/libs/childNode.js new file mode 100644 index 0000000..ab84389 --- /dev/null +++ b/libs/childNode.js @@ -0,0 +1,217 @@ +var fs = require('fs'); +var http = require('http'); +var https = require('https'); +var express = require('express'); +module.exports = function(s,config,lang,app,io){ + //setup Master for childNodes + if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){ + 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); + //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) + } + } + //child Node Websocket + childNodeWebsocket.on('connection', function (cn) { + //functions for dispersing work to child servers; + cn.on('c',function(d){ + if(config.childNodes.key.indexOf(d.socketKey) > -1){ + if(!cn.shinobi_child&&d.f=='init'){ + cn.ip = cn.request.connection.remoteAddress.replace('::ffff:','')+':'+d.port + cn.shinobi_child = 1 + tx = function(z){ + cn.emit('c',z) + } + if(!s.childNodes[cn.ip]){ + s.childNodes[cn.ip] = {} + }; + s.childNodes[cn.ip].cnid = cn.id + s.childNodes[cn.ip].cpu = 0 + s.childNodes[cn.ip].activeCameras = {} + d.availableHWAccels.forEach(function(accel){ + if(config.availableHWAccels.indexOf(accel) === -1)config.availableHWAccels.push(accel) + }) + tx({ + f : 'init_success', + childNodes : s.childNodes + }) + s.childNodes[cn.ip].coreCount = d.coreCount + }else{ + switch(d.f){ + case'cpu': + s.childNodes[cn.ip].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'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'created_file_chunk': + if(!s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename]){ + d.dir = s.getVideoDirectory(s.group[d.ke].mon_conf[d.mid]) + s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename] = fs.createWriteStream(d.dir+d.filename) + } + s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename].write(d.chunk) + break; + case'created_file': + if(!s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename]){ + return console.log('FILE NOT EXIST') + } + s.group[d.ke].mon[d.mid].childNodeStreamWriters[d.filename].end(); + 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/1000000).toFixed(2)) + } + s.insertDatabaseRow(d.d,insert) + s.insertCompletedVideoExtensions.forEach(function(extender){ + extender(d.d,insert) + }) + //purge over max + s.purgeDiskForGroup(d) + //send new diskUsage values + s.setDiskUsedForGroup(d,insert.filesizeMB) + clearTimeout(s.group[d.ke].mon[d.mid].recordingChecker) + clearTimeout(s.group[d.ke].mon[d.mid].streamChecker) + break; + } + } + } + }) + cn.on('disconnect',function(){ + console.log('childNodeWebsocket.disconnect') + + if(s.childNodes[cn.ip]){ + var activeCameraKeys = Object.keys(s.childNodes[cn.ip].activeCameras) + activeCameraKeys.forEach(function(key){ + var monitor = s.childNodes[cn.ip].activeCameras[key] + s.camera('stop',s.cleanMonitorObject(monitor)) + delete(s.group[monitor.ke].mon[monitor.mid].childNode) + delete(s.group[monitor.ke].mon[monitor.mid].childNodeId) + setTimeout(function(){ + s.camera(monitor.mode,s.cleanMonitorObject(monitor)) + },1300) + }) + delete(s.childNodes[cn.ip]); + } + }) + }) + }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); + 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})} + s.queuedSqlCallbacks = {} + s.sqlQuery = function(query,values,onMoveOn){ + var callbackId = s.gid() + if(!values){values=[]} + if(typeof values === 'function'){ + 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}); + } + setInterval(function(){ + s.cpuUsage(function(cpu){ + io.emit('c',{f:'cpu',cpu:parseFloat(cpu)}); + }) + },2000); + 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); + s.cameraDestroy(s.group[d.d.ke].mon[d.d.id].spawn,d.d) + break; + case'sync': + s.initiateMonitorObject(d.sync); + Object.keys(d.sync).forEach(function(v){ + s.group[d.sync.ke].mon[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'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; + }) + } +} diff --git a/libs/cloudUploaders.js b/libs/cloudUploaders.js new file mode 100644 index 0000000..b0188cd --- /dev/null +++ b/libs/cloudUploaders.js @@ -0,0 +1,555 @@ +var fs = require('fs'); +var exec = require('child_process').exec; +var spawn = require('child_process').spawn; +var webdav = require("webdav-fs"); +module.exports = function(s,config,lang){ + // WebDAV + var beforeAccountSaveForWebDav = function(d){ + //d = save event + d.form.details.use_webdav=d.d.use_webdav + } + var cloudDiskUseStartupForWebDav = function(group,userDetails){ + group.cloudDiskUse['webdav'].name = 'WebDAV' + group.cloudDiskUse['webdav'].sizeLimitCheck = (userDetails.use_webdav_size_limit === '1') + if(!userDetails.webdav_size_limit || userDetails.webdav_size_limit === ''){ + group.cloudDiskUse['webdav'].sizeLimit = 10000 + }else{ + group.cloudDiskUse['webdav'].sizeLimit = parseFloat(userDetails.webdav_size_limit) + } + } + var loadWebDavForUser = function(e){ + // e = user + var ar = JSON.parse(e.details); + //owncloud/webdav + if(!s.group[e.ke].webdav && + ar.webdav_user&& + ar.webdav_user!==''&& + ar.webdav_pass&& + ar.webdav_pass!==''&& + ar.webdav_url&& + ar.webdav_url!=='' + ){ + if(!ar.webdav_dir||ar.webdav_dir===''){ + ar.webdav_dir='/' + } + ar.webdav_dir = s.checkCorrectPathEnding(ar.webdav_dir) + s.group[e.ke].webdav = webdav( + ar.webdav_url, + ar.webdav_user, + ar.webdav_pass + ) + } + } + var unloadWebDavForUser = function(user){ + s.group[user.ke].webdav = null + } + var deleteVideoFromWebDav = function(e,video,callback){ + // e = user + try{ + var videoDetails = JSON.parse(video.details) + }catch(err){ + var videoDetails = video.details + } + if(!videoDetails.location){ + var prefix = s.addUserPassToUrl(s.checkCorrectPathEnding(s.group[e.ke].init.webdav_url),s.group[e.ke].init.webdav_user,s.group[e.ke].init.webdav_pass) + videoDetails.location = video.href.replace(prefix,'') + } + s.group[e.ke].webdav.unlink(videoDetails.location, function(err) { + if (err) console.log(videoDetails.location,err) + callback() + }) + } + var uploadVideoToWebDav = function(e,k){ + //e = video object + //k = temporary values + if(!k)k={}; + //cloud saver - webdav + var wfs = s.group[e.ke].webdav + if(wfs && s.group[e.ke].init.use_webdav !== '0' && s.group[e.ke].init.webdav_save === "1"){ + var webdavUploadDir = s.group[e.ke].init.webdav_dir+e.ke+'/'+e.mid+'/' + var startWebDavUpload = function(){ + s.group[e.ke].mon[e.id].webdavDirExist = true + var wfsWriteStream = + fs.createReadStream(k.dir + k.filename).pipe(wfs.createWriteStream(webdavUploadDir + k.filename)) + if(s.group[e.ke].init.webdav_log === '1'){ + var webdavRemoteUrl = s.addUserPassToUrl(s.checkCorrectPathEnding(s.group[e.ke].init.webdav_url),s.group[e.ke].init.webdav_user,s.group[e.ke].init.webdav_pass) + s.group[e.ke].init.webdav_dir + e.ke + '/'+e.mid+'/'+k.filename + var save = [ + e.mid, + e.ke, + k.startTime, + 1, + s.s({ + type : 'webdav', + location : webdavUploadDir + k.filename + }), + k.filesize, + k.endTime, + webdavRemoteUrl + ] + s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) + s.setCloudDiskUsedForGroup(e,{ + amount : k.filesizeMB, + storageType : 'webdav' + }) + s.purgeCloudDiskForGroup(e,'webdav') + } + } + if(s.group[e.ke].mon[e.id].webdavDirExist !== true){ + //check if webdav dir exist + var parentPoint = 0 + var webDavParentz = webdavUploadDir.split('/') + var webDavParents = [] + webDavParentz.forEach(function(v){ + if(v && v !== '')webDavParents.push(v) + }) + var stitchPieces = './' + var lastParentCheck = function(){ + ++parentPoint + if(parentPoint === webDavParents.length){ + startWebDavUpload() + } + checkPathPiece(webDavParents[parentPoint]) + } + var checkPathPiece = function(pathPiece){ + if(pathPiece && pathPiece !== ''){ + stitchPieces += pathPiece + '/' + wfs.stat(stitchPieces, function(error, stats) { + if(error){ + reply = { + status : error.status, + msg : lang.WebdavErrorTextTryCreatingDir, + dir : stitchPieces, + } + s.userLog(e,{type:lang['Webdav Error'],msg:reply}) + wfs.mkdir(stitchPieces, function(error) { + if(error){ + reply = { + status : error.status, + msg : lang.WebdavErrorTextCreatingDir, + dir : stitchPieces, + } + s.userLog(e,{type:lang['Webdav Error'],msg:reply}) + }else{ + lastParentCheck() + } + }) + }else{ + lastParentCheck() + } + }) + }else{ + ++parentPoint + } + } + checkPathPiece(webDavParents[0]) + }else{ + startWebDavUpload() + } + } + } + //Amazon S3 + var beforeAccountSaveForAmazonS3 = function(d){ + //d = save event + d.form.details.use_aws_s3=d.d.use_aws_s3 + } + var cloudDiskUseStartupForAmazonS3 = function(group,userDetails){ + group.cloudDiskUse['s3'].name = 'Amazon S3' + group.cloudDiskUse['s3'].sizeLimitCheck = (userDetails.use_aws_s3_size_limit === '1') + if(!userDetails.aws_s3_size_limit || userDetails.aws_s3_size_limit === ''){ + group.cloudDiskUse['s3'].sizeLimit = 10000 + }else{ + group.cloudDiskUse['s3'].sizeLimit = parseFloat(userDetails.aws_s3_size_limit) + } + } + var loadAmazonS3ForUser = function(e){ + // e = user + var ar = JSON.parse(e.details); + //Amazon S3 + if(!s.group[e.ke].aws && + !s.group[e.ke].aws_s3 && + ar.aws_s3 !== '0' && + ar.aws_accessKeyId !== ''&& + ar.aws_secretAccessKey && + ar.aws_secretAccessKey !== ''&& + ar.aws_region && + ar.aws_region !== ''&& + ar.aws_s3_bucket !== '' + ){ + if(!ar.aws_s3_dir || ar.aws_s3_dir === '/'){ + ar.aws_s3_dir = '' + } + if(ar.aws_s3_dir !== ''){ + ar.aws_s3_dir = s.checkCorrectPathEnding(ar.aws_s3_dir) + } + s.group[e.ke].aws = new require("aws-sdk") + s.group[e.ke].aws.config = new s.group[e.ke].aws.Config({ + accessKeyId: ar.aws_accessKeyId, + secretAccessKey: ar.aws_secretAccessKey, + region: ar.aws_region + }) + s.group[e.ke].aws_s3 = new s.group[e.ke].aws.S3(); + } + } + var unloadAmazonS3ForUser = function(user){ + s.group[user.ke].aws = null + s.group[user.ke].aws_s3 = null + } + var deleteVideoFromAmazonS3 = function(e,video,callback){ + // e = user + try{ + var videoDetails = JSON.parse(video.details) + }catch(err){ + var videoDetails = video.details + } + if(!videoDetails.location){ + videoDetails.location = video.href.split('.amazonaws.com')[1] + } + s.group[e.ke].aws_s3.deleteObject({ + Bucket: s.group[e.ke].init.aws_s3_bucket, + Key: videoDetails.location, + }, function(err, data) { + if (err) console.log(err); + callback() + }); + } + var uploadVideoToAmazonS3 = function(e,k){ + //e = video object + //k = temporary values + if(!k)k={}; + //cloud saver - amazon s3 + if(s.group[e.ke].aws_s3 && s.group[e.ke].init.use_aws_s3 !== '0' && s.group[e.ke].init.aws_s3_save === '1'){ + var ext = k.filename.split('.') + ext = ext[ext.length - 1] + var fileStream = fs.createReadStream(k.dir+k.filename); + fileStream.on('error', function (err) { + console.error(err) + }) + var saveLocation = s.group[e.ke].init.aws_s3_dir+e.ke+'/'+e.mid+'/'+k.filename + s.group[e.ke].aws_s3.upload({ + Bucket: s.group[e.ke].init.aws_s3_bucket, + Key: saveLocation, + Body:fileStream, + ACL:'public-read', + ContentType:'video/'+ext + },function(err,data){ + if(err){ + s.userLog(e,{type:lang['Amazon S3 Upload Error'],msg:err}) + } + if(s.group[e.ke].init.aws_s3_log === '1' && data && data.Location){ + var save = [ + e.mid, + e.ke, + k.startTime, + 1, + s.s({ + type : 's3', + location : saveLocation + }), + k.filesize, + k.endTime, + data.Location + ] + s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) + s.setCloudDiskUsedForGroup(e,{ + amount : k.filesizeMB, + storageType : 's3' + }) + s.purgeCloudDiskForGroup(e,'s3') + } + }) + } + } + //Backblaze B2 + var beforeAccountSaveForBackblazeB2 = function(d){ + //d = save event + d.form.details.use_aws_s3=d.d.use_bb_b2 + } + var cloudDiskUseStartupForBackblazeB2 = function(group,userDetails){ + group.cloudDiskUse['b2'].name = 'Backblaze B2' + group.cloudDiskUse['b2'].sizeLimitCheck = (userDetails.use_bb_b2_size_limit === '1') + if(!userDetails.bb_b2_size_limit || userDetails.bb_b2_size_limit === ''){ + group.cloudDiskUse['b2'].sizeLimit = 10000 + }else{ + group.cloudDiskUse['b2'].sizeLimit = parseFloat(userDetails.bb_b2_size_limit) + } + } + var loadBackblazeB2ForUser = function(e){ + var ar = JSON.parse(e.details); + try{ + if(!s.group[e.ke].bb_b2 && + ar.bb_b2_accountId && + ar.bb_b2_accountId !=='' && + ar.bb_b2_applicationKey && + ar.bb_b2_applicationKey !=='' && + ar.bb_b2_bucket && + ar.bb_b2_bucket !== '' + ){ + var B2 = require('backblaze-b2') + if(!ar.bb_b2_dir || ar.bb_b2_dir === '/'){ + ar.bb_b2_dir = '' + } + if(ar.bb_b2_dir !== ''){ + ar.bb_b2_dir = s.checkCorrectPathEnding(ar.bb_b2_dir) + } + var b2 = new B2({ + accountId: ar.bb_b2_accountId, + applicationKey: ar.bb_b2_applicationKey + }) + s.group[e.ke].bb_b2 = b2 + var backblazeErr = function(err){ + // console.log(err) + s.userLog({mid:'$USER',ke:e.ke},{type:lang['Backblaze Error'],msg:err.data}) + } + b2.authorize().then(function(resp){ + s.group[e.ke].bb_b2_downloadUrl = resp.data.downloadUrl + b2.listBuckets().then(function(resp){ + var buckets = resp.data.buckets + var bucketN = -2 + buckets.forEach(function(item,n){ + if(item.bucketName === ar.bb_b2_bucket){ + bucketN = n + } + }) + if(bucketN > -1){ + s.group[e.ke].bb_b2_bucketId = buckets[bucketN].bucketId + }else{ + b2.createBucket( + ar.bb_b2_bucket, + 'allPublic' + ).then(function(resp){ + s.group[e.ke].bb_b2_bucketId = resp.data.bucketId + }).catch(backblazeErr) + } + }).catch(backblazeErr) + }).catch(backblazeErr) + } + }catch(err){ + s.debugLog(err) + } + } + var unloadBackblazeB2ForUser = function(user){ + s.group[user.ke].bb_b2 = null + } + var deleteVideoFromBackblazeB2 = function(e,video,callback){ + // e = user + try{ + var videoDetails = JSON.parse(video.details) + }catch(err){ + var videoDetails = video.details + } + s.group[e.ke].bb_b2.deleteFileVersion({ + fileId: videoDetails.fileId, + fileName: videoDetails.fileName + }).then(function(resp){ + // console.log('deleteFileVersion',resp.data) + }).catch(function(err){ + console.log('deleteFileVersion',err) + }) + } + var uploadVideoToBackblazeB2 = function(e,k){ + //e = video object + //k = temporary values + if(!k)k={}; + //cloud saver - Backblaze B2 + if(s.group[e.ke].bb_b2 && s.group[e.ke].init.use_bb_b2 !== '0' && s.group[e.ke].init.bb_b2_save === '1'){ + var backblazeErr = function(err){ + // console.log(err) + s.userLog({mid:'$USER',ke:e.ke},{type:lang['Backblaze Error'],msg:err.data}) + } + fs.readFile(k.dir+k.filename,function(err,data){ + var backblazeSavePath = s.group[e.ke].init.bb_b2_dir+e.ke+'/'+e.mid+'/'+k.filename + var getUploadUrl = function(bucketId,callback){ + s.group[e.ke].bb_b2.getUploadUrl(bucketId).then(function(resp){ + callback(resp.data) + }).catch(backblazeErr) + } + getUploadUrl(s.group[e.ke].bb_b2_bucketId,function(req){ + s.group[e.ke].bb_b2.uploadFile({ + uploadUrl: req.uploadUrl, + uploadAuthToken: req.authorizationToken, + filename: backblazeSavePath, + data: data, + onUploadProgress: null + }).then(function(resp){ + if(s.group[e.ke].init.bb_b2_log === '1' && resp.data.fileId){ + var backblazeDownloadUrl = s.group[e.ke].bb_b2_downloadUrl + '/file/' + s.group[e.ke].init.bb_b2_bucket + '/' + backblazeSavePath + var save = [ + e.mid, + e.ke, + k.startTime, + 1, + s.s({ + type : 'b2', + bucketId : resp.data.bucketId, + fileId : resp.data.fileId, + fileName : resp.data.fileName + }), + k.filesize, + k.endTime, + backblazeDownloadUrl + ] + s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) + s.setCloudDiskUsedForGroup(e,{ + amount : k.filesizeMB, + storageType : 'b2' + }) + s.purgeCloudDiskForGroup(e,'b2') + } + }).catch(backblazeErr) + }) + }) + } + } + //SFTP + // var beforeAccountSaveForSftp = function(d){ + // //d = save event + // d.form.details.use_sftp = d.d.use_sftp + // } + // var cloudDiskUseStartupForSftp = function(group,userDetails){ + // group.cloudDiskUse['sftp'].name = 'SFTP' + // group.cloudDiskUse['sftp'].sizeLimitCheck = (userDetails.use_aws_s3_size_limit === '1') + // if(!userDetails.aws_s3_size_limit || userDetails.aws_s3_size_limit === ''){ + // group.cloudDiskUse['sftp'].sizeLimit = 10000 + // }else{ + // group.cloudDiskUse['sftp'].sizeLimit = parseFloat(userDetails.aws_s3_size_limit) + // } + // } + // var loadSftpForUser = function(e){ + // // e = user + // var ar = JSON.parse(e.details); + // //SFTP + // if(!s.group[e.ke].sftp && + // !s.group[e.ke].sftp && + // ar.sftp !== '0' && + // ar.sftp_accessKeyId !== ''&& + // ar.sftp_secretAccessKey && + // ar.sftp_secretAccessKey !== ''&& + // ar.sftp_region && + // ar.sftp_region !== ''&& + // ar.sftp_bucket !== '' + // ){ + // if(!ar.sftp_dir || ar.sftp_dir === '/'){ + // ar.sftp_dir = '' + // } + // if(ar.sftp_dir !== ''){ + // ar.sftp_dir = s.checkCorrectPathEnding(ar.sftp_dir) + // } + // s.group[e.ke].sftp = new s.group[e.ke].sftp.S3(); + // s.group[e.ke].sftp = new require('ssh2-sftp-client')(); + // var connectionDetails = { + // host: ar.sftp_host, + // port: ar.sftp_port + // } + // if(!ar.sftp_port)ar.sftp_port = 22 + // if(ar.sftp_username)connectionDetails.username = ar.sftp_username + // if(ar.sftp_password)connectionDetails.password = ar.sftp_password + // if(ar.sftp_privateKey)connectionDetails.privateKey = ar.sftp_privateKey + // sftp.connect(connectionDetails).then(() => { + // return sftp.list('/pathname'); + // }).then((data) => { + // console.log(data, 'the data info'); + // }).catch((err) => { + // console.log(err, 'catch error'); + // }); + // } + // } + // var unloadSftpForUser = function(user){ + // s.group[user.ke].sftp = null + // } + // var deleteVideoFromSftp = function(e,video,callback){ + // // e = user + // try{ + // var videoDetails = JSON.parse(video.details) + // }catch(err){ + // var videoDetails = video.details + // } + // s.group[e.ke].sftp.deleteObject({ + // Bucket: s.group[e.ke].init.sftp_bucket, + // Key: videoDetails.location, + // }, function(err, data) { + // if (err) console.log(err); + // callback() + // }); + // } + // var uploadVideoToSftp = function(e,k){ + // //e = video object + // //k = temporary values + // if(!k)k={}; + // //cloud saver - SFTP + // if(s.group[e.ke].sftp && s.group[e.ke].init.use_sftp !== '0' && s.group[e.ke].init.sftp_save === '1'){ + // var fileStream = fs.createReadStream(k.dir+k.filename); + // fileStream.on('error', function (err) { + // console.error(err) + // }) + // var saveLocation = s.group[e.ke].init.sftp_dir+e.ke+'/'+e.mid+'/'+k.filename + // s.group[e.ke].sftp.upload({ + // Bucket: s.group[e.ke].init.sftp_bucket, + // Key: saveLocation, + // Body:fileStream, + // ACL:'public-read' + // },function(err,data){ + // if(err){ + // s.userLog(e,{type:lang['SFTP Upload Error'],msg:err}) + // } + // if(s.group[e.ke].init.sftp_log === '1' && data && data.Location){ + // var save = [ + // e.mid, + // e.ke, + // k.startTime, + // 1, + // s.s({ + // type : 'sftp', + // location : saveLocation + // }), + // k.filesize, + // k.endTime, + // data.Location + // ] + // s.sqlQuery('INSERT INTO `Cloud Videos` (mid,ke,time,status,details,size,end,href) VALUES (?,?,?,?,?,?,?,?)',save) + // s.setCloudDiskUsedForGroup(e,{ + // amount : k.filesizeMB, + // storageType : 'sftp' + // }) + // s.purgeCloudDiskForGroup(e,'sftp') + // } + // }) + // } + // } + //add the extenders + //webdav + s.loadGroupAppExtender(loadWebDavForUser) + s.unloadGroupAppExtender(unloadWebDavForUser) + s.insertCompletedVideoExtender(uploadVideoToWebDav) + s.deleteVideoFromCloudExtensions['webdav'] = deleteVideoFromWebDav + s.cloudDiskUseStartupExtensions['webdav'] = cloudDiskUseStartupForWebDav + s.beforeAccountSave(beforeAccountSaveForWebDav) + s.onAccountSave(cloudDiskUseStartupForWebDav) + s.cloudDisksLoader('webdav') + //amazon s3 + s.loadGroupAppExtender(loadAmazonS3ForUser) + s.unloadGroupAppExtender(unloadAmazonS3ForUser) + s.insertCompletedVideoExtender(uploadVideoToAmazonS3) + s.deleteVideoFromCloudExtensions['s3'] = deleteVideoFromAmazonS3 + s.cloudDiskUseStartupExtensions['s3'] = cloudDiskUseStartupForAmazonS3 + s.beforeAccountSave(beforeAccountSaveForAmazonS3) + s.onAccountSave(cloudDiskUseStartupForAmazonS3) + s.cloudDisksLoader('s3') + //backblaze b2 + s.loadGroupAppExtender(loadBackblazeB2ForUser) + s.unloadGroupAppExtender(unloadBackblazeB2ForUser) + s.insertCompletedVideoExtender(uploadVideoToBackblazeB2) + s.deleteVideoFromCloudExtensions['b2'] = deleteVideoFromBackblazeB2 + s.cloudDiskUseStartupExtensions['b2'] = cloudDiskUseStartupForBackblazeB2 + s.beforeAccountSave(beforeAccountSaveForBackblazeB2) + s.onAccountSave(cloudDiskUseStartupForBackblazeB2) + s.cloudDisksLoader('b2') + //SFTP + // s.loadGroupAppExtender(loadSftpForUser) + // s.unloadGroupAppExtender(unloadSftpForUser) + // s.insertCompletedVideoExtender(uploadVideoToSftp) + // s.deleteVideoFromCloudExtensions['sftp'] = deleteVideoFromSftp + // s.cloudDiskUseStartupExtensions['sftp'] = cloudDiskUseStartupForSftp + // s.beforeAccountSave(beforeAccountSaveForSftp) + // s.onAccountSave(cloudDiskUseStartupForSftp) + // s.cloudDisksLoader('sftp') +} diff --git a/libs/config.js b/libs/config.js new file mode 100644 index 0000000..185daf0 --- /dev/null +++ b/libs/config.js @@ -0,0 +1,55 @@ +module.exports = function(s){ + s.location = { + super : s.mainDirectory+'/super.json', + config : s.mainDirectory+'/conf.json', + languages : s.mainDirectory+'/languages' + } + var config = require(s.location.config); + if(!config.productType){ + config.productType='CE' + } + //config defaults + if(config.cpuUsageMarker === undefined){config.cpuUsageMarker='%Cpu'} + if(config.customCpuCommand === undefined){config.customCpuCommand=null} + if(config.autoDropCache === undefined){config.autoDropCache=true} + if(config.doSnapshot === undefined){config.doSnapshot=true} + if(config.restart === undefined){config.restart={}} + if(config.systemLog === undefined){config.systemLog=true} + if(config.deleteCorruptFiles === undefined){config.deleteCorruptFiles=true} + if(config.restart.onVideoNotExist === undefined){config.restart.onVideoNotExist=true} + if(config.ip === undefined||config.ip===''||config.ip.indexOf('0.0.0.0')>-1){config.ip='localhost'}else{config.bindip=config.ip}; + if(config.cron === undefined)config.cron={}; + if(config.cron.enabled === undefined)config.cron.enabled=true; + if(config.cron.deleteOld === undefined)config.cron.deleteOld=true; + if(config.cron.deleteNoVideo === undefined)config.cron.deleteNoVideo=true; + if(config.cron.deleteNoVideoRecursion === undefined)config.cron.deleteNoVideoRecursion=false; + if(config.cron.deleteOverMax === undefined)config.cron.deleteOverMax=true; + if(config.cron.deleteOverMaxOffset === undefined)config.cron.deleteOverMaxOffset=0.9; + if(config.cron.deleteLogs === undefined)config.cron.deleteLogs=true; + if(config.cron.deleteEvents === undefined)config.cron.deleteEvents=true; + if(config.cron.deleteFileBins === undefined)config.cron.deleteFileBins=true; + if(config.cron.interval === undefined)config.cron.interval=1; + if(config.databaseType === undefined){config.databaseType='mysql'} + if(config.pluginKeys === undefined)config.pluginKeys={}; + if(config.databaseLogs === undefined){config.databaseLogs=false} + if(config.useUTC === undefined){config.useUTC=false} + if(config.iconURL === undefined){config.iconURL = "https://shinobi.video/libs/assets/icon/apple-touch-icon-152x152.png"} + if(config.pipeAddition === undefined){config.pipeAddition=7}else{config.pipeAddition=parseInt(config.pipeAddition)} + if(config.hideCloudSaveUrls === undefined){config.hideCloudSaveUrls = true} + if(config.insertOrphans === undefined){config.insertOrphans = true} + if(config.orphanedVideoCheckMax === undefined){config.orphanedVideoCheckMax = 20} + //Child Nodes + if(config.childNodes === undefined)config.childNodes = {}; + //enabled + if(config.childNodes.enabled === undefined)config.childNodes.enabled = false; + //mode, set value as `child` for all other machines in the cluster + if(config.childNodes.mode === undefined)config.childNodes.mode = 'master'; + //child node connection port + if(config.childNodes.port === undefined)config.childNodes.port = 8288; + //child node connection key + if(config.childNodes.key === undefined)config.childNodes.key = [ + '3123asdasdf1dtj1hjk23sdfaasd12asdasddfdbtnkkfgvesra3asdsd3123afdsfqw345' + ]; + + return config +} diff --git a/libs/detector.js b/libs/detector.js new file mode 100644 index 0000000..7cfbc05 --- /dev/null +++ b/libs/detector.js @@ -0,0 +1,204 @@ +var P2P = require('pipe2pam'); +var PamDiff = require('pam-diff'); +module.exports = function(s,config){ + s.createPamDiffEngine = function(e){ + var width, + height, + globalSensitivity, + globalColorThreshold, + fullFrame = false + if(s.group[e.ke].mon_conf[e.id].details.detector_scale_x===''||s.group[e.ke].mon_conf[e.id].details.detector_scale_y===''){ + width = s.group[e.ke].mon_conf[e.id].details.detector_scale_x; + height = s.group[e.ke].mon_conf[e.id].details.detector_scale_y; + }else{ + width = e.width + height = e.height + } + if(e.details.detector_sensitivity===''){ + globalSensitivity = 10 + }else{ + globalSensitivity = parseInt(e.details.detector_sensitivity) + } + if(e.details.detector_color_threshold===''){ + globalColorThreshold = 9 + }else{ + globalColorThreshold = parseInt(e.details.detector_color_threshold) + } + + globalThreshold = parseInt(e.details.detector_threshold) || 0 + + var regionJson + try{ + regionJson = JSON.parse(s.group[e.ke].mon_conf[e.id].details.cords) + }catch(err){ + regionJson = s.group[e.ke].mon_conf[e.id].details.cords + } + + if(Object.keys(regionJson).length === 0 || e.details.detector_frame === '1'){ + fullFrame = { + name:'FULL_FRAME', + sensitivity:globalSensitivity, + color_threshold:globalColorThreshold, + points:[ + [0,0], + [0,height], + [width,height], + [width,0] + ] + }; + } + + e.triggerTimer = {} + + var regions = s.createPamDiffRegionArray(regionJson,globalColorThreshold,globalSensitivity,fullFrame) + + s.group[e.ke].mon[e.id].pamDiff = new PamDiff({grayscale: 'luminosity', regions : regions.forPam}); + s.group[e.ke].mon[e.id].p2p = new P2P(); + var sendTrigger = function(trigger){ + var detectorObject = { + f:'trigger', + id:e.id, + ke:e.ke, + name:trigger.name, + details:{ + plug:'built-in', + name:trigger.name, + reason:'motion', + confidence:trigger.percent, + }, + plates:[], + imgHeight:height, + imgWidth:width + } + var region = Object.values(regionJson).find(x => x.name == detectorObject.name) + s.checkMaximumSensitivity(e, region, detectorObject, function() { + s.checkTriggerThreshold(e, region, detectorObject, function() { + detectorObject.doObjectDetection = (s.ocv && e.details.detector_use_detect_object === '1') + s.triggerEvent(detectorObject) + }) + }) + } + if(e.details.detector_noise_filter==='1'){ + if(!s.group[e.ke].mon[e.id].noiseFilterArray)s.group[e.ke].mon[e.id].noiseFilterArray = {} + var noiseFilterArray = s.group[e.ke].mon[e.id].noiseFilterArray + Object.keys(regions.notForPam).forEach(function(name){ + if(!noiseFilterArray[name])noiseFilterArray[name]=[]; + }) + s.group[e.ke].mon[e.id].pamDiff.on('diff', (data) => { + data.trigger.forEach(function(trigger){ + s.filterTheNoise(e,noiseFilterArray,regions,trigger,function(){ + sendTrigger(trigger) + }) + }) + }) + }else{ + s.group[e.ke].mon[e.id].pamDiff.on('diff', (data) => { + data.trigger.forEach(sendTrigger) + }) + } + } + + s.createPamDiffRegionArray = function(regions,globalColorThreshold,globalSensitivity,fullFrame){ + var pamDiffCompliantArray = [], + arrayForOtherStuff = [], + json + try{ + json = JSON.parse(regions) + }catch(err){ + json = regions + } + if(fullFrame){ + json[fullFrame.name]=fullFrame; + } + Object.values(json).forEach(function(region){ + if(!region)return false; + region.polygon = []; + region.points.forEach(function(points){ + region.polygon.push({x:parseFloat(points[0]),y:parseFloat(points[1])}) + }) + if(region.sensitivity===''){ + region.sensitivity = globalSensitivity + }else{ + region.sensitivity = parseInt(region.sensitivity) + } + if(region.color_threshold===''){ + region.color_threshold = globalColorThreshold + }else{ + region.color_threshold = parseInt(region.color_threshold) + } + pamDiffCompliantArray.push({name: region.name, difference: region.color_threshold, percent: region.sensitivity, polygon:region.polygon}) + arrayForOtherStuff[region.name] = region; + }) + if(pamDiffCompliantArray.length===0){pamDiffCompliantArray = null} + return {forPam:pamDiffCompliantArray,notForPam:arrayForOtherStuff}; + } + + s.filterTheNoise = function(e,noiseFilterArray,regions,trigger,callback){ + if(noiseFilterArray[trigger.name].length > 2){ + var thePreviousTriggerPercent = noiseFilterArray[trigger.name][noiseFilterArray[trigger.name].length - 1]; + var triggerDifference = trigger.percent - thePreviousTriggerPercent; + var noiseRange = e.details.detector_noise_filter_range + if(!noiseRange || noiseRange === ''){ + noiseRange = 6 + } + noiseRange = parseFloat(noiseRange) + if(((trigger.percent - thePreviousTriggerPercent) < noiseRange)||(thePreviousTriggerPercent - trigger.percent) > -noiseRange){ + noiseFilterArray[trigger.name].push(trigger.percent); + } + }else{ + noiseFilterArray[trigger.name].push(trigger.percent); + } + if(noiseFilterArray[trigger.name].length > 10){ + noiseFilterArray[trigger.name] = noiseFilterArray[trigger.name].splice(1,10) + } + var theNoise = 0; + noiseFilterArray[trigger.name].forEach(function(v,n){ + theNoise += v; + }) + theNoise = theNoise / noiseFilterArray[trigger.name].length; + var triggerPercentWithoutNoise = trigger.percent - theNoise; + if(triggerPercentWithoutNoise > regions.notForPam[trigger.name].sensitivity){ + callback(trigger) + } + } + + s.checkMaximumSensitivity = function(monitor, region, detectorObject, success) { + var logName = detectorObject.id + ':' + detectorObject.name + var globalMaxSensitivity = parseInt(monitor.details.detector_max_sensitivity) || undefined + var maxSensitivity = parseInt(region.max_sensitivity) || globalMaxSensitivity + if (maxSensitivity === undefined || detectorObject.details.confidence <= maxSensitivity) { + success() + } else { + if (monitor.triggerTimer[detectorObject.name] !== undefined) { + clearTimeout(monitor.triggerTimer[detectorObject.name].timeout) + monitor.triggerTimer[detectorObject.name] = undefined + } + } + } + + s.checkTriggerThreshold = function(monitor, region, detectorObject, success){ + var threshold = parseInt(region.threshold) || globalThreshold + if (threshold <= 1) { + success() + } else { + if (monitor.triggerTimer[detectorObject.name] === undefined) { + monitor.triggerTimer[detectorObject.name] = { + count : threshold, + timeout : null + } + } + if (--monitor.triggerTimer[detectorObject.name].count == 0) { + success() + clearTimeout(monitor.triggerTimer[detectorObject.name].timeout) + monitor.triggerTimer[detectorObject.name] = undefined + } else { + var fps = parseFloat(monitor.details.detector_fps) || 2 + if (monitor.triggerTimer[detectorObject.name].timeout !== null) + clearTimeout(monitor.triggerTimer[detectorObject.name].timeout) + monitor.triggerTimer[detectorObject.name].timeout = setTimeout(function() { + monitor.triggerTimer[detectorObject.name] = undefined + }, ((threshold+0.5) * 1000) / fps) + } + } + } +} diff --git a/libs/events.js b/libs/events.js new file mode 100644 index 0000000..8e134d6 --- /dev/null +++ b/libs/events.js @@ -0,0 +1,384 @@ +var moment = require('moment'); +var execSync = require('child_process').execSync; +var exec = require('child_process').exec; +var spawn = require('child_process').spawn; +var request = require('request'); +module.exports = function(s,config,lang){ + s.filterEvents = function(x,d){ + switch(x){ + case'archive': + d.videos.forEach(function(v,n){ + s.video('archive',v) + }) + break; + case'delete': + d.videos.forEach(function(v,n){ + s.deleteVideo(v) + }) + break; + case'execute': + exec(d.execute,{detached: true}) + break; + } + s.onEventTriggerBeforeFilterExtensions.forEach(function(extender){ + extender(x,d) + }) + } + s.triggerEvent = function(d){ + var filter = { + halt : false, + addToMotionCounter : true, + useLock : true, + save : true, + webhook : true, + command : true, + record : true, + indifference : false + } + s.onEventTriggerBeforeFilterExtensions.forEach(function(extender){ + extender(d,filter) + }) + if(s.group[d.ke].mon[d.id].open){ + d.details.videoTime = s.group[d.ke].mon[d.id].open; + } + var detailString = JSON.stringify(d.details); + if(!s.group[d.ke]||!s.group[d.ke].mon[d.id]){ + return s.systemLog(lang['No Monitor Found, Ignoring Request']) + } + d.mon=s.group[d.ke].mon_conf[d.id]; + var currentConfig = s.group[d.ke].mon[d.id].details + //read filters + if( + currentConfig.use_detector_filters === '1' && + ((currentConfig.use_detector_filters_object === '1' && d.details.matrices) || + currentConfig.use_detector_filters_object !== '1') + ){ + var parseValue = function(key,val){ + var newVal + switch(val){ + case'': + newVal = filter[key] + break; + case'0': + newVal = false + break; + case'1': + newVal = true + break; + default: + newVal = val + break; + } + return newVal + } + var filters = currentConfig.detector_filters + Object.keys(filters).forEach(function(key){ + var conditionChain = {} + var dFilter = filters[key] + dFilter.where.forEach(function(condition,place){ + conditionChain[place] = {ok:false,next:condition.p4,matrixCount:0} + if(d.details.matrices)conditionChain[place].matrixCount = d.details.matrices.length + var modifyFilters = function(toCheck,matrixPosition){ + var param = toCheck[condition.p1] + var pass = function(){ + if(matrixPosition && dFilter.actions.halt === '1'){ + delete(d.details.matrices[matrixPosition]) + }else{ + conditionChain[place].ok = true + } + } + switch(condition.p2){ + case'indexOf': + if(param.indexOf(condition.p3) > -1){ + pass() + } + break; + case'!indexOf': + if(param.indexOf(condition.p3) === -1){ + pass() + } + break; + default: + if(eval('param '+condition.p2+' "'+condition.p3.replace(/"/g,'\\"')+'"')){ + pass() + } + break; + } + } + switch(condition.p1){ + case'tag': + case'x': + case'y': + case'height': + case'width': + if(d.details.matrices){ + d.details.matrices.forEach(function(matrix,position){ + modifyFilters(matrix,position) + }) + } + break; + case'time': + var timeNow = new Date() + var timeCondition = new Date() + var doAtTime = condition.p3.split(':') + var atHour = parseInt(doAtTime[0]) - 1 + var atHourNow = timeNow.getHours() + var atMinuteNow = timeNow.getMinutes() + var atSecondNow = timeNow.getSeconds() + if(atHour){ + var atMinute = parseInt(doAtTime[1]) - 1 || timeNow.getMinutes() + 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')){ + conditionChain[place].ok = true + } + } + break; + default: + modifyFilters(d.details) + break; + } + }) + var conditionArray = Object.values(conditionChain) + var validationString = '' + conditionArray.forEach(function(condition,number){ + validationString += condition.ok+' ' + if(conditionArray.length-1 !== number){ + validationString += condition.next+' ' + } + }) + if(eval(validationString)){ + 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) + }) + }else{ + filter.halt = true + } + } + }) + if(d.details.matrices && d.details.matrices.length === 0 || filter.halt === true){ + return + }else if(d.details.matrices && d.details.matrices.length > 0){ + var reviewedMatrix = [] + d.details.matrices.forEach(function(matrix){ + if(matrix)reviewedMatrix.push(matrix) + }) + d.details.matrices = reviewedMatrix + } + } + //motion counter + if(filter.addToMotionCounter && filter.record){ + if(!s.group[d.ke].mon[d.id].detector_motion_count){ + s.group[d.ke].mon[d.id].detector_motion_count=0 + } + s.group[d.ke].mon[d.id].detector_motion_count+=1 + } + if(filter.useLock){ + if(s.group[d.ke].mon[d.id].motion_lock){ + return + } + var detector_lock_timeout + if(!currentConfig.detector_lock_timeout||currentConfig.detector_lock_timeout===''){ + detector_lock_timeout = 2000 + } + detector_lock_timeout = parseFloat(currentConfig.detector_lock_timeout); + if(!s.group[d.ke].mon[d.id].detector_lock_timeout){ + s.group[d.ke].mon[d.id].detector_lock_timeout=setTimeout(function(){ + clearTimeout(s.group[d.ke].mon[d.id].detector_lock_timeout) + delete(s.group[d.ke].mon[d.id].detector_lock_timeout) + },detector_lock_timeout) + }else{ + return + } + } + // check modified indifference + if(filter.indifference !== false && d.details.confidence < parseFloat(filter.indifference)){ + // fails indifference check for modified indifference + return + } + // + if(d.doObjectDetection === true){ + s.ocvTx({ + f : 'frame', + mon : s.group[d.ke].mon_conf[d.id].details, + ke : d.ke, + id : d.id, + time : s.formattedTime(), + frame : s.group[d.ke].mon[d.id].lastJpegDetectorFrame + }) + }else{ + //save this detection result in SQL, only coords. not image. + if(filter.save && currentConfig.detector_save==='1'){ + s.sqlQuery('INSERT INTO Events (ke,mid,details) VALUES (?,?,?)',[d.ke,d.id,detailString]) + } + if(currentConfig.detector_notrigger === '1'){ + var detector_notrigger_timeout + if(!currentConfig.detector_notrigger_timeout||currentConfig.detector_notrigger_timeout===''){ + detector_notrigger_timeout = 10 + } + detector_notrigger_timeout = parseFloat(currentConfig.detector_notrigger_timeout)*1000*60; + s.group[d.ke].mon[d.id].detector_notrigger_timeout = detector_notrigger_timeout; + clearInterval(s.group[d.ke].mon[d.id].detector_notrigger_timeout) + s.group[d.ke].mon[d.id].detector_notrigger_timeout = setInterval(s.group[d.ke].mon[d.id].detector_notrigger_timeout_function,detector_notrigger_timeout) + } + var detector_timeout + if(!currentConfig.detector_timeout||currentConfig.detector_timeout===''){ + detector_timeout = 10 + }else{ + detector_timeout = parseFloat(currentConfig.detector_timeout) + } + if(filter.record && d.mon.mode=='start'&¤tConfig.detector_trigger==='1'&¤tConfig.detector_record_method==='sip'){ + s.createEventBasedRecording(d) + }else if(filter.record && d.mon.mode!=='stop'&¤tConfig.detector_trigger=='1'&¤tConfig.detector_record_method==='hot'){ + if(!d.auth){ + d.auth=s.gid(); + } + if(!s.group[d.ke].users[d.auth]){ + s.group[d.ke].users[d.auth]={system:1,details:{},lang:lang} + } + d.urlQuery = [] + d.url = 'http://'+config.ip+':'+config.port+'/'+d.auth+'/monitor/'+d.ke+'/'+d.id+'/record/'+detector_timeout+'/min'; + if(currentConfig.watchdog_reset!=='0'){ + d.urlQuery.push('reset=1') + } + if(currentConfig.detector_trigger_record_fps&¤tConfig.detector_trigger_record_fps!==''&¤tConfig.detector_trigger_record_fps!=='0'){ + d.urlQuery.push('fps='+currentConfig.detector_trigger_record_fps) + } + if(d.urlQuery.length>0){ + d.url+='?'+d.urlQuery.join('&') + } + request({url:d.url,method:'GET'},function(err,data){ + if(err){ + //could not start hotswap + }else{ + delete(s.group[d.ke].users[d.auth]) + d.cx.f='detector_record_engaged'; + d.cx.msg = JSON.parse(data.body) + s.tx(d.cx,'GRP_'+d.ke); + } + }) + } + d.currentTime = new Date() + d.currentTimestamp = s.timeObject(d.currentTime).format() + d.screenshotName = 'Motion_'+(d.mon.name.replace(/[^\w\s]/gi,''))+'_'+d.id+'_'+d.ke+'_'+s.formattedTime() + d.screenshotBuffer = null + + s.onEventTriggerExtensions.forEach(function(extender){ + extender(d,filter) + }) + + if(filter.webhook && currentConfig.detector_webhook === '1'){ + var detector_webhook_url = currentConfig.detector_webhook_url + .replace(/{{TIME}}/g,d.currentTimestamp) + .replace(/{{REGION_NAME}}/g,d.details.name) + .replace(/{{SNAP_PATH}}/g,s.dir.streams+'/'+d.ke+'/'+d.id+'/s.jpg') + .replace(/{{MONITOR_ID}}/g,d.id) + .replace(/{{GROUP_KEY}}/g,d.ke) + .replace(/{{DETAILS}}/g,detailString) + if(d.details.confidence){ + detector_webhook_url = detector_webhook_url + .replace(/{{CONFIDENCE}}/g,d.details.confidence) + } + request({url:detector_webhook_url,method:'GET',encoding:null},function(err,data){ + if(err){ + s.userLog(d,{type:lang["Event Webhook Error"],msg:{error:err,data:data}}) + } + }) + } + + if(filter.command && currentConfig.detector_command_enable === '1' && !s.group[d.ke].mon[d.id].detector_command){ + var detector_command_timeout + if(!currentConfig.detector_command_timeout||currentConfig.detector_command_timeout===''){ + detector_command_timeout = 1000*60*10; + }else{ + detector_command_timeout = parseFloat(currentConfig.detector_command_timeout)*1000*60; + } + s.group[d.ke].mon[d.id].detector_command=setTimeout(function(){ + clearTimeout(s.group[d.ke].mon[d.id].detector_command); + delete(s.group[d.ke].mon[d.id].detector_command); + + },detector_command_timeout); + var detector_command = currentConfig.detector_command + .replace(/{{TIME}}/g,d.currentTimestamp) + .replace(/{{REGION_NAME}}/g,d.details.name) + .replace(/{{SNAP_PATH}}/g,s.dir.streams+'/'+d.ke+'/'+d.id+'/s.jpg') + .replace(/{{MONITOR_ID}}/g,d.id) + .replace(/{{GROUP_KEY}}/g,d.ke) + .replace(/{{DETAILS}}/g,detailString) + if(d.details.confidence){ + detector_command = detector_command + .replace(/{{CONFIDENCE}}/g,d.details.confidence) + } + exec(detector_command,{detached: true}) + } + } + //show client machines the event + d.cx={f:'detector_trigger',id:d.id,ke:d.ke,details:d.details,doObjectDetection:d.doObjectDetection}; + s.tx(d.cx,'DETECTOR_'+d.ke+d.id); + } + s.createEventBasedRecording = function(d){ + var currentConfig = s.group[d.ke].mon[d.id].details + var detector_timeout + if(!currentConfig.detector_timeout||currentConfig.detector_timeout===''){ + detector_timeout = 10 + }else{ + detector_timeout = parseFloat(currentConfig.detector_timeout) + } + s.group[d.ke].mon[d.id].eventBasedRecording.timeout = setTimeout(function(){ + s.group[d.ke].mon[d.id].eventBasedRecording.allowEnd=true; + },detector_timeout * 950 * 60) + if(!s.group[d.ke].mon[d.id].eventBasedRecording.process){ + if(!d.auth){ + d.auth = s.gid(60) + } + if(!s.api[d.auth]){ + s.api[d.auth] = { + system: 1, + ip: '0.0.0.0', + details: {}, + lang: lang + } + } + s.group[d.ke].mon[d.id].eventBasedRecording.allowEnd = false; + var runRecord = function(){ + var filename = s.formattedTime()+'.mp4' + s.userLog(d,{type:"Traditional Recording",msg:"Started"}) + //-t 00:'+s.timeObject(new Date(detector_timeout * 1000 * 60)).format('mm:ss')+' + s.group[d.ke].mon[d.id].eventBasedRecording.process = spawn(config.ffmpegDir,s.splitForFFPMEG(('-loglevel warning -analyzeduration 1000000 -probesize 1000000 -re -i http://'+config.ip+':'+config.port+'/'+d.auth+'/hls/'+d.ke+'/'+d.id+'/detectorStream.m3u8 -t 00:'+s.timeObject(new Date(detector_timeout * 1000 * 60)).format('mm:ss')+' -c:v copy -strftime 1 "'+s.getVideoDirectory(d.mon) + filename + '"'))) + var ffmpegError=''; + var error + s.group[d.ke].mon[d.id].eventBasedRecording.process.stderr.on('data',function(data){ + s.userLog(d,{type:"Traditional Recording",msg:data.toString()}) + }) + s.group[d.ke].mon[d.id].eventBasedRecording.process.on('close',function(){ + if(!s.group[d.ke].mon[d.id].eventBasedRecording.allowEnd){ + s.userLog(d,{type:"Traditional Recording",msg:"Detector Recording Process Exited Prematurely. Restarting."}) + runRecord() + return + } + s.insertCompletedVideo(d.mon,{ + file : filename + }) + s.userLog(d,{type:"Traditional Recording",msg:"Detector Recording Complete"}) + delete(s.api[d.auth]) + s.userLog(d,{type:"Traditional Recording",msg:'Clear Recorder Process'}) + delete(s.group[d.ke].mon[d.id].eventBasedRecording.process) + delete(s.group[d.ke].mon[d.id].eventBasedRecording.timeout) + clearTimeout(s.group[d.ke].mon[d.id].recordingChecker) + }) + } + runRecord() + } + } + s.closeEventBasedRecording = function(e){ + if(s.group[e.ke].mon[e.id].eventBasedRecording.process){ + clearTimeout(s.group[e.ke].mon[e.id].eventBasedRecording.timeout) + s.group[e.ke].mon[e.id].eventBasedRecording.allowEnd = true; + s.group[e.ke].mon[e.id].eventBasedRecording.process.kill('SIGTERM'); + } + } +} diff --git a/libs/extenders.js b/libs/extenders.js new file mode 100644 index 0000000..6278fda --- /dev/null +++ b/libs/extenders.js @@ -0,0 +1,58 @@ +module.exports = function(s,config){ + ////// USER ////// + s.loadGroupAppExtensions = [] + s.loadGroupAppExtender = function(callback){ + s.loadGroupAppExtensions.push(callback) + } + // + s.unloadGroupAppExtensions = [] + s.unloadGroupAppExtender = function(callback){ + s.unloadGroupAppExtensions.push(callback) + } + // + s.cloudDisksLoaded = [] + s.cloudDisksLoader = function(storageType){ + s.cloudDisksLoaded.push(storageType) + } + // + s.onAccountSaveExtensions = [] + s.onAccountSave = function(callback){ + s.onAccountSaveExtensions.push(callback) + } + // + s.beforeAccountSaveExtensions = [] + s.beforeAccountSave = function(callback){ + s.beforeAccountSaveExtensions.push(callback) + } + // + s.onTwoFactorAuthCodeNotificationExtensions = [] + s.onTwoFactorAuthCodeNotification = function(callback){ + s.onTwoFactorAuthCodeNotificationExtensions.push(callback) + } + // + s.cloudDiskUseStartupExtensions = {} + + ////// EVENTS ////// + s.onEventTriggerExtensions = [] + s.onEventTrigger = function(callback){ + s.onEventTriggerExtensions.push(callback) + } + s.onEventTriggerBeforeFilterExtensions = [] + s.onEventTriggerBeforeFilter = function(callback){ + s.onEventTriggerBeforeFilterExtensions.push(callback) + } + s.onFilterEventExtensions = [] + s.onFilterEvent = function(callback){ + s.onFilterEventExtensions.push(callback) + } + + ////// MONITOR ////// + s.onMonitorInitExtensions = [] + s.onMonitorInit = function(callback){ + s.onMonitorInitExtensions.push(callback) + } + s.onDetectorNoTriggerTimeoutExtensions = [] + s.onDetectorNoTriggerTimeout = function(callback){ + s.onDetectorNoTriggerTimeoutExtensions.push(callback) + } +} diff --git a/libs/ffmpeg.js b/libs/ffmpeg.js new file mode 100644 index 0000000..788fa71 --- /dev/null +++ b/libs/ffmpeg.js @@ -0,0 +1,910 @@ +var fs = require('fs'); +var spawn = require('child_process').spawn; +var execSync = require('child_process').execSync; +module.exports = function(s,config,onFinish){ + var ffmpeg = {} + var downloadingFfmpeg = false; + //check local ffmpeg + ffmpeg.checkForWindows = function(failback){ + if (s.isWin && fs.existsSync(s.mainDirectory+'/ffmpeg/ffmpeg.exe')) { + config.ffmpegDir = s.mainDirectory+'/ffmpeg/ffmpeg.exe' + }else{ + failback() + } + } + //check local ffmpeg + ffmpeg.checkForUnix = function(failback){ + if(s.isWin === false){ + if (fs.existsSync('/usr/bin/ffmpeg')) { + config.ffmpegDir = '/usr/bin/ffmpeg' + }else{ + if (fs.existsSync('/usr/local/bin/ffmpeg')) { + config.ffmpegDir = '/usr/local/bin/ffmpeg' + }else{ + failback() + } + } + }else{ + failback() + } + } + //check node module : ffmpeg-static + ffmpeg.checkForNpmStatic = function(failback){ + try{ + var staticFFmpeg = require('ffmpeg-static').path; + if (fs.statSync(staticFFmpeg)) { + config.ffmpegDir = staticFFmpeg + }else{ + console.log('"ffmpeg-static" from NPM has failed to provide a compatible library or has been corrupted.') + console.log('Run "npm uninstall ffmpeg-static" to remove it.') + console.log('Run "npm install ffbinaries" to get a different static FFmpeg downloader.') + } + }catch(err){ + console.log('No "ffmpeg-static".') + failback() + } + } + //check node module : ffbinaries + ffmpeg.checkForFfbinary = function(failback){ + try{ + ffbinaries = require('ffbinaries') + var ffbinaryDir = s.mainDirectory + '/ffmpeg/' + var downloadFFmpeg = function(){ + downloadingFfmpeg = true + console.log('ffbinaries : Downloading FFmpeg. Please Wait...'); + ffbinaries.downloadBinaries(['ffmpeg', 'ffprobe'], { + destination: ffbinaryDir, + version : '3.4' + },function () { + config.ffmpegDir = ffbinaryDir + 'ffmpeg' + console.log('ffbinaries : FFmpeg Downloaded.'); + ffmpeg.completeCheck() + }) + } + if (!fs.existsSync(ffbinaryDir + 'ffmpeg')) { + downloadFFmpeg() + }else{ + config.ffmpegDir = ffbinaryDir + 'ffmpeg' + } + }catch(err){ + console.log('No "ffbinaries". Continuing.') + console.log('Run "npm install ffbinaries" to get this static FFmpeg downloader.') + failback() + } + } + //ffmpeg version + ffmpeg.checkVersion = function(callback){ + try{ + s.ffmpegVersion = execSync(config.ffmpegDir+" -version").toString().split('Copyright')[0].replace('ffmpeg version','').trim() + if(s.ffmpegVersion.indexOf(': 2.')>-1){ + s.systemLog('FFMPEG is too old : '+s.ffmpegVersion+', Needed : 3.2+',err) + throw (new Error()) + } + }catch(err){ + console.log('No FFmpeg found.') + // process.exit() + } + callback() + } + //check available hardware acceleration methods + ffmpeg.checkHwAccelMethods = function(callback){ + if(config.availableHWAccels === undefined){ + hwAccels = execSync(config.ffmpegDir+" -loglevel quiet -hwaccels").toString().split('\n') + hwAccels.shift() + availableHWAccels = [] + hwAccels.forEach(function(method){ + if(method && method !== '')availableHWAccels.push(method.trim()) + }) + config.availableHWAccels = availableHWAccels + config.availableHWAccels = ['auto'].concat(config.availableHWAccels) + console.log('Available Hardware Acceleration Methods : ',availableHWAccels.join(', ')) + } + callback() + } + ffmpeg.completeCheck = function(){ + ffmpeg.checkVersion(function(){ + ffmpeg.checkHwAccelMethods(function(){ + onFinish(ffmpeg) + }) + }) + } + //ffmpeg string cleaner, splits for use with spawn() + s.splitForFFPMEG = function (ffmpegCommandAsString) { + //this function ignores spaces inside quotes. + return ffmpegCommandAsString.replace(/\s+/g,' ').trim().match(/\\?.|^$/g).reduce((p, c) => { + if(c === '"'){ + p.quote ^= 1; + }else if(!p.quote && c === ' '){ + p.a.push(''); + }else{ + p.a[p.a.length-1] += c.replace(/\\(.)/,"$1"); + } + return p; + }, {a: ['']}).a + }; + s.createFFmpegMap = function(e,arrayOfMaps){ + //`e` is the monitor object + var string = ''; + if(e.details.input_maps && e.details.input_maps.length > 0){ + if(arrayOfMaps && arrayOfMaps instanceof Array && arrayOfMaps.length>0){ + arrayOfMaps.forEach(function(v){ + if(v.map==='')v.map='0' + string += ' -map '+v.map + }) + }else{ + string += ' -map 0:0' + } + } + return string; + } + s.createInputMap = function(e,number,input){ + //`e` is the monitor object + //`x` is an object used to contain temporary values. + var x = {} + x.cust_input = '' + x.hwaccel = '' + if(input.cust_input&&input.cust_input!==''){x.cust_input+=' '+input.cust_input} + //input - analyze duration + if(input.aduration&&input.aduration!==''){x.cust_input+=' -analyzeduration '+input.aduration} + //input - probe size + if(input.probesize&&input.probesize!==''){x.cust_input+=' -probesize '+input.probesize} + //input - stream loop (good for static files/lists) + if(input.stream_loop === '1'){x.cust_input+=' -stream_loop -1'} + //input - fps + if(x.cust_input.indexOf('-r ')===-1&&input.sfps&&input.sfps!==''){ + input.sfps=parseFloat(input.sfps); + if(isNaN(input.sfps)){input.sfps=1} + x.cust_input+=' -r '+input.sfps + } + //input - is mjpeg + if(input.type==='mjpeg'){ + if(x.cust_input.indexOf('-f ')===-1){ + x.cust_input+=' -f mjpeg' + } + //input - frames per second + x.cust_input+=' -reconnect 1' + }else + //input - is h264 has rtsp in address and transport method is chosen + if((input.type==='h264'||input.type==='mp4')&&input.fulladdress.indexOf('rtsp://')>-1&&input.rtsp_transport!==''&&input.rtsp_transport!=='no'){ + x.cust_input += ' -rtsp_transport '+input.rtsp_transport + }else + if((input.type==='mp4'||input.type==='mjpeg')&&x.cust_input.indexOf('-re')===-1){ + x.cust_input += ' -re' + } + //hardware acceleration + if(input.accelerator&&input.accelerator==='1'){ + if(input.hwaccel&&input.hwaccel!==''){ + x.hwaccel+=' -hwaccel '+input.hwaccel; + } + if(input.hwaccel_vcodec&&input.hwaccel_vcodec!==''&&input.hwaccel_vcodec!=='auto'&&input.hwaccel_vcodec!=='no'){ + x.hwaccel+=' -c:v '+input.hwaccel_vcodec; + } + if(input.hwaccel_device&&input.hwaccel_device!==''){ + switch(input.hwaccel){ + case'vaapi': + x.hwaccel+=' -vaapi_device '+input.hwaccel_device+' -hwaccel_output_format vaapi'; + break; + default: + x.hwaccel+=' -hwaccel_device '+input.hwaccel_device; + break; + } + } + } + //custom - input flags + return x.hwaccel+x.cust_input+' -i "'+input.fulladdress+'"'; + } + //create sub stream channel + s.createStreamChannel = function(e,number,channel){ + //`e` is the monitor object + //`x` is an object used to contain temporary values. + var x = { + pipe:'' + } + if(!number||number==''){ + x.channel_sdir = e.sdir; + }else{ + x.channel_sdir = e.sdir+'channel'+number+'/'; + if (!fs.existsSync(x.channel_sdir)){ + fs.mkdirSync(x.channel_sdir); + } + } + x.stream_video_filters=[] + //stream - frames per second + if(channel.stream_vcodec!=='copy'){ + if(!channel.stream_fps||channel.stream_fps===''){ + switch(channel.stream_type){ + case'rtmp': + channel.stream_fps=30 + break; + default: + // channel.stream_fps=5 + break; + } + } + } + if(channel.stream_fps&&channel.stream_fps!==''){x.stream_fps=' -r '+channel.stream_fps}else{x.stream_fps=''} + + //stream - hls vcodec + if(channel.stream_vcodec&&channel.stream_vcodec!=='no'){ + if(channel.stream_vcodec!==''){x.stream_vcodec=' -c:v '+channel.stream_vcodec}else{x.stream_vcodec=' -c:v libx264'} + }else{ + x.stream_vcodec=''; + } + //stream - hls acodec + if(channel.stream_acodec!=='no'){ + if(channel.stream_acodec&&channel.stream_acodec!==''){x.stream_acodec=' -c:a '+channel.stream_acodec}else{x.stream_acodec=''} + }else{ + x.stream_acodec=' -an'; + } + //stream - resolution + if(channel.stream_scale_x&&channel.stream_scale_x!==''&&channel.stream_scale_y&&channel.stream_scale_y!==''){ + x.dimensions = channel.stream_scale_x+'x'+channel.stream_scale_y; + } + //stream - hls segment time + if(channel.hls_time&&channel.hls_time!==''){x.hls_time=channel.hls_time}else{x.hls_time="2"} + //hls list size + if(channel.hls_list_size&&channel.hls_list_size!==''){x.hls_list_size=channel.hls_list_size}else{x.hls_list_size=2} + //stream - custom flags + if(channel.cust_stream&&channel.cust_stream!==''){x.cust_stream=' '+channel.cust_stream}else{x.cust_stream=''} + //stream - preset + if(channel.stream_type !== 'h265' && channel.preset_stream && channel.preset_stream!==''){x.preset_stream=' -preset '+channel.preset_stream;}else{x.preset_stream=''} + //hardware acceleration + if(e.details.accelerator&&e.details.accelerator==='1'){ + if(e.details.hwaccel === 'auto')e.details.hwaccel = '' + if(e.details.hwaccel && e.details.hwaccel!==''){ + x.hwaccel+=' -hwaccel '+e.details.hwaccel; + } + if(e.details.hwaccel_vcodec&&e.details.hwaccel_vcodec!==''){ + x.hwaccel+=' -c:v '+e.details.hwaccel_vcodec; + } + if(e.details.hwaccel_device&&e.details.hwaccel_device!==''){ + switch(e.details.hwaccel){ + case'vaapi': + x.hwaccel+=' -vaapi_device '+e.details.hwaccel_device+' -hwaccel_output_format vaapi'; + break; + default: + x.hwaccel+=' -hwaccel_device '+e.details.hwaccel_device; + break; + } + } + // else{ + // if(e.details.hwaccel==='vaapi'){ + // x.hwaccel+=' -hwaccel_device 0'; + // } + // } + } + + if(channel.rotate_stream&&channel.rotate_stream!==""&&channel.rotate_stream!=="no"){ + x.stream_video_filters.push('transpose='+channel.rotate_stream); + } + //stream - video filter + if(channel.svf&&channel.svf!==''){ + x.stream_video_filters.push(channel.svf) + } + if(x.stream_video_filters.length>0){ + var string = x.stream_video_filters.join(',').trim() + if(string===''){ + x.stream_video_filters='' + }else{ + x.stream_video_filters=' -vf '+string + } + }else{ + x.stream_video_filters='' + } + if(e.details.input_map_choices&&e.details.input_map_choices.record){ + //add input feed map + x.pipe += s.createFFmpegMap(e,e.details.input_map_choices['stream_channel-'+(number-config.pipeAddition)]) + } + if(channel.stream_vcodec !== 'copy' || channel.stream_type === 'mjpeg' || channel.stream_type === 'b64'){ + x.cust_stream += x.stream_fps + } + switch(channel.stream_type){ + case'mp4': + x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream" -reset_timestamps 1' + if(channel.stream_vcodec!=='copy'){ + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; + x.cust_stream+=x.preset_stream + x.cust_stream+=x.stream_video_filters + } + x.pipe+=' -f mp4'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; + break; + case'rtmp': + x.rtmp_server_url=s.checkCorrectPathEnding(channel.rtmp_server_url); + if(channel.stream_vcodec!=='copy'){ + if(channel.stream_vcodec==='libx264'){ + channel.stream_vcodec = 'h264' + } + if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; + x.cust_stream+=x.preset_stream + if(channel.stream_v_br&&channel.stream_v_br!==''){x.cust_stream+=' -b:v '+channel.stream_v_br} + } + if(channel.stream_vcodec!=='no'&&channel.stream_vcodec!==''){ + x.cust_stream+=' -vcodec '+channel.stream_vcodec + } + if(channel.stream_acodec!=='copy'){ + if(!channel.stream_acodec||channel.stream_acodec===''||channel.stream_acodec==='no'){ + channel.stream_acodec = 'aac' + } + if(!channel.stream_a_br||channel.stream_a_br===''){channel.stream_a_br='128k'} + x.cust_stream+=' -ab '+channel.stream_a_br + } + if(channel.stream_acodec!==''){ + x.cust_stream+=' -acodec '+channel.stream_acodec + } + x.pipe+=' -f flv'+x.stream_video_filters+x.cust_stream+' "'+x.rtmp_server_url+channel.rtmp_stream_key+'"'; + break; + case'h264': + if(channel.stream_vcodec!=='copy'){ + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; + x.cust_stream+=x.preset_stream + x.cust_stream+=x.stream_video_filters + } + x.pipe+=' -f mpegts'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; + break; + case'flv': + if(channel.stream_vcodec!=='copy'){ + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; + x.cust_stream+=x.preset_stream + x.cust_stream+=x.stream_video_filters + } + x.pipe+=' -f flv'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:'+number; + break; + case'hls': + if(channel.stream_vcodec!=='h264_vaapi'&&channel.stream_vcodec!=='copy'){ + if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -crf '+channel.stream_quality; + if(x.cust_stream.indexOf('-tune')===-1){x.cust_stream+=' -tune zerolatency'} + if(x.cust_stream.indexOf('-g ')===-1){x.cust_stream+=' -g 1'} + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + x.cust_stream+=x.stream_video_filters + } + x.pipe+=x.preset_stream+x.stream_acodec+x.stream_vcodec+' -f hls'+x.cust_stream+' -hls_time '+x.hls_time+' -hls_list_size '+x.hls_list_size+' -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+x.channel_sdir+'s.m3u8"'; + break; + case'mjpeg': + if(channel.stream_quality && channel.stream_quality !== '')x.cust_stream+=' -q:v '+channel.stream_quality; + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + x.pipe+=' -c:v mjpeg -f mpjpeg -boundary_tag shinobi'+x.cust_stream+x.stream_video_filters+' pipe:'+number; + break; + default: + x.pipe='' + break; + } + return x.pipe + } + ffmpeg.buildMainInput = function(e,x){ + //e = monitor object + //x = temporary values + //check if CUDA is enabled + e.isStreamer = (e.type === 'dashcam'|| e.type === 'socket') + if(e.details.accelerator === '1' && e.details.hwaccel === 'cuvid' && e.details.hwaccel_vcodec === ('h264_cuvid' || 'hevc_cuvid' || 'mjpeg_cuvid' || 'mpeg4_cuvid')){ + e.cudaEnabled = true + } + // + x.hwaccel = '' + x.cust_input = '' + //input - frame rate (capture rate) + if(e.details.sfps && e.details.sfps!==''){x.input_fps=' -r '+e.details.sfps}else{x.input_fps=''} + //input - analyze duration + if(e.details.aduration&&e.details.aduration!==''){x.cust_input+=' -analyzeduration '+e.details.aduration}; + //input - probe size + if(e.details.probesize&&e.details.probesize!==''){x.cust_input+=' -probesize '+e.details.probesize}; + //input - stream loop (good for static files/lists) + if(e.details.stream_loop === '1' && (e.type === 'mp4' || e.type === 'local')){x.cust_input+=' -stream_loop -1'}; + //input + if(e.details.cust_input.indexOf('-fflags') === -1){x.cust_input+=' -fflags +igndts'} + switch(e.type){ + case'h264': + switch(e.protocol){ + case'rtsp': + if(e.details.rtsp_transport&&e.details.rtsp_transport!==''&&e.details.rtsp_transport!=='no'){x.cust_input+=' -rtsp_transport '+e.details.rtsp_transport;} + break; + } + break; + } + //hardware acceleration + if(e.details.accelerator && e.details.accelerator==='1' && e.isStreamer === false){ + if(e.details.hwaccel&&e.details.hwaccel!==''){ + x.hwaccel+=' -hwaccel '+e.details.hwaccel; + } + if(e.details.hwaccel_vcodec&&e.details.hwaccel_vcodec!==''){ + x.hwaccel+=' -c:v '+e.details.hwaccel_vcodec; + } + if(e.details.hwaccel_device&&e.details.hwaccel_device!==''){ + switch(e.details.hwaccel){ + case'vaapi': + x.hwaccel+=' -vaapi_device '+e.details.hwaccel_device; + break; + default: + x.hwaccel+=' -hwaccel_device '+e.details.hwaccel_device; + break; + } + } + // else{ + // if(e.details.hwaccel==='vaapi'){ + // x.hwaccel+=' -hwaccel_device 0'; + // } + // } + } + //logging - level + if(e.details.loglevel&&e.details.loglevel!==''){x.loglevel='-loglevel '+e.details.loglevel;}else{x.loglevel='-loglevel error'} + //custom - input flags + if(e.details.cust_input&&e.details.cust_input!==''){x.cust_input+=' '+e.details.cust_input;} + //add main input + if((e.type === 'mp4' || e.type === 'mjpeg') && x.cust_input.indexOf('-re') === -1){ + x.cust_input += ' -re' + } + } + ffmpeg.buildMainStream = function(e,x){ + //e = monitor object + //x = temporary values + x.stream_video_filters = [] + x.pipe = '' + //stream - timestamp + if(e.details.stream_timestamp&&e.details.stream_timestamp=="1"&&e.details.vcodec!=='copy'){ + //font + if(e.details.stream_timestamp_font&&e.details.stream_timestamp_font!==''){x.stream_timestamp_font=e.details.stream_timestamp_font}else{x.stream_timestamp_font='/usr/share/fonts/truetype/freefont/FreeSans.ttf'} + //position x + if(e.details.stream_timestamp_x&&e.details.stream_timestamp_x!==''){x.stream_timestamp_x=e.details.stream_timestamp_x}else{x.stream_timestamp_x='(w-tw)/2'} + //position y + if(e.details.stream_timestamp_y&&e.details.stream_timestamp_y!==''){x.stream_timestamp_y=e.details.stream_timestamp_y}else{x.stream_timestamp_y='0'} + //text color + if(e.details.stream_timestamp_color&&e.details.stream_timestamp_color!==''){x.stream_timestamp_color=e.details.stream_timestamp_color}else{x.stream_timestamp_color='white'} + //box color + if(e.details.stream_timestamp_box_color&&e.details.stream_timestamp_box_color!==''){x.stream_timestamp_box_color=e.details.stream_timestamp_box_color}else{x.stream_timestamp_box_color='0x00000000@1'} + //text size + if(e.details.stream_timestamp_font_size&&e.details.stream_timestamp_font_size!==''){x.stream_timestamp_font_size=e.details.stream_timestamp_font_size}else{x.stream_timestamp_font_size='10'} + + x.stream_video_filters.push('drawtext=fontfile='+x.stream_timestamp_font+':text=\'%{localtime}\':x='+x.stream_timestamp_x+':y='+x.stream_timestamp_y+':fontcolor='+x.stream_timestamp_color+':box=1:boxcolor='+x.stream_timestamp_box_color+':fontsize='+x.stream_timestamp_font_size); + } + //stream - watermark for -vf + if(e.details.stream_watermark&&e.details.stream_watermark=="1"&&e.details.stream_watermark_location&&e.details.stream_watermark_location!==''){ + switch(e.details.stream_watermark_position){ + case'tl'://top left + x.stream_watermark_position='10:10' + break; + case'tr'://top right + x.stream_watermark_position='main_w-overlay_w-10:10' + break; + case'bl'://bottom left + x.stream_watermark_position='10:main_h-overlay_h-10' + break; + default://bottom right + x.stream_watermark_position='(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' + break; + } + x.stream_video_filters.push('movie='+e.details.stream_watermark_location+'[watermark],[in][watermark]overlay='+x.stream_watermark_position+'[out]'); + } + //stream - rotation + if(e.details.rotate_stream&&e.details.rotate_stream!==""&&e.details.rotate_stream!=="no"&&e.details.stream_vcodec!=='copy'){ + x.stream_video_filters.push('transpose='+e.details.rotate_stream); + } + //stream - hls vcodec + if(e.details.stream_vcodec&&e.details.stream_vcodec!=='no'){ + if(e.details.stream_vcodec!==''){x.stream_vcodec=' -c:v '+e.details.stream_vcodec}else{x.stream_vcodec=' -c:v libx264'} + }else{ + x.stream_vcodec=''; + } + //stream - hls acodec + if(e.details.stream_acodec!=='no'){ + if(e.details.stream_acodec&&e.details.stream_acodec!==''){x.stream_acodec=' -c:a '+e.details.stream_acodec}else{x.stream_acodec=''} + }else{ + x.stream_acodec=' -an'; + } + //stream - hls segment time + if(e.details.hls_time&&e.details.hls_time!==''){x.hls_time=e.details.hls_time}else{x.hls_time="2"} //hls list size + if(e.details.hls_list_size&&e.details.hls_list_size!==''){x.hls_list_size=e.details.hls_list_size}else{x.hls_list_size=2} + //stream - custom flags + if(e.details.cust_stream&&e.details.cust_stream!==''){x.cust_stream=' '+e.details.cust_stream}else{x.cust_stream=''} + //stream - preset + if(e.details.stream_type !== 'h265' && e.details.preset_stream && e.details.preset_stream !== ''){x.preset_stream=' -preset '+e.details.preset_stream;}else{x.preset_stream=''} + + if(e.details.stream_vcodec==='h264_vaapi'){ + x.stream_video_filters=[] + x.stream_video_filters.push('format=nv12,hwupload'); + if(e.details.stream_scale_x&&e.details.stream_scale_x!==''&&e.details.stream_scale_y&&e.details.stream_scale_y!==''){ + x.stream_video_filters.push('scale_vaapi=w='+e.details.stream_scale_x+':h='+e.details.stream_scale_y) + } + } + if(e.cudaEnabled && (e.details.stream_type === 'mjpeg' || e.details.stream_type === 'b64')){ + x.stream_video_filters.push('hwdownload,format=nv12') + } + //stream - video filter + if(e.details.svf && e.details.svf !== ''){ + x.stream_video_filters.push(e.details.svf) + } + if(x.stream_video_filters.length>0){ + x.stream_video_filters=' -vf "'+x.stream_video_filters.join(',')+'"' + }else{ + x.stream_video_filters='' + } + //stream - pipe build + if(e.details.input_map_choices&&e.details.input_map_choices.stream){ + //add input feed map + x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.stream) + } + if(e.details.stream_vcodec !== 'copy' || e.details.stream_type === 'mjpeg' || e.details.stream_type === 'b64'){ + x.cust_stream += x.stream_fps + } + switch(e.details.stream_type){ + case'mp4': + x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream" -reset_timestamps 1' + if(e.details.stream_vcodec!=='copy'){ + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; + x.cust_stream+=x.preset_stream + x.cust_stream+=x.stream_video_filters + } + x.pipe+=' -f mp4'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:1'; + break; + case'flv': + if(e.details.stream_vcodec!=='copy'){ + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; + x.cust_stream+=x.preset_stream + x.cust_stream+=x.stream_video_filters + } + x.pipe+=' -f flv'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:1'; + break; + case'hls': + if(e.details.stream_vcodec!=='h264_vaapi'&&e.details.stream_vcodec!=='copy'){ + if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; + if(x.cust_stream.indexOf('-tune')===-1){x.cust_stream+=' -tune zerolatency'} + if(x.cust_stream.indexOf('-g ')===-1){x.cust_stream+=' -g 1'} + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + x.cust_stream+=x.stream_video_filters + } + x.pipe+=x.preset_stream+x.stream_acodec+x.stream_vcodec+' -f hls'+x.cust_stream+' -hls_time '+x.hls_time+' -hls_list_size '+x.hls_list_size+' -start_number 0 -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'s.m3u8"'; + break; + case'mjpeg': + if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -q:v '+e.details.stream_quality; + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + x.pipe+=' -an -c:v mjpeg -f mpjpeg -boundary_tag shinobi'+x.cust_stream+x.stream_video_filters+' pipe:1'; + break; + case'h265': + x.cust_stream+=' -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Shinobi H.265 Stream" -reset_timestamps 1' + if(e.details.stream_vcodec!=='copy'){ + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -crf '+e.details.stream_quality; + x.cust_stream+=x.preset_stream + x.cust_stream+=x.stream_video_filters + } + x.pipe+=' -f hevc'+x.stream_acodec+x.stream_vcodec+x.cust_stream+' pipe:1'; + break; + case'b64':case'':case undefined:case null://base64 + if(e.details.stream_quality && e.details.stream_quality !== '')x.cust_stream+=' -q:v '+e.details.stream_quality; + if(x.dimensions && x.cust_stream.indexOf('-s ')===-1){x.cust_stream+=' -s '+x.dimensions} + x.pipe+=' -an -c:v mjpeg -f image2pipe'+x.cust_stream+x.stream_video_filters+' pipe:1'; + break; + default: + x.pipe='' + break; + } + if(e.details.stream_channels){ + e.details.stream_channels.forEach(function(v,n){ + x.pipe += s.createStreamChannel(e,n+config.pipeAddition,v) + }) + } + //api - snapshot bin/ cgi.bin (JPEG Mode) + if(e.details.snap === '1'){ + if(e.details.input_map_choices&&e.details.input_map_choices.snap){ + //add input feed map + x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.snap) + } + if(!e.details.snap_fps || e.details.snap_fps === ''){e.details.snap_fps = 1} + if(e.details.snap_vf && e.details.snap_vf !== '' || e.cudaEnabled){ + var snapVf = e.details.snap_vf.split(',') + if(e.details.snap_vf === '')snapVf.shift() + if(e.cudaEnabled){ + snapVf.push('hwdownload,format=nv12') + } + //-vf "thumbnail_cuda=2,hwdownload,format=nv12" + x.snap_vf=' -vf "'+snapVf.join(',')+'"' + }else{ + x.snap_vf='' + } + if(e.details.snap_scale_x && e.details.snap_scale_x !== '' && e.details.snap_scale_y && e.details.snap_scale_y !== ''){x.snap_ratio = ' -s '+e.details.snap_scale_x+'x'+e.details.snap_scale_y}else{x.snap_ratio=''} + if(e.details.cust_snap && e.details.cust_snap !== ''){x.cust_snap = ' '+e.details.cust_snap}else{x.cust_snap=''} + x.pipe+=' -update 1 -r '+e.details.snap_fps+x.cust_snap+x.snap_ratio+x.snap_vf+' "'+e.sdir+'s.jpg" -y'; + } + //custom - output + if(e.details.custom_output&&e.details.custom_output!==''){x.pipe+=' '+e.details.custom_output;} + } + ffmpeg.buildMainRecording = function(e,x){ + //e = monitor object + //x = temporary values + x.record_video_filters = [] + x.record_string = '' + //record - resolution + if(e.width!==''&&e.height!==''&&!isNaN(e.width)&&!isNaN(e.height)){ + x.record_dimensions=' -s '+e.width+'x'+e.height + }else{ + x.record_dimensions='' + } + if(e.details.stream_scale_x&&e.details.stream_scale_x!==''&&e.details.stream_scale_y&&e.details.stream_scale_y!==''){ + x.dimensions = e.details.stream_scale_x+'x'+e.details.stream_scale_y; + } + //record - segmenting + x.segment=' -f segment -segment_atclocktime 1 -reset_timestamps 1 -strftime 1 -segment_list pipe:2 -segment_time '+(60*e.cutoff)+' "'+e.dir+'%Y-%m-%dT%H-%M-%S.'+e.ext+'"'; + //record - set defaults for extension, video quality + switch(e.ext){ + case'mp4': + x.vcodec='libx264';x.acodec='aac'; + if(e.details.crf&&e.details.crf!==''){x.vcodec+=' -crf '+e.details.crf} + break; + case'webm': + x.acodec='libvorbis',x.vcodec='libvpx'; + if(e.details.crf&&e.details.crf!==''){x.vcodec+=' -q:v '+e.details.crf}else{x.vcodec+=' -q:v 1';} + break; + } + if(e.details.vcodec==='h264_vaapi'){ + x.record_video_filters.push('format=nv12,hwupload'); + } + //record - use custom video codec + if(e.details.vcodec&&e.details.vcodec!==''&&e.details.vcodec!=='default'){x.vcodec=e.details.vcodec} + //record - use custom audio codec + if(e.details.acodec&&e.details.acodec!==''&&e.details.acodec!=='default'){x.acodec=e.details.acodec} + if(e.details.cust_record){ + if(x.acodec=='aac'&&e.details.cust_record.indexOf('-strict -2')===-1){e.details.cust_record+=' -strict -2';} + if(e.details.cust_record.indexOf('-threads')===-1){e.details.cust_record+=' -threads 1';} + } + // if(e.details.cust_input&&(e.details.cust_input.indexOf('-use_wallclock_as_timestamps 1')>-1)===false){e.details.cust_input+=' -use_wallclock_as_timestamps 1';} + //record - ready or reset codecs + if(x.acodec!=='no'){ + if(x.acodec.indexOf('none')>-1){x.acodec=''}else{x.acodec=' -acodec '+x.acodec} + }else{ + x.acodec=' -an' + } + if(x.vcodec.indexOf('none')>-1){x.vcodec=''}else{x.vcodec=' -vcodec '+x.vcodec} + //record - frames per second (fps) + if(e.fps&&e.fps!==''&&e.details.vcodec!=='copy'){x.record_fps=' -r '+e.fps}else{x.record_fps=''} + //stream - frames per second (fps) + if(e.details.stream_fps&&e.details.stream_fps!==''){x.stream_fps=' -r '+e.details.stream_fps}else{x.stream_fps=''} + //record - timestamp options for -vf + if(e.details.timestamp&&e.details.timestamp=="1"&&e.details.vcodec!=='copy'){ + //font + if(e.details.timestamp_font&&e.details.timestamp_font!==''){x.time_font=e.details.timestamp_font}else{x.time_font='/usr/share/fonts/truetype/freefont/FreeSans.ttf'} + //position x + if(e.details.timestamp_x&&e.details.timestamp_x!==''){x.timex=e.details.timestamp_x}else{x.timex='(w-tw)/2'} + //position y + if(e.details.timestamp_y&&e.details.timestamp_y!==''){x.timey=e.details.timestamp_y}else{x.timey='0'} + //text color + if(e.details.timestamp_color&&e.details.timestamp_color!==''){x.time_color=e.details.timestamp_color}else{x.time_color='white'} + //box color + if(e.details.timestamp_box_color&&e.details.timestamp_box_color!==''){x.time_box_color=e.details.timestamp_box_color}else{x.time_box_color='0x00000000@1'} + //text size + if(e.details.timestamp_font_size&&e.details.timestamp_font_size!==''){x.time_font_size=e.details.timestamp_font_size}else{x.time_font_size='10'} + + x.record_video_filters.push('drawtext=fontfile='+x.time_font+':text=\'%{localtime}\':x='+x.timex+':y='+x.timey+':fontcolor='+x.time_color+':box=1:boxcolor='+x.time_box_color+':fontsize='+x.time_font_size); + } + //record - watermark for -vf + if(e.details.watermark&&e.details.watermark=="1"&&e.details.watermark_location&&e.details.watermark_location!==''){ + switch(e.details.watermark_position){ + case'tl'://top left + x.watermark_position='10:10' + break; + case'tr'://top right + x.watermark_position='main_w-overlay_w-10:10' + break; + case'bl'://bottom left + x.watermark_position='10:main_h-overlay_h-10' + break; + default://bottom right + x.watermark_position='(main_w-overlay_w-10)/2:(main_h-overlay_h-10)/2' + break; + } + x.record_video_filters.push('movie='+e.details.watermark_location+'[watermark],[in][watermark]overlay='+x.watermark_position+'[out]'); + } + //record - rotation + if(e.details.rotate_record&&e.details.rotate_record!==""&&e.details.rotate_record!=="no"&&e.details.stream_vcodec!=="copy"){ + x.record_video_filters.push('transpose='+e.details.rotate_record); + } + //check custom record filters for -vf + if(e.details.vf&&e.details.vf!==''){ + x.record_video_filters.push(e.details.vf) + } + //compile filter string for -vf + if(x.record_video_filters.length>0){ + x.record_video_filters=' -vf '+x.record_video_filters.join(',') + }else{ + x.record_video_filters='' + } + //build record string. + if(e.mode === 'record'){ + if(e.details.input_map_choices&&e.details.input_map_choices.record){ + //add input feed map + x.record_string += s.createFFmpegMap(e,e.details.input_map_choices.record) + } + //if h264, hls, mp4, or local add the audio codec flag + switch(e.type){ + case'h264':case'hls':case'mp4':case'local': + x.record_string+=x.acodec; + break; + } + //custom flags + if(e.details.cust_record&&e.details.cust_record!==''){x.record_string+=' '+e.details.cust_record;} + //preset flag + if(e.details.preset_record&&e.details.preset_record!==''){x.record_string+=' -preset '+e.details.preset_record;} + //main string write + x.record_string+=x.vcodec+x.record_fps+x.record_video_filters+x.record_dimensions+x.segment; + } + } + ffmpeg.buildMainDetector = function(e,x){ + //e = monitor object + //x = temporary values + x.cust_detect = ' ' + //detector - plugins, motion + if(e.details.detector === '1' && e.details.detector_send_frames === '1'){ + if(e.details.input_map_choices&&e.details.input_map_choices.detector){ + //add input feed map + x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) + } + if(!e.details.detector_fps||e.details.detector_fps===''){e.details.detector_fps=2} + if(e.details.detector_scale_x&&e.details.detector_scale_x!==''&&e.details.detector_scale_y&&e.details.detector_scale_y!==''){x.dratio=' -s '+e.details.detector_scale_x+'x'+e.details.detector_scale_y}else{x.dratio=' -s 320x240'} + if(e.details.cust_detect&&e.details.cust_detect!==''){x.cust_detect+=e.details.cust_detect;} + x.detector_vf = ['fps='+e.details.detector_fps] + if(e.cudaEnabled){ + x.detector_vf.push('hwdownload,format=nv12') + } + x.detector_vf = '-vf "'+x.detector_vf.join(',')+'"' + if(e.details.detector_pam==='1'){ + if(e.cudaEnabled){ + x.pipe += ' -vf "hwdownload,format=nv12"' + } + x.pipe+=' -an -c:v pam -pix_fmt gray -f image2pipe -r '+e.details.detector_fps+x.cust_detect+x.dratio+' pipe:3' + if(e.details.detector_use_detect_object === '1'){ + //for object detection + x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector) + x.pipe += ' -f singlejpeg '+x.detector_vf+x.cust_detect+x.dratio+' pipe:4'; + } + }else{ + x.pipe+=' -f image2pipe '+x.detector_vf+x.cust_detect+x.dratio+' pipe:3'; + } + } + //Traditional Recording Buffer + if(e.details.detector=='1'&&e.details.detector_trigger=='1'&&e.details.detector_record_method==='sip'){ + if(e.details.input_map_choices&&e.details.input_map_choices.detector_sip_buffer){ + //add input feed map + x.pipe += s.createFFmpegMap(e,e.details.input_map_choices.detector_sip_buffer) + } + x.detector_buffer_filters=[] + if(!e.details.detector_buffer_vcodec||e.details.detector_buffer_vcodec===''||e.details.detector_buffer_vcodec==='auto'){ + switch(e.type){ + case'h264':case'hls':case'mp4': + e.details.detector_buffer_vcodec = 'copy' + break; + default: + if(e.details.accelerator === '1' && e.cudaEnabled){ + e.details.detector_buffer_vcodec = 'h264_nvenc' + }else{ + e.details.detector_buffer_vcodec = 'libx264' + } + break; + } + } + if(!e.details.detector_buffer_acodec||e.details.detector_buffer_acodec===''||e.details.detector_buffer_acodec==='auto'){ + switch(e.type){ + case'mjpeg':case'jpeg':case'socket': + e.details.detector_buffer_acodec = 'no' + break; + case'h264':case'hls':case'mp4': + e.details.detector_buffer_acodec = 'copy' + break; + default: + e.details.detector_buffer_acodec = 'aac' + break; + } + } + if(e.details.detector_buffer_acodec === 'no'){ + x.detector_buffer_acodec = ' -an' + }else{ + x.detector_buffer_acodec = ' -c:a '+e.details.detector_buffer_acodec + } + if(!e.details.detector_buffer_tune||e.details.detector_buffer_tune===''){e.details.detector_buffer_tune='zerolatency'} + if(!e.details.detector_buffer_g||e.details.detector_buffer_g===''){e.details.detector_buffer_g='1'} + if(!e.details.detector_buffer_hls_time||e.details.detector_buffer_hls_time===''){e.details.detector_buffer_hls_time='2'} + if(!e.details.detector_buffer_hls_list_size||e.details.detector_buffer_hls_list_size===''){e.details.detector_buffer_hls_list_size='4'} + if(!e.details.detector_buffer_start_number||e.details.detector_buffer_start_number===''){e.details.detector_buffer_start_number='0'} + if(!e.details.detector_buffer_live_start_index||e.details.detector_buffer_live_start_index===''){e.details.detector_buffer_live_start_index='-3'} + + if(e.details.detector_buffer_vcodec.indexOf('_vaapi')>-1){ + if(x.hwaccel.indexOf('-vaapi_device')>-1){ + x.detector_buffer_filters.push('format=nv12') + x.detector_buffer_filters.push('hwupload') + }else{ + e.details.detector_buffer_vcodec='libx264' + } + } + if(e.details.detector_buffer_vcodec!=='copy'){ + if(e.details.detector_buffer_fps&&e.details.detector_buffer_fps!==''){ + x.detector_buffer_fps=' -r '+e.details.detector_buffer_fps + }else{ + x.detector_buffer_fps=' -r 30' + } + }else{ + x.detector_buffer_fps='' + } + if(x.detector_buffer_filters.length>0){ + x.pipe+=' -vf '+x.detector_buffer_filters.join(',') + } + x.pipe+=x.detector_buffer_fps+x.detector_buffer_acodec+' -c:v '+e.details.detector_buffer_vcodec+' -f hls -tune '+e.details.detector_buffer_tune+' -g '+e.details.detector_buffer_g+' -hls_time '+e.details.detector_buffer_hls_time+' -hls_list_size '+e.details.detector_buffer_hls_list_size+' -start_number '+e.details.detector_buffer_start_number+' -live_start_index '+e.details.detector_buffer_live_start_index+' -hls_allow_cache 0 -hls_flags +delete_segments+omit_endlist "'+e.sdir+'detectorStream.m3u8"' + } + } + ffmpeg.assembleMainPieces = function(e,x){ + //create executeable FFMPEG command + x.ffmpegCommandString = x.loglevel+x.input_fps; + //progress pipe + x.ffmpegCommandString += ' -progress pipe:5'; + + switch(e.type){ + case'dashcam': + x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i -'; + break; + case'socket':case'jpeg':case'pipe'://case'webpage': + x.ffmpegCommandString += ' -pattern_type glob -f image2pipe'+x.record_fps+' -vcodec mjpeg'+x.cust_input+x.hwaccel+' -i -'; + break; + case'mjpeg': + x.ffmpegCommandString += ' -reconnect 1 -f mjpeg'+x.cust_input+x.hwaccel+' -i "'+e.url+'"'; + break; + case'h264':case'hls':case'mp4': + x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+e.url+'"'; + break; + case'local': + x.ffmpegCommandString += x.cust_input+x.hwaccel+' -i "'+e.path+'"'; + break; + } + //add extra input maps + if(e.details.input_maps){ + e.details.input_maps.forEach(function(v,n){ + x.ffmpegCommandString += s.createInputMap(e,n+1,v) + }) + } + //add recording and stream outputs + x.ffmpegCommandString += x.record_string+x.pipe + } + ffmpeg.createPipeArray = function(e,x){ + //create additional pipes from ffmpeg + x.stdioPipes = []; + var times = config.pipeAddition; + if(e.details.stream_channels){ + times+=e.details.stream_channels.length + } + for(var i=0; i < times; i++){ + x.stdioPipes.push('pipe') + } + } + s.ffmpeg = function(e){ + //set X for temporary values so we don't break our main monitor object. + var x = {tmp : ''} + //set some placeholding values to avoid "undefined" in ffmpeg string. + ffmpeg.buildMainInput(e,x) + ffmpeg.buildMainStream(e,x) + ffmpeg.buildMainRecording(e,x) + ffmpeg.buildMainDetector(e,x) + ffmpeg.assembleMainPieces(e,x) + ffmpeg.createPipeArray(e,x) + //hold ffmpeg command for log stream + s.group[e.ke].mon[e.mid].ffmpeg = x.ffmpegCommandString + //clean the string of spatial impurities and split for spawn() + x.ffmpegCommandString = s.splitForFFPMEG(x.ffmpegCommandString) + //launch that bad boy + return spawn(config.ffmpegDir,x.ffmpegCommandString,{detached: true,stdio:x.stdioPipes}) + } + if(!config.ffmpegDir){ + ffmpeg.checkForWindows(function(){ + ffmpeg.checkForUnix(function(){ + ffmpeg.checkForFfbinary(function(){ + ffmpeg.checkForNpmStatic(function(){ + console.log('No FFmpeg found.') + }) + }) + }) + }) + } + if(downloadingFfmpeg === false){ + //not downloading ffmpeg + ffmpeg.completeCheck() + } + return ffmpeg +} diff --git a/libs/folders.js b/libs/folders.js new file mode 100644 index 0000000..d73b10b --- /dev/null +++ b/libs/folders.js @@ -0,0 +1,49 @@ +var fs = require('fs'); +module.exports = function(s,config){ + //directories + s.group={}; + if(!config.windowsTempDir&&s.isWin===true){config.windowsTempDir='C:/Windows/Temp'} + if(!config.defaultMjpeg){config.defaultMjpeg=s.mainDirectory+'/web/libs/img/bg.jpg'} + //default stream folder check + if(!config.streamDir){ + if(s.isWin===false){ + config.streamDir='/dev/shm' + }else{ + config.streamDir=config.windowsTempDir + } + if(!fs.existsSync(config.streamDir)){ + config.streamDir=s.mainDirectory+'/streams/' + }else{ + config.streamDir+='/streams/' + } + } + if(!config.videosDir){config.videosDir=s.mainDirectory+'/videos/'} + if(!config.binDir){config.binDir=s.mainDirectory+'/fileBin/'} + if(!config.addStorage){config.addStorage=[]} + s.dir={ + videos:s.checkCorrectPathEnding(config.videosDir), + streams:s.checkCorrectPathEnding(config.streamDir), + fileBin:s.checkCorrectPathEnding(config.binDir), + addStorage:config.addStorage, + languages:s.location.languages+'/' + }; + //streams dir + if(!fs.existsSync(s.dir.streams)){ + fs.mkdirSync(s.dir.streams); + } + //videos dir + if(!fs.existsSync(s.dir.videos)){ + fs.mkdirSync(s.dir.videos); + } + //fileBin dir + if(!fs.existsSync(s.dir.fileBin)){ + fs.mkdirSync(s.dir.fileBin); + } + //additional storage areas + s.dir.addStorage.forEach(function(v,n){ + v.path=s.checkCorrectPathEnding(v.path) + if(!fs.existsSync(v.path)){ + fs.mkdirSync(v.path); + } + }) +} diff --git a/libs/health.js b/libs/health.js new file mode 100644 index 0000000..95193b7 --- /dev/null +++ b/libs/health.js @@ -0,0 +1,76 @@ +var exec = require('child_process').exec; +var spawn = require('child_process').spawn; +module.exports = function(s,config,lang,io){ + s.sendDiskUsedAmountToClients = function(e){ + //send the amount used disk space to connected users + if(s.group[e.ke]&&s.group[e.ke].init){ + s.tx({f:'diskUsed',size:s.group[e.ke].usedSpace,limit:s.group[e.ke].sizeLimit},'GRP_'+e.ke); + } + } + s.heartBeat = function(){ + setTimeout(s.heartBeat, 8000); + io.sockets.emit('ping',{beat:1}); + } + s.heartBeat() + s.cpuUsage = function(callback){ + k={} + switch(s.platform){ + case'win32': + k.cmd="@for /f \"skip=1\" %p in ('wmic cpu get loadpercentage') do @echo %p%" + break; + case'darwin': + k.cmd="ps -A -o %cpu | awk '{s+=$1} END {print s}'"; + break; + case'linux': + k.cmd='LANG=C top -b -n 2 | grep "^'+config.cpuUsageMarker+'" | awk \'{print $2}\' | tail -n1'; + break; + case'freebsd': + k.cmd='vmstat 1 2 | tail -1 | awk \'{print $17}\'' + break; + } + if(config.customCpuCommand){ + exec(config.customCpuCommand,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true) { + d = d.replace(/(\r\n|\n|\r)/gm, "").replace(/%/g, "") + } + callback(d) + }); + } else if(k.cmd){ + exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true){ + d=d.replace(/(\r\n|\n|\r)/gm,"").replace(/%/g,"") + } + callback(d) + }); + } else { + callback(0) + } + } + s.ramUsage = function(callback){ + k={} + switch(s.platform){ + case'win32': + k.cmd = "wmic OS get FreePhysicalMemory /Value" + break; + case'darwin': + k.cmd = "vm_stat | awk '/^Pages free: /{f=substr($3,1,length($3)-1)} /^Pages active: /{a=substr($3,1,length($3-1))} /^Pages inactive: /{i=substr($3,1,length($3-1))} /^Pages speculative: /{s=substr($3,1,length($3-1))} /^Pages wired down: /{w=substr($4,1,length($4-1))} /^Pages occupied by compressor: /{c=substr($5,1,length($5-1)); print ((a+w)/(f+a+i+w+s+c))*100;}'" + break; + case'freebsd': + k.cmd = "echo \"scale=4; $(vmstat -H | tail -1 | awk '{print $5}')*1024*100/$(sysctl hw.physmem | awk '{print $2}')\" | bc" + break; + default: + k.cmd = "LANG=C free | grep Mem | awk '{print $7/$2 * 100.0}'"; + break; + } + if(k.cmd){ + exec(k.cmd,{encoding:'utf8',detached: true},function(err,d){ + if(s.isWin===true){ + d=(parseInt(d.split('=')[1])/(s.totalmem/1000))*100 + } + callback(d) + }); + }else{ + callback(0) + } + } +} diff --git a/libs/language.js b/libs/language.js new file mode 100644 index 0000000..93fbe72 --- /dev/null +++ b/libs/language.js @@ -0,0 +1,61 @@ +module.exports = function(s,config){ + if(!config.language){ + config.language='en_CA' + } + try{ + var lang = require(s.location.languages+'/'+config.language+'.json'); + }catch(er){ + console.error(er) + console.log('There was an error loading your language file.') + var lang = require(s.location.languages+'/en_CA.json'); + } + s.location.definitions = s.mainDirectory+'/definitions' + try{ + var definitions = require(s.location.definitions+'/'+config.language+'.json'); + }catch(er){ + console.error(er) + console.log('There was an error loading your language file.') + var definitions = require(s.location.definitions+'/en_CA.json'); + } + //load languages dynamically + s.loadedLanguages={} + s.loadedLanguages[config.language]=lang; + s.getLanguageFile = function(rule){ + if(rule && rule !== ''){ + var file = s.loadedLanguages[file] + if(!file){ + try{ + s.loadedLanguages[rule] = require(s.location.languages+'/'+rule+'.json') + s.loadedLanguages[rule] = Object.assign(lang,s.loadedLanguages[rule]) + file = s.loadedLanguages[rule] + }catch(err){ + file = lang + } + } + }else{ + file = lang + } + return file + } + //load defintions dynamically + s.loadedDefinitons={} + s.loadedDefinitons[config.language]=definitions; + s.getDefinitonFile = function(rule){ + if(rule && rule !== ''){ + var file = s.loadedDefinitons[file] + if(!file){ + try{ + s.loadedDefinitons[rule] = require(s.location.definitions+'/'+rule+'.json') + s.loadedDefinitons[rule] = Object.assign(definitions,s.loadedDefinitons[rule]) + file = s.loadedDefinitons[rule] + }catch(err){ + file = definitions + } + } + }else{ + file = definitions + } + return file + } + return lang +} diff --git a/libs/monitor.js b/libs/monitor.js new file mode 100644 index 0000000..332c4d0 --- /dev/null +++ b/libs/monitor.js @@ -0,0 +1,1321 @@ +var fs = require('fs'); +var events = require('events'); +var spawn = require('child_process').spawn; +var exec = require('child_process').exec; +var Mp4Frag = require('mp4frag'); +var onvif = require('node-onvif'); +var request = require('request'); +var connectionTester = require('connection-tester') +var URL = require('url') +module.exports = function(s,config,lang){ + s.initiateMonitorObject = function(e){ + if(!s.group[e.ke]){s.group[e.ke]={}}; + if(!s.group[e.ke].mon){s.group[e.ke].mon={}} + if(!s.group[e.ke].mon[e.mid]){s.group[e.ke].mon[e.mid]={}} + if(!s.group[e.ke].mon[e.mid].streamIn){s.group[e.ke].mon[e.mid].streamIn={}}; + if(!s.group[e.ke].mon[e.mid].emitterChannel){s.group[e.ke].mon[e.mid].emitterChannel={}}; + if(!s.group[e.ke].mon[e.mid].mp4frag){s.group[e.ke].mon[e.mid].mp4frag={}}; + if(!s.group[e.ke].mon[e.mid].firstStreamChunk){s.group[e.ke].mon[e.mid].firstStreamChunk={}}; + if(!s.group[e.ke].mon[e.mid].contentWriter){s.group[e.ke].mon[e.mid].contentWriter={}}; + if(!s.group[e.ke].mon[e.mid].childNodeStreamWriters){s.group[e.ke].mon[e.mid].childNodeStreamWriters={}}; + if(!s.group[e.ke].mon[e.mid].eventBasedRecording){s.group[e.ke].mon[e.mid].eventBasedRecording={}}; + if(!s.group[e.ke].mon[e.mid].watch){s.group[e.ke].mon[e.mid].watch={}}; + if(!s.group[e.ke].mon[e.mid].fixingVideos){s.group[e.ke].mon[e.mid].fixingVideos={}}; + if(!s.group[e.ke].mon[e.mid].isStarted){s.group[e.ke].mon[e.mid].isStarted = false}; + if(s.group[e.ke].mon[e.mid].delete){clearTimeout(s.group[e.ke].mon[e.mid].delete)} + if(!s.group[e.ke].mon_conf){s.group[e.ke].mon_conf={}} + s.onMonitorInitExtensions.forEach(function(extender){ + extender(e) + }) + } + s.sendMonitorStatus = function(e){ + s.group[e.ke].mon[e.id].monitorStatus = e.status + s.tx(Object.assign(e,{f:'monitor_status'}),'GRP_'+e.ke) + } + s.getMonitorCpuUsage = function(e,callback){ + if(s.group[e.ke].mon[e.mid].spawn){ + var getUsage = function(callback2){ + fs.readFile("/proc/" + s.group[e.ke].mon[e.mid].spawn.pid + "/stat", function(err, data){ + if(!err){ + var elems = data.toString().split(' '); + var utime = parseInt(elems[13]); + var stime = parseInt(elems[14]); + + callback2(utime + stime); + }else{ + clearInterval(s.group[e.ke].mon[e.mid].getMonitorCpuUsage) + } + }) + } + getUsage(function(startTime){ + setTimeout(function(){ + getUsage(function(endTime){ + var delta = endTime - startTime; + var percentage = 100 * (delta / 10000); + callback(percentage) + }); + }, 1000) + }) + }else{ + callback(0) + } + } + s.buildMonitorUrl = function(e,noPath){ + var authd = '' + var url + if(e.details.muser&&e.details.muser!==''&&e.host.indexOf('@')===-1) { + e.username = e.details.muser + e.password = e.details.mpass + authd = e.details.muser+':'+e.details.mpass+'@' + } + if(e.port==80&&e.details.port_force!=='1'){e.porty=''}else{e.porty=':'+e.port} + url = e.protocol+'://'+authd+e.host+e.porty + if(noPath !== true)url += e.path + return url + } + s.cleanMonitorObject = function(e){ + x={keys:Object.keys(e),ar:{}}; + x.keys.forEach(function(v){ + if(v!=='last_frame'&&v!=='record'&&v!=='spawn'&&v!=='running'&&(v!=='time'&&typeof e[v]!=='function')){x.ar[v]=e[v];} + }); + return x.ar; + } + s.getRawSnapshotFromMonitor = function(monitor,options,callback){ + if(!callback){ + callback = options + var options = '' + }else{ + options = ' '+options + } + var url + var runExtraction = function(){ + var snapBuffer = [] + var snapProcess = spawn(config.ffmpegDir,('-loglevel quiet -re -i '+url+options+' -frames:v 1 -f mjpeg pipe:1').split(' '),{detached: true}) + snapProcess.stdout.on('data',function(data){ + snapBuffer.push(data) + }); + snapProcess.stderr.on('data',function(data){ + console.log(data.toString()) + }); + snapProcess.on('close',function(data){ + snapBuffer = Buffer.concat(snapBuffer) + callback(snapBuffer,false) + }) + } + var checkExists = function(localStream,callback){ + fs.stat(localStream,function(err){ + if(err){ + callback(false) + }else{ + callback(true) + } + }) + } + var localStream = s.dir.streams+monitor.ke+'/'+monitor.mid+'/' + checkExists(localStream+'s.jpg',function(success){ + if(success === false){ + checkExists(localStream+'detectorStream.m3u8',function(success){ + if(success === false){ + checkExists(localStream+'s.m3u8',function(success){ + if(success === false){ + url = s.buildMonitorUrl(monitor) + }else{ + url = localStream+'s.m3u8' + } + runExtraction() + }) + }else{ + url = localStream+'detectorStream.m3u8' + runExtraction() + } + }) + }else{ + fs.readFile(localStream+'s.jpg',function(err,snapBuffer){ + callback(snapBuffer,true) + }) + } + }) + } + s.mergeDetectorBufferChunks = function(monitor,callback){ + var pathDir = s.dir.streams+monitor.ke+'/'+monitor.id+'/' + var mergedFile = s.formattedTime()+'.mp4' + var mergedFilepath = pathDir+mergedFile + var streamDirItems = fs.readdirSync(pathDir) + var items = [] + var copiedItems = [] + var createMerged = function(copiedItems){ + var allts = pathDir+items.join('_') + fs.stat(allts,function(err,stats){ + if(err){ + //not exist + var cat = 'cat '+copiedItems.join(' ')+' > '+allts + exec(cat,function(){ + var merger = spawn(config.ffmpegDir,s.splitForFFPMEG(('-re -i '+allts+' -acodec copy -vcodec copy '+pathDir+mergedFile))) + merger.stderr.on('data',function(data){ + s.userLog(monitor,{type:"Buffer Merge",msg:data.toString()}) + }) + merger.on('close',function(){ + s.file('delete',allts) + copiedItems.forEach(function(copiedItem){ + s.file('delete',copiedItem) + }) + setTimeout(function(){ + s.file('delete',mergedFilepath) + },1000 * 60 * 3) + delete(merger) + callback(mergedFilepath,mergedFile) + }) + }) + }else{ + //file exist + callback(mergedFilepath,mergedFile) + } + }) + } + streamDirItems.forEach(function(filename){ + if(filename.indexOf('detectorStream') > -1 && filename.indexOf('.m3u8') === -1){ + items.push(filename) + } + }) + items.sort() + items = items.slice(items.length - 5,items.length) + items.forEach(function(filename){ + try{ + var tempFilename = filename.split('.') + tempFilename[0] = tempFilename[0] + 'm' + tempFilename = tempFilename.join('.') + var tempWriteStream = fs.createWriteStream(pathDir+tempFilename) + tempWriteStream.on('finish', function(){ + copiedItems.push(pathDir+tempFilename) + if(copiedItems.length === items.length){ + createMerged(copiedItems.sort()) + } + }) + fs.createReadStream(pathDir+filename).pipe(tempWriteStream) + }catch(err){ + + } + }) + return items + } + + s.cameraDestroy = function(x,e,p){ + if(s.group[e.ke]&&s.group[e.ke].mon[e.id]&&s.group[e.ke].mon[e.id].spawn !== undefined){ + if(s.group[e.ke].mon[e.id].spawn){ + s.group[e.ke].mon[e.id].allowStdinWrite = false + s.txToDashcamUsers({ + f : 'disable_stream', + ke : e.ke, + mid : e.id + },e.ke) + s.group[e.ke].mon[e.id].spawn.stdio[3].unpipe(); + // if(s.group[e.ke].mon[e.id].p2pStream){s.group[e.ke].mon[e.id].p2pStream.unpipe();} + if(s.group[e.ke].mon[e.id].p2p){s.group[e.ke].mon[e.id].p2p.unpipe();} + delete(s.group[e.ke].mon[e.id].p2pStream) + delete(s.group[e.ke].mon[e.id].p2p) + delete(s.group[e.ke].mon[e.id].pamDiff) + try{ + s.group[e.ke].mon[e.id].spawn.removeListener('end',s.group[e.ke].mon[e.id].spawn_exit); + s.group[e.ke].mon[e.id].spawn.removeListener('exit',s.group[e.ke].mon[e.id].spawn_exit); + delete(s.group[e.ke].mon[e.id].spawn_exit); + }catch(er){} + } + s.group[e.ke].mon[e.id].firstStreamChunk = {} + clearTimeout(s.group[e.ke].mon[e.id].recordingChecker); + delete(s.group[e.ke].mon[e.id].recordingChecker); + clearTimeout(s.group[e.ke].mon[e.id].streamChecker); + delete(s.group[e.ke].mon[e.id].streamChecker); + clearTimeout(s.group[e.ke].mon[e.id].checkSnap); + delete(s.group[e.ke].mon[e.id].checkSnap); + clearTimeout(s.group[e.ke].mon[e.id].watchdog_stop); + delete(s.group[e.ke].mon[e.id].watchdog_stop); + delete(s.group[e.ke].mon[e.id].lastJpegDetectorFrame); + clearTimeout(s.group[e.ke].mon[e.id].recordingSnapper); + clearInterval(s.group[e.ke].mon[e.id].getMonitorCpuUsage); + if(s.group[e.ke].mon[e.id].onChildNodeExit){ + s.group[e.ke].mon[e.id].onChildNodeExit() + } + if(s.group[e.ke].mon[e.id].mp4frag){ + var mp4FragChannels = Object.keys(s.group[e.ke].mon[e.id].mp4frag) + mp4FragChannels.forEach(function(channel){ + s.group[e.ke].mon[e.id].mp4frag[channel].removeAllListeners() + delete(s.group[e.ke].mon[e.id].mp4frag[channel]) + }) + } + if(s.group[e.ke].mon[e.id].childNode){ + s.cx({f:'kill',d:s.cleanMonitorObject(e)},s.group[e.ke].mon[e.id].childNodeId) + }else{ + if(!x||x===1){return}; + p=x.pid; + if(s.group[e.ke].mon_conf[e.id].type===('dashcam'||'socket'||'jpeg'||'pipe')){ + x.stdin.pause();setTimeout(function(){x.kill('SIGTERM');},500) + }else{ + try{ + x.stdin.setEncoding('utf8');x.stdin.write('q'); + }catch(er){} + } + setTimeout(function(){exec('kill -9 '+p,{detached: true})},1000) + } + } + } + s.cameraCheckObjectsInDetails = function(e){ + //parse Objects + (['detector_cascades','cords','detector_filters','input_map_choices']).forEach(function(v){ + if(e.details&&e.details[v]&&(e.details[v] instanceof Object)===false){ + try{ + if(e.details[v] === '') e.details[v] = '{}' + e.details[v]=JSON.parse(e.details[v]); + if(!e.details[v])e.details[v]={}; + s.group[e.ke].mon[e.id].details = e.details; + }catch(err){ + + } + } + }); + //parse Arrays + (['stream_channels','input_maps']).forEach(function(v){ + if(e.details&&e.details[v]&&(e.details[v] instanceof Array)===false){ + try{ + e.details[v]=JSON.parse(e.details[v]); + if(!e.details[v])e.details[v]=[]; + }catch(err){ + e.details[v]=[]; + } + } + }); + } + s.cameraControl = function(e,callback){ + s.checkDetails(e) + if(!s.group[e.ke]||!s.group[e.ke].mon[e.id]){return} + var monitorConfig = s.group[e.ke].mon_conf[e.id]; + if(monitorConfig.details.control!=="1"){s.userLog(e,{type:lang['Control Error'],msg:lang.ControlErrorText1});return} + if(!monitorConfig.details.control_base_url||monitorConfig.details.control_base_url===''){ + e.base = s.buildMonitorUrl(monitorConfig, true); + }else{ + e.base = monitorConfig.details.control_base_url; + } + if(!monitorConfig.details.control_url_stop_timeout || monitorConfig.details.control_url_stop_timeout === ''){ + monitorConfig.details.control_url_stop_timeout = 1000 + } + if(!monitorConfig.details.control_url_method||monitorConfig.details.control_url_method===''){monitorConfig.details.control_url_method="GET"} + var controlURL = e.base+monitorConfig.details['control_url_'+e.direction] + var controlURLOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig) + if(monitorConfig.details.control_url_stop_timeout === '0' && monitorConfig.details.control_stop === '1' && s.group[e.ke].mon[e.id].ptzMoving === true){ + e.direction = 'stopMove' + s.group[e.ke].mon[e.id].ptzMoving = false + }else{ + s.group[e.ke].mon[e.id].ptzMoving = true + } + if(monitorConfig.details.control_url_method === 'ONVIF'){ + try{ + var move = function(device){ + var stopOptions = {ProfileToken : device.current_profile.token,'PanTilt': true,'Zoom': true} + switch(e.direction){ + case'center': +// device.services.ptz.gotoHomePosition() + msg = {type:'Center button inactive'} + s.userLog(e,msg) + callback(msg) + break; + case'stopMove': + msg = {type:'Control Trigger Ended'} + s.userLog(e,msg) + callback(msg) + device.services.ptz.stop(stopOptions).then((result) => { +// console.log(JSON.stringify(result['data'], null, ' ')); + }).catch((error) => { +// console.error(error); + }); + break; + default: + var controlOptions = { + ProfileToken : device.current_profile.token, + Velocity : {} + } + var onvifDirections = { + "left" : [-1.0,'x'], + "right" : [1.0,'x'], + "down" : [-1.0,'y'], + "up" : [1.0,'y'], + "zoom_in" : [1.0,'zoom'], + "zoom_out" : [-1.0,'zoom'] + } + var direction = onvifDirections[e.direction] + controlOptions.Velocity[direction[1]] = direction[0]; + (['x','y','z']).forEach(function(axis){ + if(!controlOptions.Velocity[axis]) + controlOptions.Velocity[axis] = 0 + }) + if(monitorConfig.details.control_stop=='1'){ + device.services.ptz.continuousMove(controlOptions).then(function(err){ + s.userLog(e,{type:'Control Trigger Started'}); + if(monitorConfig.details.control_url_stop_timeout !== '0'){ + setTimeout(function(){ + msg = {type:'Control Trigger Ended'} + s.userLog(e,msg) + callback(msg) + device.services.ptz.stop(stopOptions).then((result) => { +// console.log(JSON.stringify(result['data'], null, ' ')); + }).catch((error) => { + console.log(error); + }); + },monitorConfig.details.control_url_stop_timeout) + } + }).catch(function(err){ + console.log(err) + }); + }else{ + device.services.ptz.absoluteMove(controlOptions).then(function(err){ + msg = {type:'Control Triggered'} + s.userLog(e,msg); + callback(msg) + }).catch(function(err){ + console.log(err) + }); + } + break; + } + } + //create onvif connection + if(!s.group[e.ke].mon[e.id].onvifConnection){ + s.group[e.ke].mon[e.id].onvifConnection = new onvif.OnvifDevice({ + xaddr : 'http://' + controlURLOptions.host + ':' + controlURLOptions.port + '/onvif/device_service', + user : controlURLOptions.username, + pass : controlURLOptions.password + }) + s.group[e.ke].mon[e.id].onvifConnection.init().then((info) => { + move(s.group[e.ke].mon[e.id].onvifConnection) + }).catch(function(error){ + console.log(error) + s.userLog(e,{type:lang['Control Error'],msg:error}) + }) + }else{ + move(s.group[e.ke].mon[e.id].onvifConnection) + } + }catch(err){ + console.log(err) + msg = {type:lang['Control Error'],msg:{msg:lang.ControlErrorText2,error:err,options:controlURLOptions,direction:e.direction}} + s.userLog(e,msg) + callback(msg) + } + }else{ + var stopCamera = function(){ + var stopURL = e.base+monitorConfig.details['control_url_'+e.direction+'_stop'] + var options = s.cameraControlOptionsFromUrl(stopURL,monitorConfig) + var requestOptions = { + url : stopURL, + method : options.method, + auth : { + user : options.username, + pass : options.password + } + } + if(monitorConfig.details.control_digest_auth === '1'){ + requestOptions.sendImmediately = true + } + request(requestOptions,function(err,data){ + if(err){ + msg = {ok:false,type:'Control Error',msg:err} + }else{ + msg = {ok:true,type:'Control Trigger Ended'} + } + callback(msg) + s.userLog(e,msg); + }) + } + if(e.direction === 'stopMove'){ + stopCamera() + }else{ + var requestOptions = { + url : controlURL, + method : controlURLOptions.method, + auth : { + user : controlURLOptions.username, + pass : controlURLOptions.password + } + } + if(monitorConfig.details.control_digest_auth === '1'){ + requestOptions.sendImmediately = true + } + request(requestOptions,function(err,data){ + if(err){ + msg = {ok:false,type:'Control Error',msg:err}; + callback(msg) + s.userLog(e,msg); + return + } + if(monitorConfig.details.control_stop=='1'&&e.direction!=='center'){ + s.userLog(e,{type:'Control Triggered Started'}); + if(monitorConfig.details.control_url_stop_timeout > 0){ + setTimeout(function(){ + stopCamera() + },monitorConfig.details.control_url_stop_timeout) + } + }else{ + msg = {ok:true,type:'Control Triggered'}; + callback(msg) + s.userLog(e,msg); + } + }) + } + } + } + s.cameraControlOptionsFromUrl = function(e,monitorConfig){ + s.checkDetails(e) + URLobject = URL.parse(e) + if(monitorConfig.details.control_url_method === 'ONVIF' && monitorConfig.details.control_base_url === ''){ + if(monitorConfig.details.onvif_port === ''){ + monitorConfig.details.onvif_port = 8000 + } + URLobject.port = monitorConfig.details.onvif_port + }else if(!URLobject.port){ + URLobject.port = 80 + } + options = { + host: URLobject.hostname, + port: URLobject.port, + method: monitorConfig.details.control_url_method, + path: URLobject.pathname, + }; + if(URLobject.query){ + options.path=options.path+'?'+URLobject.query + } + if(URLobject.username&&URLobject.password){ + options.username = URLobject.username + options.password = URLobject.password + options.auth=URLobject.username+':'+URLobject.password + }else if(URLobject.auth){ + var auth = URLobject.auth.split(':') + options.auth=URLobject.auth + options.username = auth[0] + options.password = auth[1] + } + return options + } + s.cameraSendSnapshot = function(e){ + s.checkDetails(e) + if(config.doSnapshot===true){ + if(e.mon.mode!=='stop'){ + var pathDir = s.dir.streams+e.ke+'/'+e.mid+'/' + fs.stat(pathDir+'icon.jpg',function(err){ + if(!err){ + fs.readFile(pathDir+'icon.jpg',function(err,data){ + if(err){s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke);return}; + s.tx({f:'monitor_snapshot',snapshot:data,snapshot_format:'ab',mid:e.mid,ke:e.ke},'GRP_'+e.ke) + }) + }else{ + e.url = s.buildMonitorUrl(e.mon) + switch(e.mon.type){ + case'mjpeg':case'h264':case'local': + if(e.mon.type==='local'){e.url=e.mon.path;} + s.getRawSnapshotFromMonitor(e.mon,'-s 200x200',function(data,isStaticFile){ + if((data[data.length-2] === 0xFF && data[data.length-1] === 0xD9)){ + if(!isStaticFile){ + fs.writeFile(s.dir.streams+e.ke+'/'+e.mid+'/icon.jpg',data,function(){}) + } + s.tx({ + f:'monitor_snapshot', + snapshot:data.toString('base64'), + snapshot_format:'b64', + mid:e.mid, + ke:e.ke + },'GRP_'+e.ke) + }else{ + s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) + } + }) + break; + case'jpeg': + request({url:e.url,method:'GET',encoding:null},function(err,data){ + if(err){s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke);return}; + s.tx({f:'monitor_snapshot',snapshot:data.body,snapshot_format:'ab',mid:e.mid,ke:e.ke},'GRP_'+e.ke) + }) + break; + default: + s.tx({f:'monitor_snapshot',snapshot:'...',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) + break; + } + } + }) + }else{ + s.tx({f:'monitor_snapshot',snapshot:'Disabled',snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) + } + }else{ + s.tx({f:'monitor_snapshot',snapshot:e.mon.name,snapshot_format:'plc',mid:e.mid,ke:e.ke},'GRP_'+e.ke) + } + } + s.createCameraFolders = function(e){ + //set the recording directory + if(e.details && e.details.dir && e.details.dir !== '' && config.childNodes.mode !== 'child'){ + //addStorage choice + e.dir=s.checkCorrectPathEnding(e.details.dir)+e.ke+'/'; + if (!fs.existsSync(e.dir)){ + fs.mkdirSync(e.dir); + } + e.dir=e.dir+e.id+'/'; + if (!fs.existsSync(e.dir)){ + fs.mkdirSync(e.dir); + } + }else{ + //MAIN videos dir + e.dir=s.dir.videos+e.ke+'/'; + if (!fs.existsSync(e.dir)){ + fs.mkdirSync(e.dir); + } + e.dir=s.dir.videos+e.ke+'/'+e.id+'/'; + if (!fs.existsSync(e.dir)){ + fs.mkdirSync(e.dir); + } + } + // exec('chmod -R 777 '+e.dir,function(err){ + // + // }) + //set the temporary files directory + var setStreamDir = function(){ + //stream dir + e.sdir=s.dir.streams+e.ke+'/'; + if (!fs.existsSync(e.sdir)){ + fs.mkdirSync(e.sdir); + } + e.sdir=s.dir.streams+e.ke+'/'+e.id+'/'; + if (!fs.existsSync(e.sdir)){ + fs.mkdirSync(e.sdir); + }else{ + s.file('deleteFolder',e.sdir+'*') + } + } + setStreamDir() + // exec('chmod -R 777 '+e.sdir,function(err){ + // + // }) + return setStreamDir + } + s.stripAuthFromHost = function(e){ + var host = e.host.split('@'); + if(host[1]){ + //username and password found + host = host[1] + }else{ + //no username or password in `host` string + host = host[0] + } + return host + } + s.resetRecordingCheck = function(e){ + clearTimeout(s.group[e.ke].mon[e.id].recordingChecker) + var cutoff = e.cutoff + 0 + if(e.type === 'dashcam'){ + cutoff *= 100 + } + s.group[e.ke].mon[e.id].recordingChecker = setTimeout(function(){ + if(s.group[e.ke].mon[e.id].isStarted === true && s.group[e.ke].mon_conf[e.id].mode === 'record'){ + s.launchMonitorProcesses(e); + s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Restarting}); + s.userLog(e,{type:lang['Camera is not recording'],msg:{msg:lang['Restarting Process']}}); + s.orphanedVideoCheck(e,2,null,true) + } + },60000 * cutoff * 1.3); + } + s.resetStreamCheck = function(e){ + clearTimeout(s.group[e.ke].mon[e.id].streamChecker) + s.group[e.ke].mon[e.id].streamChecker = setTimeout(function(){ + if(s.group[e.ke].mon[e.id].isStarted === true){ + s.launchMonitorProcesses(e); + s.userLog(e,{type:lang['Camera is not streaming'],msg:{msg:lang['Restarting Process']}}); + s.orphanedVideoCheck(e,2,null,true) + } + },60000*1); + } + s.cameraPullJpegStream = function(e){ + if(!e.details.sfps||e.details.sfps===''){ + e.details.sfps = 1 + } + var capture_fps = parseFloat(e.details.sfps); + if(isNaN(capture_fps)){capture_fps = 1} + if(s.group[e.ke].mon[e.id].spawn){ + s.group[e.ke].mon[e.id].spawn.stdin.on('error',function(err){ + if(err&&e.details.loglevel!=='quiet'){ + s.userLog(e,{type:'STDIN ERROR',msg:err}); + } + }) + }else{ + if(e.functionMode === 'record'){ + s.userLog(e,{type:lang.FFmpegCantStart,msg:lang.FFmpegCantStartText}); + return + } + } + e.captureOne = function(f){ + s.group[e.ke].mon[e.id].recordingSnapRequest = request({ + url: e.url, + method: 'GET', + encoding: null, + timeout: 15000 + },function(err,data){ + if(err){ + return; + } + }).on('data',function(d){ + if(!e.buffer0){ + e.buffer0 = [d] + }else{ + e.buffer0.push(d) + } + if((d[d.length-2] === 0xFF && d[d.length-1] === 0xD9)){ + e.buffer0 = Buffer.concat(e.buffer0); + if(s.group[e.ke].mon[e.id].spawn&&s.group[e.ke].mon[e.id].spawn.stdin){ + s.group[e.ke].mon[e.id].spawn.stdin.write(e.buffer0); + } + if(s.group[e.ke].mon[e.id].isStarted === true){ + s.group[e.ke].mon[e.id].recordingSnapper = setTimeout(function(){ + e.captureOne() + },1000/capture_fps) + } + e.buffer0 = null + } + if(!e.timeOut){ + e.timeOut = setTimeout(function(){ + e.errorCount = 0; + delete(e.timeOut) + },3000) + } + }).on('error', function(err){ + ++e.errorCount + clearTimeout(e.timeOut) + delete(e.timeOut) + if(e.details.loglevel !== 'quiet'){ + s.userLog(e,{ + type: lang['JPEG Error'], + msg: { + msg: lang.JPEGErrorText, + info: err + } + }); + switch(err.code){ + case'ESOCKETTIMEDOUT': + case'ETIMEDOUT': + ++s.group[e.ke].mon[e.id].errorSocketTimeoutCount + if( + e.details.fatal_max !== 0 && + s.group[e.ke].mon[e.id].errorSocketTimeoutCount > e.details.fatal_max + ){ + s.userLog(e,{type:lang['Fatal Maximum Reached'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}}); + s.camera('stop',e) + }else{ + s.userLog(e,{type:lang['Restarting Process'],msg:{code:'ESOCKETTIMEDOUT',msg:lang.FatalMaximumReachedText}}); + s.camera('restart',e) + } + return; + break; + } + } + if(e.details.fatal_max !== 0 && e.errorCount > e.details.fatal_max){ + clearTimeout(s.group[e.ke].mon[e.id].recordingSnapper) + s.launchMonitorProcesses(e) + } + }) + } + e.captureOne() + } + s.createCameraFfmpegProcess = function(e){ + //launch ffmpeg (main) + s.tx({ + f: 'monitor_starting', + mode: e.functionMode, + mid: e.id, + time: s.formattedTime() + },'GRP_'+e.ke) + s.group[e.ke].mon[e.id].spawn = s.ffmpeg(e) + s.sendMonitorStatus({id:e.id,ke:e.ke,status:e.wantedStatus}); + //on unexpected exit restart + s.group[e.ke].mon[e.id].spawn_exit = function(){ + if(s.group[e.ke].mon[e.id].isStarted === true){ + if(e.details.loglevel!=='quiet'){ + s.userLog(e,{type:lang['Process Unexpected Exit'],msg:{msg:lang['Process Crashed for Monitor'],cmd:s.group[e.ke].mon[e.id].ffmpeg}}); + } + s.fatalCameraError(e,'Process Unexpected Exit'); + s.orphanedVideoCheck(e,2,null,true) + } + } + s.group[e.ke].mon[e.id].spawn.on('end',s.group[e.ke].mon[e.id].spawn_exit) + s.group[e.ke].mon[e.id].spawn.on('exit',s.group[e.ke].mon[e.id].spawn_exit) + s.group[e.ke].mon[e.id].spawn.on('error',function(er){ + s.userLog(e,{type:'Spawn Error',msg:er});s.fatalCameraError(e,'Spawn Error') + }) + s.userLog(e,{type:lang['Process Started'],msg:{cmd:s.group[e.ke].mon[e.id].ffmpeg}}) + if(s.isWin === false){ + s.group[e.ke].mon[e.id].getMonitorCpuUsage = setInterval(function(){ + s.getMonitorCpuUsage(e,function(percent){ + s.group[e.ke].mon[e.id].currentCpuUsage = percent + s.tx({ + f: 'camera_cpu_usage', + ke: e.ke, + id: e.id, + percent: percent + },'MON_STREAM_'+e.ke+e.id) + }) + },1000 * 60) + } + } + s.createCameraStreamHandlers = function(e){ + s.group[e.ke].mon[e.id].spawn.stdio[5].on('data',function(data){ + s.resetStreamCheck(e) + }) + //emitter for mjpeg + if(!e.details.stream_mjpeg_clients||e.details.stream_mjpeg_clients===''||isNaN(e.details.stream_mjpeg_clients)===false){e.details.stream_mjpeg_clients=20;}else{e.details.stream_mjpeg_clients=parseInt(e.details.stream_mjpeg_clients)} + s.group[e.ke].mon[e.id].emitter = new events.EventEmitter().setMaxListeners(e.details.stream_mjpeg_clients); + if(e.type==='jpeg'){ + s.cameraPullJpegStream(e) + } + if(e.details.detector === '1'){ + s.ocvTx({f:'init_monitor',id:e.id,ke:e.ke}) + //frames from motion detect + if(e.details.detector_pam === '1'){ + s.createPamDiffEngine(e) + s.group[e.ke].mon[e.id].spawn.stdio[3].pipe(s.group[e.ke].mon[e.id].p2p).pipe(s.group[e.ke].mon[e.id].pamDiff) + if(e.details.detector_use_detect_object === '1'){ + s.group[e.ke].mon[e.id].spawn.stdio[4].on('data',function(d){ + s.group[e.ke].mon[e.id].lastJpegDetectorFrame = d + }) + } + }else{ + s.group[e.ke].mon[e.id].spawn.stdio[3].on('data',function(d){ + s.ocvTx({f:'frame',mon:s.group[e.ke].mon_conf[e.id].details,ke:e.ke,id:e.id,time:s.formattedTime(),frame:d}); + }) + } + } + //frames to stream + switch(e.details.stream_type){ + case'mp4': + s.group[e.ke].mon[e.id].mp4frag['MAIN'] = new Mp4Frag() + s.group[e.ke].mon[e.id].mp4frag['MAIN'].on('error',function(error){ + s.userLog(e,{type:lang['Mp4Frag'],msg:{error:error}}) + }) + s.group[e.ke].mon[e.id].spawn.stdio[1].pipe(s.group[e.ke].mon[e.id].mp4frag['MAIN']) + break; + case'flv': + e.frameToStream = function(d){ + if(!s.group[e.ke].mon[e.id].firstStreamChunk['MAIN'])s.group[e.ke].mon[e.id].firstStreamChunk['MAIN'] = d; + e.frameToStream = function(d){ + s.resetStreamCheck(e) + s.group[e.ke].mon[e.id].emitter.emit('data',d) + } + e.frameToStream(d) + } + break; + case'mjpeg': + e.frameToStream = function(d){ + s.resetStreamCheck(e) + s.group[e.ke].mon[e.id].emitter.emit('data',d) + } + break; + case'h265': + e.frameToStream = function(d){ + s.resetStreamCheck(e) + s.group[e.ke].mon[e.id].emitter.emit('data',d) + } + break; + case'b64':case undefined:case null:case'': + var buffer + e.frameToStream = function(d){ + s.resetStreamCheck(e) + if(!buffer){ + buffer=[d] + }else{ + buffer.push(d) + } + if((d[d.length-2] === 0xFF && d[d.length-1] === 0xD9)){ + s.group[e.ke].mon[e.id].emitter.emit('data',Buffer.concat(buffer)) + buffer = null + } + } + break; + } + if(e.frameToStream){ + s.group[e.ke].mon[e.id].spawn.stdout.on('data',e.frameToStream) + } + if(e.details.stream_channels && e.details.stream_channels !== ''){ + var createStreamEmitter = function(channel,number){ + var pipeNumber = number+config.pipeAddition; + if(!s.group[e.ke].mon[e.id].emitterChannel[pipeNumber]){ + s.group[e.ke].mon[e.id].emitterChannel[pipeNumber] = new events.EventEmitter().setMaxListeners(0); + } + var frameToStream + switch(channel.stream_type){ + case'mp4': + s.group[e.ke].mon[e.id].mp4frag[pipeNumber] = new Mp4Frag(); + s.group[e.ke].mon[e.id].spawn.stdio[pipeNumber].pipe(s.group[e.ke].mon[e.id].mp4frag[pipeNumber]) + break; + case'mjpeg': + frameToStream = function(d){ + s.group[e.ke].mon[e.id].emitterChannel[pipeNumber].emit('data',d) + } + break; + case'flv': + frameToStream = function(d){ + if(!s.group[e.ke].mon[e.id].firstStreamChunk[pipeNumber])s.group[e.ke].mon[e.id].firstStreamChunk[pipeNumber] = d; + frameToStream = function(d){ + s.group[e.ke].mon[e.id].emitterChannel[pipeNumber].emit('data',d) + } + frameToStream(d) + } + break; + case'h264': + frameToStream = function(d){ + s.group[e.ke].mon[e.id].emitterChannel[pipeNumber].emit('data',d) + } + break; + } + if(frameToStream){ + s.group[e.ke].mon[e.id].spawn.stdio[pipeNumber].on('data',frameToStream) + } + } + e.details.stream_channels.forEach(createStreamEmitter) + } + } + s.cameraFilterFfmpegLog = function(e){ + var checkLog = function(d,x){return d.indexOf(x)>-1} + s.group[e.ke].mon[e.id].spawn.stderr.on('data',function(d){ + d=d.toString(); + switch(true){ + case checkLog(d,'[hls @'): + case checkLog(d,'Past duration'): + case checkLog(d,'Last message repeated'): + case checkLog(d,'pkt->duration = 0'): + case checkLog(d,'Non-monotonous DTS'): + case checkLog(d,'NULL @'): + case checkLog(d,'RTP: missed'): + case checkLog(d,'deprecated pixel format used'): + return + break; + case checkLog(d,'Could not find tag for vp8'): + case checkLog(d,'Only VP8 or VP9 Video'): + case checkLog(d,'Could not write header'): + return s.userLog(e,{type:lang['Incorrect Settings Chosen'],msg:{msg:d}}) + break; + case checkLog(d,'Connection refused'): + case checkLog(d,'Connection timed out'): + //restart + setTimeout(function(){ + s.userLog(e,{type:lang['Connection timed out'],msg:lang['Retrying...']}); + s.fatalCameraError(e,'Connection timed out'); + },1000) + break; + case checkLog(d,'Immediate exit requested'): + case checkLog(d,'mjpeg_decode_dc'): + case checkLog(d,'bad vlc'): + case checkLog(d,'error dc'): + s.launchMonitorProcesses(e) + break; + case /T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(d): + var filename = d.split('.')[0]+'.'+e.ext + s.insertCompletedVideo(e,{ + file : filename + },function(err){ + s.userLog(e,{type:lang['Video Finished'],msg:{filename:d}}) + if( + e.details.detector === '1' && + s.group[e.ke].mon[e.id].isStarted === true && + e.details && + e.details.detector_record_method === 'del'&& + e.details.detector_delete_motionless_videos === '1'&& + s.group[e.ke].mon[e.id].detector_motion_count === 0 + ){ + if(e.details.loglevel !== 'quiet'){ + s.userLog(e,{type:lang['Delete Motionless Video'],msg:filename}) + } + s.deleteVideo({ + filename : filename, + ke : e.ke, + id : e.id + }) + } + s.group[e.ke].mon[e.id].detector_motion_count = 0 + }) + s.resetRecordingCheck(e) + return; + break; + } + s.userLog(e,{type:"FFMPEG STDERR",msg:d}) + }) + } + //set master based process launcher + s.launchMonitorProcesses = function(e){ + // e = monitor object + //create host string without username and password + var strippedHost = s.stripAuthFromHost(e) + var doOnThisMachine = function(){ + var setStreamDir = s.createCameraFolders(e) + s.group[e.ke].mon[e.id].allowStdinWrite = false + s.txToDashcamUsers({ + f : 'disable_stream', + ke : e.ke, + mid : e.id + },e.ke) + if(e.details.detector_trigger === '1'){ + s.group[e.ke].mon[e.id].motion_lock=setTimeout(function(){ + clearTimeout(s.group[e.ke].mon[e.id].motion_lock); + delete(s.group[e.ke].mon[e.id].motion_lock); + },15000) + } + //start "no motion" checker + if(e.details.detector === '1' && e.details.detector_notrigger === '1'){ + if(!e.details.detector_notrigger_timeout || e.details.detector_notrigger_timeout === ''){ + e.details.detector_notrigger_timeout = 10 + } + e.detector_notrigger_timeout = parseFloat(e.details.detector_notrigger_timeout)*1000*60; + s.group[e.ke].mon[e.id].detector_notrigger_timeout_function = function(){ + s.onDetectorNoTriggerTimeoutExtensions.forEach(function(extender){ + extender(r,e) + }) + } + clearInterval(s.group[e.ke].mon[e.id].detector_notrigger_timeout) + s.group[e.ke].mon[e.id].detector_notrigger_timeout=setInterval(s.group[e.ke].mon[e.id].detector_notrigger_timeout_function,s.group[e.ke].mon[e.id].detector_notrigger_timeout) + } + if(e.details.snap === '1'){ + var resetSnapCheck = function(){ + clearTimeout(s.group[e.ke].mon[e.id].checkSnap) + s.group[e.ke].mon[e.id].checkSnap = setTimeout(function(){ + if(s.group[e.ke].mon[e.id].isStarted === true){ + fs.stat(e.sdir+'s.jpg',function(err,snap){ + var notStreaming = function(){ + s.launchMonitorProcesses(e) + s.userLog(e,{type:lang['Camera is not streaming'],msg:{msg:lang['Restarting Process']}}) + s.orphanedVideoCheck(e,2,null,true) + } + if(err){ + notStreaming() + }else{ + if(!e.checkSnapTime)e.checkSnapTime = snap.mtime + if(err || e.checkSnapTime === snap.mtime){ + e.checkSnapTime = snap.mtime + notStreaming() + }else{ + resetSnapCheck() + } + } + }) + } + },60000*1); + } + resetSnapCheck() + } + if(config.childNodes.mode !== 'child' && s.platform!=='darwin' && (e.functionMode === 'record' || (e.functionMode === 'start'&&e.details.detector_record_method==='sip'))){ + //check if ffmpeg is recording + s.group[e.ke].mon[e.id].fswatch = fs.watch(e.dir, {encoding : 'utf8'}, (event, filename) => { + switch(event){ + case'rename': + s.group[e.ke].mon[e.id].open = filename.split('.')[0] + break; + case'change': + s.resetRecordingCheck(e) + break; + } + }); + } + if( + //is MacOS + s.platform !== 'darwin' && + //is Watch-Only or Record + (e.functionMode === 'start' || e.functionMode === 'record') && + //if JPEG API enabled or Stream Type is HLS + ( + e.details.stream_type === 'jpeg' || + e.details.stream_type === 'hls' || + e.details.snap === '1' + ) + ){ + s.group[e.ke].mon[e.id].fswatchStream = fs.watch(e.sdir, {encoding : 'utf8'}, () => { + s.resetStreamCheck(e) + }) + } + s.cameraSendSnapshot({mid:e.id,ke:e.ke,mon:e}) + //check host to see if has password and user in it + setStreamDir() + clearTimeout(s.group[e.ke].mon[e.id].recordingChecker) + if(s.group[e.ke].mon[e.id].isStarted === true){ + e.errorCount = 0; + s.group[e.ke].mon[e.id].errorSocketTimeoutCount = 0; + s.cameraDestroy(s.group[e.ke].mon[e.id].spawn,e) + startVideoProcessor = function(err,o){ + if(o.success === true){ + s.group[e.ke].mon[e.id].isRecording = true + s.createCameraFfmpegProcess(e) + s.createCameraStreamHandlers(e) + if(e.type === 'dashcam'){ + setTimeout(function(){ + s.group[e.ke].mon[e.id].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' + ){ + s.cameraFilterFfmpegLog(e) + } + }else{ + s.userLog(e,{type:lang["Ping Failed"],msg:lang.skipPingText1}); + s.fatalCameraError(e,"Ping Failed");return; + } + } + if( + e.type !== 'socket' && + e.type !== 'dashcam' && + e.protocol !== 'udp' && + e.type !== 'local' && + e.details.skip_ping !== '1' + ){ + connectionTester.test(strippedHost,e.port,2000,startVideoProcessor); + }else{ + startVideoProcessor(null,{success:true}) + } + }else{ + s.cameraDestroy(s.group[e.ke].mon[e.id].spawn,e) + } + } + var doOnChildMachine = function(){ + startVideoProcessor = function(){ + s.cx({ + //function + f : 'cameraStart', + //mode + mode : e.functionMode, + //data, options + d : s.group[e.ke].mon_conf[e.id] + },s.group[e.ke].mon[e.id].childNodeId) + } + if( + e.type !== 'socket' && + e.type !== 'dashcam' && + e.protocol !== 'udp' && + e.type !== 'local' && + e.details.skip_ping !== '1' + ){ + connectionTester.test(strippedHost,e.port,2000,function(err,o){ + if(o.success === true){ + startVideoProcessor() + }else{ + s.userLog(e,{type:lang["Ping Failed"],msg:lang.skipPingText1}); + s.fatalCameraError(e,"Ping Failed");return; + } + }) + }else{ + startVideoProcessor() + } + } + try{ + if(config.childNodes.enabled === true && config.childNodes.mode === 'master'){ + var copiedMonitorObject = s.cleanMonitorObject(s.group[e.ke].mon_conf[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 + // s.childNodes[ip].coreCount + s.group[e.ke].mon[e.id].onChildNodeExit = function(){ + if(s.childNodes[ip])delete(s.childNodes[ip].activeCameras[e.ke+e.id]) + } + } + var nodeWithLowestActiveCamerasCount = 65535 + var nodeWithLowestActiveCameras = null + childNodeList.forEach(function(ip){ + if(Object.keys(s.childNodes[ip].activeCameras).length < nodeWithLowestActiveCamerasCount){ + nodeWithLowestActiveCameras = ip + } + }) + if(nodeWithLowestActiveCameras)selectNode(nodeWithLowestActiveCameras) + if(e.childNodeFound === true){ + s.childNodes[e.childNodeSelected].activeCameras[e.ke+e.id] = copiedMonitorObject + s.group[e.ke].mon[e.id].childNode = e.childNodeSelected + s.group[e.ke].mon[e.id].childNodeId = s.childNodes[e.childNodeSelected].cnid; + s.cx({f:'sync',sync:s.group[e.ke].mon_conf[e.id],ke:e.ke,mid:e.id},s.group[e.ke].mon[e.id].childNodeId); + doOnChildMachine() + }else{ + doOnThisMachine() + } + }else{ + doOnThisMachine() + } + }else{ + doOnThisMachine() + } + }catch(err){ + doOnThisMachine() + console.log(err) + } + } + s.fatalCameraError = function(e,errorMessage){ + clearTimeout(s.group[e.ke].mon[e.id].err_fatal_timeout); + ++e.errorFatalCount; + if(s.group[e.ke].mon[e.id].isStarted === true){ + s.group[e.ke].mon[e.id].err_fatal_timeout = setTimeout(function(){ + if(e.details.fatal_max !== 0 && e.errorFatalCount > e.details.fatal_max){ + s.camera('stop',{id:e.id,ke:e.ke}) + }else{ + s.launchMonitorProcesses(e) + }; + },5000); + }else{ + s.cameraDestroy(s.group[e.ke].mon[e.id].spawn,e) + } + s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Died}); + } + s.isWatchCountable = function(d){ + try{ + var variableMethodsToAllow = [ + 'mp4ws', //Poseidon over Websocket + 'flvws', + 'h265ws', + ]; + var indefiniteIgnore = [ + 'mjpeg', + 'h264', + ]; + var monConfig = s.group[d.ke].mon_conf[d.id] + if( + variableMethodsToAllow.indexOf(monConfig.details.stream_type + monConfig.details.stream_flv_type) > -1 && + indefiniteIgnore.indexOf(monConfig.details.stream_type) === -1 + ){ + return true + } + }catch(err){} + return false + } + s.camera = function(x,e,cn){ + // x = function or mode + // e = monitor object + // cn = socket connection or callback or options (depends on function chosen) + if(cn && cn.ke && !e.ke){e.ke = cn.ke} + e.functionMode = x + if(!e.mode){e.mode = x} + s.checkDetails(e) + s.cameraCheckObjectsInDetails(e) + 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].mon[e.id].watch[cn.id]={}; + var numberOfViewers = Object.keys(s.group[e.ke].mon[e.id].watch).length + s.tx({ + viewers: numberOfViewers, + ke: e.ke, + id: e.id + },'MON_'+e.ke+e.id) + break; + case'watch_off'://live streamers - leave + if(cn.monitorsCurrentlyWatching){delete(cn.monitorsCurrentlyWatching[e.id])} + var numberOfViewers = 0 + delete(s.group[e.ke].mon[e.id].watch[cn.id]); + numberOfViewers = Object.keys(s.group[e.ke].mon[e.id].watch).length + s.tx({ + viewers: numberOfViewers, + ke: e.ke, + id: e.id + },'MON_'+e.ke+e.id) + break; + case'restart'://restart monitor + s.sendMonitorStatus({id:e.id,ke:e.ke,status:'Restarting'}); + s.camera('stop',e) + setTimeout(function(){ + s.camera(e.mode,e) + },1300) + break; + case'idle':case'stop'://stop monitor + if(!s.group[e.ke]||!s.group[e.ke].mon[e.id]){return} + if(config.childNodes.enabled === true && config.childNodes.mode === 'master' && s.group[e.ke].mon[e.id].childNode && s.childNodes[s.group[e.ke].mon[e.id].childNode].activeCameras[e.ke+e.id]){ + s.group[e.ke].mon[e.id].isStarted = false + s.cx({ + //function + f : 'cameraStop', + //data, options + d : s.group[e.ke].mon_conf[e.id] + },s.group[e.ke].mon[e.id].childNodeId) + s.cx({f:'sync',sync:s.group[e.ke].mon_conf[e.id],ke:e.ke,mid:e.id},s.group[e.ke].mon[e.id].childNodeId); + }else{ + s.closeEventBasedRecording(e) + if(s.group[e.ke].mon[e.id].fswatch){s.group[e.ke].mon[e.id].fswatch.close();delete(s.group[e.ke].mon[e.id].fswatch)} + if(s.group[e.ke].mon[e.id].fswatchStream){s.group[e.ke].mon[e.id].fswatchStream.close();delete(s.group[e.ke].mon[e.id].fswatchStream)} + if(s.group[e.ke].mon[e.id].last_frame){delete(s.group[e.ke].mon[e.id].last_frame)} + if(s.group[e.ke].mon[e.id].isStarted !== true){return} + s.cameraDestroy(s.group[e.ke].mon[e.id].spawn,e) + if(e.neglectTriggerTimer === 1){ + delete(e.neglectTriggerTimer); + }else{ + clearTimeout(s.group[e.ke].mon[e.id].trigger_timer) + delete(s.group[e.ke].mon[e.id].trigger_timer) + } + clearInterval(s.group[e.ke].mon[e.id].running); + clearInterval(s.group[e.ke].mon[e.id].detector_notrigger_timeout) + clearTimeout(s.group[e.ke].mon[e.id].err_fatal_timeout); + s.group[e.ke].mon[e.id].isStarted = false + s.group[e.ke].mon[e.id].isRecording = false + s.tx({f:'monitor_stopping',mid:e.id,ke:e.ke,time:s.formattedTime()},'GRP_'+e.ke); + s.cameraSendSnapshot({mid:e.id,ke:e.ke,mon:e}) + if(e.functionMode === 'stop'){ + s.userLog(e,{type:lang['Monitor Stopped'],msg:lang.MonitorStoppedText}); + clearTimeout(s.group[e.ke].mon[e.id].delete) + if(e.delete===1){ + s.group[e.ke].mon[e.id].delete=setTimeout(function(){ + delete(s.group[e.ke].mon[e.id]); + delete(s.group[e.ke].mon_conf[e.id]); + },1000*60); + } + }else{ + s.tx({f:'monitor_idle',mid:e.id,ke:e.ke,time:s.formattedTime()},'GRP_'+e.ke); + s.userLog(e,{type:lang['Monitor Idling'],msg:lang.MonitorIdlingText}); + } + } + var wantedStatus = lang.Stopped + if(e.functionMode === 'idle'){ + var wantedStatus = lang.Idle + } + s.sendMonitorStatus({id:e.id,ke:e.ke,status:wantedStatus}) + break; + case'start':case'record'://watch or record monitor url + s.initiateMonitorObject({ke:e.ke,mid:e.id}) + if(!s.group[e.ke].mon_conf[e.id]){s.group[e.ke].mon_conf[e.id]=s.cleanMonitorObject(e);} + e.url = s.buildMonitorUrl(e); + if(s.group[e.ke].mon[e.id].isStarted === true){ + //stop action, monitor already started or recording + return + } + //lock this function + s.sendMonitorStatus({id:e.id,ke:e.ke,status:lang.Starting}); + s.group[e.ke].mon[e.id].isStarted = true + //set recording status + e.wantedStatus = lang.Watching + if(e.functionMode === 'record'){ + e.wantedStatus = lang.Recording + s.group[e.ke].mon[e.id].isRecording = true + }else{ + s.group[e.ke].mon[e.mid].isRecording = false + } + //set up fatal error handler + if(e.details.fatal_max===''){ + e.details.fatal_max = 10 + }else{ + e.details.fatal_max = parseFloat(e.details.fatal_max) + } + e.errorFatalCount = 0; + //cutoff time and recording check interval + if(!e.details.cutoff||e.details.cutoff===''){e.cutoff=15}else{e.cutoff=parseFloat(e.details.cutoff)}; + if(isNaN(e.cutoff)===true){e.cutoff=15} + //start drawing files + delete(s.group[e.ke].mon[e.id].childNode) + s.launchMonitorProcesses(e) + break; + default: + console.log(x) + break; + } + if(typeof cn === 'function'){setTimeout(function(){cn()},1000)} + } +} diff --git a/libs/notification.js b/libs/notification.js new file mode 100644 index 0000000..70118e9 --- /dev/null +++ b/libs/notification.js @@ -0,0 +1,318 @@ +var Discord = require("discord.js") +module.exports = function(s,config,lang){ + //discord bot + if(config.discordBot === true){ + try{ + s.discordMsg = function(data,files,groupKey){ + if(!data)data = {}; + var bot = s.group[groupKey].discordBot + if(!bot){ + s.userLog({ke:groupKey,mid:'$USER'},{type:lang.DiscordFailedText,msg:lang.DiscordNotEnabledText}) + return + } + var sendBody = Object.assign({ + color: 3447003, + title: 'Alert from Shinobi', + description: "", + fields: [], + timestamp: new Date(), + footer: { + icon_url: config.iconURL, + text: "Shinobi Systems" + } + },data) + bot.channels.get(s.group[groupKey].init.discordbot_channel).send({ + embed: sendBody, + files: files + }).catch(err => { + if(err){ + s.userLog({ke:groupKey,mid:'$USER'},{type:lang.DiscordErrorText,msg:err}) + s.group[groupKey].discordBot = null + s.loadGroupApps({ke:groupKey}) + } + }) + } + var onEventTriggerBeforeFilterForDiscord = function(d,filter){ + filter.discord = true + } + var onEventTriggerForDiscord = function(d,filter){ + // d = event object + //discord bot + if(filter.discord && d.mon.details.detector_discordbot === '1' && !s.group[d.ke].mon[d.id].detector_discordbot){ + var detector_discordbot_timeout + if(!d.mon.details.detector_discordbot_timeout||d.mon.details.detector_discordbot_timeout===''){ + detector_discordbot_timeout = 1000*60*10; + }else{ + detector_discordbot_timeout = parseFloat(d.mon.details.detector_discordbot_timeout)*1000*60; + } + //lock mailer so you don't get emailed on EVERY trigger event. + s.group[d.ke].mon[d.id].detector_discordbot=setTimeout(function(){ + //unlock so you can mail again. + clearTimeout(s.group[d.ke].mon[d.id].detector_discordbot); + delete(s.group[d.ke].mon[d.id].detector_discordbot); + },detector_discordbot_timeout); + var files = [] + var sendAlert = function(){ + s.discordMsg({ + author: { + name: s.group[d.ke].mon_conf[d.id].name, + icon_url: config.iconURL + }, + title: lang.Event+' - '+d.screenshotName, + description: lang.EventText1+' '+d.currentTimestamp, + fields: [], + timestamp: d.currentTime, + footer: { + icon_url: config.iconURL, + text: "Shinobi Systems" + } + },files,d.ke) + } + if(d.mon.details.detector_discordbot_send_video === '1'){ + s.mergeDetectorBufferChunks(d,function(mergedFilepath,filename){ + s.discordMsg({ + author: { + name: s.group[d.ke].mon_conf[d.id].name, + icon_url: config.iconURL + }, + title: filename, + fields: [], + timestamp: d.currentTime, + footer: { + icon_url: config.iconURL, + text: "Shinobi Systems" + } + },[ + { + attachment: mergedFilepath, + name: filename + } + ],d.ke) + }) + } + s.getRawSnapshotFromMonitor(d.mon,function(data){ + if((data[data.length-2] === 0xFF && data[data.length-1] === 0xD9)){ + d.screenshotBuffer = data + files.push({ + attachment: d.screenshotBuffer, + name: d.screenshotName+'.jpg' + }) + } + sendAlert() + }) + } + } + var onTwoFactorAuthCodeNotificationForDiscord = function(r){ + // r = user + if(r.details.factor_discord === '1'){ + s.discordMsg({ + author: { + name: r.lang['2-Factor Authentication'], + icon_url: config.iconURL + }, + title: r.lang['Enter this code to proceed'], + description: '**'+s.factorAuth[r.ke][r.uid].key+'** '+r.lang.FactorAuthText1, + fields: [], + timestamp: new Date(), + footer: { + icon_url: config.iconURL, + text: "Shinobi Systems" + } + },[],r.ke) + } + } + var loadDiscordBotForUser = function(user){ + ar=JSON.parse(user.details); + //discordbot + if(!s.group[user.ke].discordBot && + config.discordBot === true && + ar.discordbot === '1' && + ar.discordbot_token !== '' + ){ + s.group[user.ke].discordBot = new Discord.Client() + s.group[user.ke].discordBot.on('ready', () => { + console.log(`${user.mail} : Discord Bot Logged in as ${s.group[user.ke].discordBot.user.tag}!`) + }) + s.group[user.ke].discordBot.login(ar.discordbot_token) + } + } + var unloadDiscordBotForUser = function(user){ + if(s.group[user.ke].discordBot && s.group[user.ke].discordBot.destroy){ + s.group[user.ke].discordBot.destroy() + delete(s.group[user.ke].discordBot) + } + } + s.loadGroupAppExtender(loadDiscordBotForUser) + s.unloadGroupAppExtender(unloadDiscordBotForUser) + s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotificationForDiscord) + s.onEventTrigger(onEventTriggerForDiscord) + s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilterForDiscord) + }catch(err){ + console.log(err) + console.log('Could not start Discord bot, please run "npm install discord.js" inside the Shinobi folder.') + s.discordMsg = function(){} + } + } + // mailing with nodemailer + try{ + if(config.mail){ + if(config.mail.from === undefined){config.mail.from = '"ShinobiCCTV" '} + s.nodemailer = require('nodemailer').createTransport(config.mail); + } + var onDetectorNoTriggerTimeoutForEmail = function(e){ + //e = monitor object + if(config.mail && e.details.detector_notrigger_mail === '1'){ + s.sqlQuery('SELECT mail FROM Users WHERE ke=? AND details NOT LIKE ?',[e.ke,'%"sub"%'],function(err,r){ + r = r[0] + var mailOptions = { + from: config.mail.from, // sender address + to: r.mail, // list of receivers + subject: lang.NoMotionEmailText1+' '+e.name+' ('+e.id+')', // Subject line + html: ''+lang.NoMotionEmailText2+' '+e.details.detector_notrigger_timeout+' '+lang.minutes+'.', + } + mailOptions.html+='
'+lang['Monitor Name']+' : '+e.name+'
' + mailOptions.html+='
'+lang['Monitor ID']+' : '+e.id+'
' + s.nodemailer.sendMail(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); + }) + }) + } + } + var onTwoFactorAuthCodeNotificationForEmail = function(r){ + // r = user object + if(r.details.factor_mail !== '0'){ + var mailOptions = { + from: config.mail.from, + to: r.mail, + subject: r.lang['2-Factor Authentication'], + html: r.lang['Enter this code to proceed']+' '+s.factorAuth[r.ke][r.uid].key+'. '+r.lang.FactorAuthText1, + }; + s.nodemailer.sendMail(mailOptions, (error, info) => { + if (error) { + s.systemLog(r.lang.MailError,error) + return + } + }) + } + } + var onFilterEventForEmail = function(x,d){ + // x = filter function + // d = filter event object + if(x === 'email'){ + if(d.videos && d.videos.length > 0){ + d.mailOptions = { + from: config.mail.from, // sender address + to: d.mail, // list of receivers + subject: lang['Filter Matches']+' : '+d.name, // Subject line + html: lang.FilterMatchesText1+' '+d.videos.length+' '+lang.FilterMatchesText2, + }; + if(d.execute&&d.execute!==''){ + d.mailOptions.html+='
'+lang.Executed+' : '+d.execute+'
' + } + if(d.delete==='1'){ + d.mailOptions.html+='
'+lang.Deleted+' : '+lang.Yes+'
' + } + d.mailOptions.html+='
'+lang.Query+' : '+d.query+'
' + d.mailOptions.html+='
'+lang['Filter ID']+' : '+d.id+'
' + s.nodemailer.sendMail(d.mailOptions, (error, info) => { + if (error) { + s.tx({f:'error',ff:'filter_mail',ke:d.ke,error:error},'GRP_'+d.ke); + return ; + } + s.tx({f:'filter_mail',ke:d.ke,info:info},'GRP_'+d.ke); + }) + } + } + } + var onEventTriggerBeforeFilterForEmail = function(d,filter){ + filter.mail = true + } + var onEventTriggerForEmail = function(d,filter){ + if(filter.mail && config.mail && !s.group[d.ke].mon[d.id].detector_mail && d.mon.details.detector_mail === '1'){ + s.sqlQuery('SELECT mail FROM Users WHERE ke=? AND details NOT LIKE ?',[d.ke,'%"sub"%'],function(err,r){ + r=r[0]; + var detector_mail_timeout + if(!d.mon.details.detector_mail_timeout||d.mon.details.detector_mail_timeout===''){ + detector_mail_timeout = 1000*60*10; + }else{ + detector_mail_timeout = parseFloat(d.mon.details.detector_mail_timeout)*1000*60; + } + //lock mailer so you don't get emailed on EVERY trigger event. + s.group[d.ke].mon[d.id].detector_mail=setTimeout(function(){ + //unlock so you can mail again. + clearTimeout(s.group[d.ke].mon[d.id].detector_mail); + delete(s.group[d.ke].mon[d.id].detector_mail); + },detector_mail_timeout); + var files = [] + var mailOptions = { + from: config.mail.from, // sender address + to: r.mail, // list of receivers + subject: lang.Event+' - '+d.screenshotName, // Subject line + html: ''+lang.EventText1+' '+d.currentTimestamp+'.', + attachments: files + } + var sendMail = function(){ + Object.keys(d.details).forEach(function(v,n){ + mailOptions.html+='
'+v+' : '+d.details[v]+'
' + }) + s.nodemailer.sendMail(mailOptions, (error, info) => { + if (error) { + s.systemLog(lang.MailError,error) + return false; + } + }) + } + if(d.mon.details.detector_mail_send_video === '1'){ + s.mergeDetectorBufferChunks(d,function(mergedFilepath,filename){ + s.nodemailer.sendMail({ + from: config.mail.from, + to: r.mail, + subject: filename, + html: '', + attachments: [ + { + filename: filename, + content: fs.readFileSync(mergedFilepath) + } + ] + }, (error, info) => { + if (error) { + s.systemLog(lang.MailError,error) + return false; + } + }) + }) + } + if(d.screenshotBuffer){ + files.push({ + filename: d.screenshotName+'.jpg', + content: d.screenshotBuffer + }) + sendMail() + }else{ + s.getRawSnapshotFromMonitor(d.mon,function(data){ + d.screenshotBuffer = data + files.push({ + filename: d.screenshotName+'.jpg', + content: data + }) + sendMail() + }) + } + }) + } + } + s.onTwoFactorAuthCodeNotification(onTwoFactorAuthCodeNotificationForEmail) + s.onEventTriggerBeforeFilter(onEventTriggerBeforeFilterForEmail) + s.onEventTrigger(onEventTriggerForEmail) + s.onFilterEvent(onFilterEventForEmail) + s.onDetectorNoTriggerTimeout(onDetectorNoTriggerTimeoutForEmail) + }catch(err){ + console.log(err) + } +} diff --git a/libs/plugins.js b/libs/plugins.js new file mode 100644 index 0000000..44f989e --- /dev/null +++ b/libs/plugins.js @@ -0,0 +1,105 @@ +var socketIOclient = require('socket.io-client'); +module.exports = function(s,config,lang){ + //function for receiving detector data + s.pluginEventController=function(d){ + switch(d.f){ + case'trigger': + s.triggerEvent(d) + break; + case's.tx': + s.tx(d.data,d.to) + break; + case's.sqlQuery': + s.sqlQuery(d.query,d.values) + break; + case'log': + s.systemLog('PLUGIN : '+d.plug+' : ',d) + break; + } + } + //multi plugin connections + s.connectedPlugins={} + s.pluginInitiatorSuccess=function(mode,d,cn){ + s.systemLog('pluginInitiatorSuccess',d) + if(mode==='client'){ + //is in client mode (camera.js is client) + cn.pluginEngine=d.plug + if(!s.connectedPlugins[d.plug]){ + s.connectedPlugins[d.plug]={plug:d.plug} + } + s.systemLog('Connected to plugin : Detector - '+d.plug+' - '+d.type) + switch(d.type){ + default:case'detector': + s.ocv={started:s.timeObject(),id:cn.id,plug:d.plug,notice:d.notice,isClientPlugin:true}; + cn.ocv=1; + s.tx({f:'detector_plugged',plug:d.plug,notice:d.notice},'CPU') + break; + } + }else{ + //is in host mode (camera.js is client) + switch(d.type){ + default:case'detector': + s.ocv={started:s.timeObject(),id:"host",plug:d.plug,notice:d.notice,isHostPlugin:true}; + break; + } + } + s.connectedPlugins[d.plug].plugged=true + s.tx({f:'readPlugins',ke:d.ke},'CPU') + s.ocvTx({f:'api_key',key:d.plug}) + s.api[d.plug]={pluginEngine:d.plug,permissions:{},details:{},ip:'0.0.0.0'}; + } + s.pluginInitiatorFail=function(mode,d,cn){ + if(s.connectedPlugins[d.plug])s.connectedPlugins[d.plug].plugged=false + if(mode==='client'){ + //is in client mode (camera.js is client) + cn.disconnect() + }else{ + //is in host mode (camera.js is client) + } + } + if(config.plugins&&config.plugins.length>0){ + config.plugins.forEach(function(v){ + s.connectedPlugins[v.id]={plug:v.id} + if(v.enabled===false){return} + if(v.mode==='host'){ + //is in host mode (camera.js is client) + if(v.https===true){ + v.https='https://' + }else{ + v.https='http://' + } + if(!v.port){ + v.port=80 + } + var socket = socketIOclient(v.https+v.host+':'+v.port) + s.connectedPlugins[v.id].tx = function(x){return socket.emit('f',x)} + socket.on('connect', function(cn){ + s.systemLog('Connected to plugin (host) : '+v.id) + s.connectedPlugins[v.id].tx({f:'init_plugin_as_host',key:v.key}) + }); + socket.on('init',function(d){ + s.systemLog('Initialize Plugin : Host',d) + if(d.ok===true){ + s.pluginInitiatorSuccess("host",d) + }else{ + s.pluginInitiatorFail("host",d) + } + }); + socket.on('ocv',s.pluginEventController); + socket.on('disconnect', function(){ + s.connectedPlugins[v.id].plugged=false + delete(s.api[v.id]) + s.systemLog('Plugin Disconnected : '+v.id) + s.connectedPlugins[v.id].reconnector = setInterval(function(){ + if(socket.connected===true){ + clearInterval(s.connectedPlugins[v.id].reconnector) + }else{ + socket.connect() + } + },1000*2) + }); + s.connectedPlugins[v.id].ws = socket; + } + }) + } +} diff --git a/libs/process.js b/libs/process.js new file mode 100644 index 0000000..122d2f1 --- /dev/null +++ b/libs/process.js @@ -0,0 +1,33 @@ +var os = require('os') +module.exports = function(process,__dirname){ + process.send = process.send || function () {}; + process.on('uncaughtException', function (err) { + console.error('Uncaught Exception occured!'); + console.error(err.stack); + }); + // [CTRL] + [C] = exit + process.on('SIGINT', function() { + console.log('Shinobi is Exiting...') + process.exit(); + }); + // s = Shinobi + s = { + //Total Memory + coreCount : os.cpus().length, + //Total Memory + totalmem : os.totalmem(), + //Check Platform + platform : os.platform(), + //JSON stringify short-hand + s : JSON.stringify, + //Pretty Print JSON + prettyPrint : function(obj){return JSON.stringify(obj,null,3)}, + //Check if Windows + isWin : (process.platform === 'win32' || process.platform === 'win64'), + //UTC Offset + utcOffset : require('moment')().utcOffset(), + //directory path for this file + mainDirectory : __dirname + } + return s +} diff --git a/libs/socketio.js b/libs/socketio.js new file mode 100644 index 0000000..0f7058d --- /dev/null +++ b/libs/socketio.js @@ -0,0 +1,1433 @@ +var os = require('os'); +var moment = require('moment'); +var execSync = require('child_process').execSync; +var exec = require('child_process').exec; +var spawn = require('child_process').spawn; +var jsonfile = require("jsonfile"); +var onvif = require("node-onvif"); +module.exports = function(s,config,lang,io){ + //send data to detector plugin + s.ocvTx=function(data){ + if(!s.ocv){return} + if(s.ocv.isClientPlugin===true){ + s.tx(data,s.ocv.id) + }else{ + s.connectedPlugins[s.ocv.plug].tx(data) + } + } + //send data to socket client function + s.tx = function(z,y,x){if(x){return x.broadcast.to(y).emit('f',z)};io.to(y).emit('f',z);} + s.txToDashcamUsers = function(data,groupKey){ + if(s.group[groupKey] && s.group[groupKey].dashcamUsers){ + Object.keys(s.group[groupKey].dashcamUsers).forEach(function(auth){ + s.tx(data,s.group[groupKey].dashcamUsers[auth].cnid) + }) + } + } + s.txWithSubPermissions = function(z,y,permissionChoices){ + if(typeof permissionChoices==='string'){ + permissionChoices=[permissionChoices] + } + if(s.group[z.ke]){ + Object.keys(s.group[z.ke].users).forEach(function(v){ + var user = s.group[z.ke].users[v] + if(user.details.sub){ + if(user.details.allmonitors!=='1'){ + var valid=0 + var checked=permissionChoices.length + permissionChoices.forEach(function(b){ + if(user.details[b] && user.details[b].indexOf(z.mid)!==-1){ + ++valid + } + }) + if(valid===checked){ + s.tx(z,user.cnid) + } + }else{ + s.tx(z,user.cnid) + } + }else{ + s.tx(z,user.cnid) + } + }) + } + } + + ////socket controller + io.on('connection', function (cn) { + var tx; + //set "client" detector plugin event function + cn.on('ocv',function(d){ + if(!cn.pluginEngine&&d.f==='init'){ + if(config.pluginKeys[d.plug]===d.pluginKey){ + s.pluginInitiatorSuccess("client",d,cn) + }else{ + s.pluginInitiatorFail("client",d,cn) + } + }else{ + if(config.pluginKeys[d.plug]===d.pluginKey){ + s.pluginEventController(d) + }else{ + cn.disconnect() + } + } + }) + //unique h265 socket stream + cn.on('h265',function(d){ + if(!s.group[d.ke]||!s.group[d.ke].mon||!s.group[d.ke].mon[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);} + d.failed=function(msg){ + tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); + cn.disconnect(); + } + d.success=function(r){ + r=r[0]; + var Emitter,chunkChannel + if(!d.channel){ + Emitter = s.group[d.ke].mon[d.id].emitter + chunkChannel = 'MAIN' + }else{ + Emitter = s.group[d.ke].mon[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] + chunkChannel = parseInt(d.channel)+config.pipeAddition + } + if(!Emitter){ + cn.disconnect();return; + } + if(!d.channel)d.channel = 'MAIN'; + cn.ke=d.ke, + cn.uid=d.uid, + cn.auth=d.auth; + cn.channel=d.channel; + cn.removeListenerOnDisconnect=true; + cn.socketVideoStream=d.id; + 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]){ + d.success(s.group[d.ke].users[d.auth]); + }else{ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + r=r[0] + r.details=JSON.parse(r.details) + if(r.details.auth_socket==='1'){ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + d.failed('User not found') + } + }) + }else{ + d.failed('Permissions for this key do not allow authentication with Websocket') + } + }else{ + d.failed('Not an API key') + } + }) + } + }) + } + }) + //unique Base64 socket stream + cn.on('Base64',function(d){ + if(!s.group[d.ke]||!s.group[d.ke].mon||!s.group[d.ke].mon[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);} + d.failed=function(msg){ + tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); + cn.disconnect(); + } + d.success=function(r){ + r=r[0]; + var Emitter,chunkChannel + if(!d.channel){ + Emitter = s.group[d.ke].mon[d.id].emitter + chunkChannel = 'MAIN' + }else{ + Emitter = s.group[d.ke].mon[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] + chunkChannel = parseInt(d.channel)+config.pipeAddition + } + if(!Emitter){ + cn.disconnect();return; + } + if(!d.channel)d.channel = 'MAIN'; + cn.ke=d.ke, + cn.uid=d.uid, + cn.auth=d.auth; + cn.channel=d.channel; + cn.removeListenerOnDisconnect=true; + cn.socketVideoStream=d.id; + 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]){ + d.success(s.group[d.ke].users[d.auth]); + }else{ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + r=r[0] + r.details=JSON.parse(r.details) + if(r.details.auth_socket==='1'){ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + d.failed('User not found') + } + }) + }else{ + d.failed('Permissions for this key do not allow authentication with Websocket') + } + }else{ + d.failed('Not an API key') + } + }) + } + }) + } + }) + //unique FLV socket stream + cn.on('FLV',function(d){ + if(!s.group[d.ke]||!s.group[d.ke].mon||!s.group[d.ke].mon[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);} + d.failed=function(msg){ + tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); + cn.disconnect(); + } + d.success=function(r){ + r=r[0]; + var Emitter,chunkChannel + if(!d.channel){ + Emitter = s.group[d.ke].mon[d.id].emitter + chunkChannel = 'MAIN' + }else{ + Emitter = s.group[d.ke].mon[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] + chunkChannel = parseInt(d.channel)+config.pipeAddition + } + if(!Emitter){ + cn.disconnect();return; + } + if(!d.channel)d.channel = 'MAIN'; + cn.ke=d.ke, + cn.uid=d.uid, + cn.auth=d.auth; + cn.channel=d.channel; + cn.removeListenerOnDisconnect=true; + cn.socketVideoStream=d.id; + var contentWriter + cn.closeSocketVideoStream = function(){ + Emitter.removeListener('data', contentWriter); + } + tx({time:toUTC(),buffer:s.group[d.ke].mon[d.id].firstStreamChunk[chunkChannel]}) + Emitter.on('data',contentWriter = function(buffer){ + tx({time:toUTC(),buffer:buffer}) + }) + } + if(s.group[d.ke] && s.group[d.ke].users && s.group[d.ke].users[d.auth]){ + d.success(s.group[d.ke].users[d.auth]); + }else{ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + r=r[0] + r.details=JSON.parse(r.details) + if(r.details.auth_socket==='1'){ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + d.failed('User not found') + } + }) + }else{ + d.failed('Permissions for this key do not allow authentication with Websocket') + } + }else{ + d.failed('Not an API key') + } + }) + } + }) + } + }) + //unique MP4 socket stream + cn.on('MP4',function(d){ + if(!s.group[d.ke]||!s.group[d.ke].mon||!s.group[d.ke].mon[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);} + d.failed=function(msg){ + tx({f:'stop_reconnect',msg:msg,token_used:d.auth,ke:d.ke}); + cn.disconnect(); + } + d.success=function(r){ + r=r[0]; + var Emitter,chunkChannel + if(!d.channel){ + Emitter = s.group[d.ke].mon[d.id].emitter + chunkChannel = 'MAIN' + }else{ + Emitter = s.group[d.ke].mon[d.id].emitterChannel[parseInt(d.channel)+config.pipeAddition] + chunkChannel = parseInt(d.channel)+config.pipeAddition + } + if(!Emitter){ + cn.disconnect();return; + } + if(!d.channel)d.channel = 'MAIN'; + cn.ke=d.ke, + cn.uid=d.uid, + cn.auth=d.auth; + cn.channel=d.channel; + cn.socketVideoStream=d.id; + var mp4frag = s.group[d.ke].mon[d.id].mp4frag[d.channel]; + var onInitialized = () => { + cn.emit('mime', mp4frag.mime); + mp4frag.removeListener('initialized', onInitialized); + }; + //event listener + var onSegment = function(data){ + cn.emit('segment', data); + }; + cn.closeSocketVideoStream = function(){ + if(mp4frag){ + mp4frag.removeListener('segment', onSegment) + mp4frag.removeListener('initialized', onInitialized) + } + } + cn.on('MP4Command',function(msg){ + switch (msg) { + case 'mime' ://client is requesting mime + var mime = mp4frag.mime; + if (mime) { + cn.emit('mime', mime); + } else { + mp4frag.on('initialized', onInitialized); + } + break; + case 'initialization' ://client is requesting initialization segment + cn.emit('initialization', mp4frag.initialization); + break; + case 'segment' ://client is requesting a SINGLE segment + var segment = mp4frag.segment; + if (segment) { + cn.emit('segment', segment); + } else { + mp4frag.once('segment', onSegment); + } + break; + case 'segments' ://client is requesting ALL segments + //send current segment first to start video asap + var segment = mp4frag.segment; + if (segment) { + cn.emit('segment', segment); + } + //add listener for segments being dispatched by mp4frag + mp4frag.on('segment', onSegment); + break; + case 'pause' : + mp4frag.removeListener('segment', onSegment); + break; + case 'resume' : + mp4frag.on('segment', onSegment); + break; + case 'stop' ://client requesting to stop receiving segments + cn.closeSocketVideoStream() + break; + } + }) + } + if(s.group[d.ke]&&s.group[d.ke].users&&s.group[d.ke].users[d.auth]){ + d.success(s.group[d.ke].users[d.auth]); + }else{ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + r=r[0] + r.details=JSON.parse(r.details) + if(r.details.auth_socket==='1'){ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + d.failed('User not found') + } + }) + }else{ + d.failed('Permissions for this key do not allow authentication with Websocket') + } + }else{ + d.failed('Not an API key') + } + }) + } + }) + } + }) + //main socket control functions + cn.on('f',function(d){ + if(!cn.ke&&d.f==='init'){//socket login + cn.ip=cn.request.connection.remoteAddress; + tx=function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);} + d.failed=function(){tx({ok:false,msg:'Not Authorized',token_used:d.auth,ke:d.ke});cn.disconnect();} + d.success=function(r){ + r=r[0];cn.join('GRP_'+d.ke);cn.join('CPU'); + cn.ke=d.ke, + cn.uid=d.uid, + cn.auth=d.auth; + if(!s.group[d.ke])s.group[d.ke]={}; + // if(!s.group[d.ke].vid)s.group[d.ke].vid={}; + if(!s.group[d.ke].users)s.group[d.ke].users={}; + // s.group[d.ke].vid[cn.id]={uid:d.uid}; + s.group[d.ke].users[d.auth]={cnid:cn.id,uid:r.uid,mail:r.mail,details:JSON.parse(r.details),logged_in_at:s.timeObject(new Date).format(),login_type:'Dashboard'} + try{s.group[d.ke].users[d.auth].details=JSON.parse(r.details)}catch(er){} + 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.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].mon){ + s.group[d.ke].mon={} + if(!s.group[d.ke].mon){s.group[d.ke].mon={}} + } + if(s.ocv){ + tx({f:'detector_plugged',plug:s.ocv.plug,notice:s.ocv.notice}) + s.ocvTx({f:'readPlugins',ke:d.ke}) + } + tx({f:'users_online',users:s.group[d.ke].users}) + s.tx({f:'user_status_change',ke:d.ke,uid:cn.uid,status:1,user:s.group[d.ke].users[d.auth]},'GRP_'+d.ke) + s.sendDiskUsedAmountToClients(d) + s.loadGroupApps(d) + s.sqlQuery('SELECT * FROM API WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,rrr) { + tx({ + f:'init_success', + users:s.group[d.ke].vid, + apis:rrr, + os:{ + platform:s.platform, + cpuCount:s.coreCount, + totalmem:s.totalmem + } + }) + try{ + s.sqlQuery('SELECT * FROM Monitors WHERE ke=?', [d.ke], function(err,r) { + if(r && r[0]){ + r.forEach(function(monitor){ + s.cameraSendSnapshot({mid:monitor.mid,ke:monitor.ke,mon:monitor}) + }) + } + }) + }catch(err){ + console.log(err) + } + }) + } + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + s.sqlQuery('SELECT * FROM API WHERE ke=? AND code=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + r=r[0] + r.details=JSON.parse(r.details) + if(r.details.auth_socket==='1'){ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND uid=?',[r.ke,r.uid],function(err,r) { + if(r&&r[0]){ + d.success(r) + }else{ + d.failed() + } + }) + }else{ + d.failed() + } + }else{ + d.failed() + } + }) + } + }) + return; + } + if((d.id||d.uid||d.mid)&&cn.ke){ + try{ + switch(d.f){ + case'ocv_in': + s.ocvTx(d.data) + break; + case'monitorOrder': + if(d.monitorOrder&&d.monitorOrder instanceof Object){ + s.sqlQuery('SELECT details FROM Users WHERE uid=? AND ke=?',[cn.uid,cn.ke],function(err,r){ + if(r&&r[0]){ + r=JSON.parse(r[0].details); + r.monitorOrder=d.monitorOrder; + s.sqlQuery('UPDATE Users SET details=? WHERE uid=? AND ke=?',[JSON.stringify(r),cn.uid,cn.ke]) + } + }) + } + break; + case'update': + if(!config.updateKey){ + tx({error:lang.updateKeyText1}); + return; + } + if(d.key===config.updateKey){ + exec('chmod +x '+s.mainDirectory+'/UPDATE.sh&&'+s.mainDirectory+'/UPDATE.sh',{detached: true}) + }else{ + tx({error:lang.updateKeyText2}); + } + break; + case'cron': + if(s.group[cn.ke]&&s.group[cn.ke].users[cn.auth].details&&!s.group[cn.ke].users[cn.auth].details.sub){ + s.tx({f:d.ff},s.cron.id) + } + break; + case'api': + switch(d.ff){ + case'delete': + d.set=[],d.ar=[]; + d.form.ke=cn.ke;d.form.uid=cn.uid;delete(d.form.ip); + if(!d.form.code){tx({f:'form_incomplete',form:'APIs',uid:cn.uid});return} + d.for=Object.keys(d.form); + d.for.forEach(function(v){ + d.set.push(v+'=?'),d.ar.push(d.form[v]); + }); + s.sqlQuery('DELETE FROM API WHERE '+d.set.join(' AND '),d.ar,function(err,r){ + if(!err){ + tx({f:'api_key_deleted',form:d.form,uid:cn.uid}); + delete(s.api[d.form.code]); + }else{ + s.systemLog('API Delete Error : '+e.ke+' : '+' : '+e.mid,err) + } + }) + break; + case'add': + d.set=[],d.qu=[],d.ar=[]; + d.form.ke=cn.ke,d.form.uid=cn.uid,d.form.code=s.gid(30); + d.for=Object.keys(d.form); + d.for.forEach(function(v){ + d.set.push(v),d.qu.push('?'),d.ar.push(d.form[v]); + }); + s.sqlQuery('INSERT INTO API ('+d.set.join(',')+') VALUES ('+d.qu.join(',')+')',d.ar,function(err,r){ + d.form.time=s.formattedTime(new Date,'YYYY-DD-MM HH:mm:ss'); + if(!err){tx({f:'api_key_added',form:d.form,uid:cn.uid});}else{s.systemLog(err)} + }); + break; + } + break; + case'settings': + switch(d.ff){ + case'filters': + switch(d.fff){ + case'save':case'delete': + s.sqlQuery('SELECT details FROM Users WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,r){ + if(r&&r[0]){ + r=r[0]; + d.d=JSON.parse(r.details); + if(d.form.id===''){d.form.id=s.gid(5)} + if(!d.d.filters)d.d.filters={}; + //save/modify or delete + if(d.fff==='save'){ + d.d.filters[d.form.id]=d.form; + }else{ + delete(d.d.filters[d.form.id]); + } + s.sqlQuery('UPDATE Users SET details=? WHERE ke=? AND uid=?',[JSON.stringify(d.d),d.ke,d.uid],function(err,r){ + tx({f:'filters_change',uid:d.uid,ke:d.ke,filters:d.d.filters}); + }); + } + }) + break; + } + break; + case'edit': + s.sqlQuery('SELECT details FROM Users WHERE ke=? AND uid=?',[d.ke,d.uid],function(err,r){ + if(r&&r[0]){ + r=r[0]; + d.d=JSON.parse(r.details); + if(!d.d.sub || d.d.user_change === "1"){ + if(d.d.get_server_log==='1'){ + cn.join('GRPLOG_'+d.ke) + }else{ + cn.leave('GRPLOG_'+d.ke) + } + ///unchangeable from client side, so reset them in case they did. + d.form.details=JSON.parse(d.form.details) + s.beforeAccountSaveExtensions.forEach(function(extender){ + extender(d) + }) + //admin permissions + d.form.details.permissions=d.d.permissions + d.form.details.edit_size=d.d.edit_size + d.form.details.edit_days=d.d.edit_days + d.form.details.use_admin=d.d.use_admin + d.form.details.use_ldap=d.d.use_ldap + //check + if(d.d.edit_days=="0"){ + d.form.details.days=d.d.days; + } + if(d.d.edit_size=="0"){ + d.form.details.size=d.d.size; + } + if(d.d.sub){ + d.form.details.sub=d.d.sub; + if(d.d.monitors){d.form.details.monitors=d.d.monitors;} + if(d.d.allmonitors){d.form.details.allmonitors=d.d.allmonitors;} + if(d.d.monitor_create){d.form.details.monitor_create=d.d.monitor_create;} + if(d.d.video_delete){d.form.details.video_delete=d.d.video_delete;} + if(d.d.video_view){d.form.details.video_view=d.d.video_view;} + if(d.d.monitor_edit){d.form.details.monitor_edit=d.d.monitor_edit;} + if(d.d.size){d.form.details.size=d.d.size;} + if(d.d.days){d.form.details.days=d.d.days;} + delete(d.form.details.mon_groups) + } + var newSize = d.form.details.size || 10000 + d.form.details=JSON.stringify(d.form.details) + /// + d.set=[],d.ar=[]; + if(d.form.pass&&d.form.pass!==''){d.form.pass=s.createHash(d.form.pass);}else{delete(d.form.pass)}; + delete(d.form.password_again); + d.for=Object.keys(d.form); + d.for.forEach(function(v){ + d.set.push(v+'=?'),d.ar.push(d.form[v]); + }); + d.ar.push(d.ke),d.ar.push(d.uid); + s.sqlQuery('UPDATE Users SET '+d.set.join(',')+' WHERE ke=? AND uid=?',d.ar,function(err,r){ + if(!d.d.sub){ + var user = Object.assign(d.form,{ke : d.ke}) + var userDetails = JSON.parse(d.form.details) + s.group[d.ke].sizeLimit = parseFloat(newSize) + s.onAccountSaveExtensions.forEach(function(extender){ + extender(s.group[d.ke],userDetails) + }) + s.unloadGroupAppExtensions.forEach(function(extender){ + extender(user) + }) + s.loadGroupApps(d) + } + tx({f:'user_settings_change',uid:d.uid,ke:d.ke,form:d.form}); + }); + } + } + }) + break; + } + break; + case'monitor': + switch(d.ff){ + case'get': + switch(d.fff){ + case'videos&events': + if(!d.eventLimit){ + d.eventLimit = 500 + }else{ + d.eventLimit = parseInt(d.eventLimit); + } + if(!d.eventStartDate&&d.startDate){ + d.eventStartDate = s.stringToSqlTime(d.startDate) + } + if(!d.eventEndDate&&d.endDate){ + d.eventEndDate = s.stringToSqlTime(d.endDate) + } + var monitorQuery = '' + var monitorValues = [] + var permissions = s.group[d.ke].users[cn.auth].details; + if(!d.mid){ + if(permissions.sub&&permissions.monitors&&permissions.allmonitors!=='1'){ + try{permissions.monitors=JSON.parse(permissions.monitors);}catch(er){} + var or = []; + permissions.monitors.forEach(function(v,n){ + or.push('mid=?'); + monitorValues.push(v) + }) + monitorQuery += ' AND ('+or.join(' OR ')+')' + } + }else if(!permissions.sub||permissions.allmonitors!=='0'||permissions.monitors.indexOf(d.mid)>-1){ + monitorQuery += ' and mid=?'; + monitorValues.push(d.mid) + } + var getEvents = function(callback){ + var eventQuery = 'SELECT * FROM Events WHERE ke=?'; + var eventQueryValues = [cn.ke]; + if(d.eventStartDate&&d.eventStartDate!==''){ + if(d.eventEndDate&&d.eventEndDate!==''){ + eventQuery+=' AND `time` >= ? AND `time` <= ?'; + eventQueryValues.push(d.eventStartDate) + eventQueryValues.push(d.eventEndDate) + }else{ + eventQuery+=' AND `time` >= ?'; + eventQueryValues.push(d.eventStartDate) + } + } + if(monitorValues.length>0){ + eventQuery += monitorQuery; + eventQueryValues = eventQueryValues.concat(monitorValues); + } + eventQuery+=' ORDER BY `time` DESC LIMIT '+d.eventLimit+''; + s.sqlQuery(eventQuery,eventQueryValues,function(err,r){ + if(err){ + console.log(eventQuery) + console.error('LINE 2428',err) + setTimeout(function(){ + getEvents(callback) + },2000) + }else{ + if(!r){r=[]} + r.forEach(function(v,n){ + r[n].details=JSON.parse(v.details); + }) + callback(r) + } + }) + } + if(!d.videoLimit&&d.limit){ + d.videoLimit=d.limit + eventQuery.push() + } + if(!d.videoStartDate&&d.startDate){ + d.videoStartDate = s.stringToSqlTime(d.startDate) + } + if(!d.videoEndDate&&d.endDate){ + d.videoEndDate = s.stringToSqlTime(d.endDate) + } + var getVideos = function(callback){ + var videoQuery='SELECT * FROM Videos WHERE ke=?'; + var videoQueryValues=[cn.ke]; + if(d.videoStartDate||d.videoEndDate){ + if(!d.videoStartDateOperator||d.videoStartDateOperator==''){ + d.videoStartDateOperator='>=' + } + if(!d.videoEndDateOperator||d.videoEndDateOperator==''){ + d.videoEndDateOperator='<=' + } + switch(true){ + case(d.videoStartDate&&d.videoStartDate!==''&&d.videoEndDate&&d.videoEndDate!==''): + videoQuery+=' AND `time` '+d.videoStartDateOperator+' ? AND `end` '+d.videoEndDateOperator+' ?'; + videoQueryValues.push(d.videoStartDate) + videoQueryValues.push(d.videoEndDate) + break; + case(d.videoStartDate&&d.videoStartDate!==''): + videoQuery+=' AND `time` '+d.videoStartDateOperator+' ?'; + videoQueryValues.push(d.videoStartDate) + break; + case(d.videoEndDate&&d.videoEndDate!==''): + videoQuery+=' AND `end` '+d.videoEndDateOperator+' ?'; + videoQueryValues.push(d.videoEndDate) + break; + } + } + if(monitorValues.length>0){ + videoQuery += monitorQuery; + videoQueryValues = videoQueryValues.concat(monitorValues); + } + videoQuery+=' ORDER BY `time` DESC'; + if(!d.videoLimit||d.videoLimit==''){ + d.videoLimit='100' + } + if(d.videoLimit!=='0'){ + videoQuery+=' LIMIT '+d.videoLimit + } + s.sqlQuery(videoQuery,videoQueryValues,function(err,r){ + if(err){ + console.log(videoQuery) + console.error('LINE 2416',err) + setTimeout(function(){ + getVideos(callback) + },2000) + }else{ + s.buildVideoLinks(r,{ + auth : cn.auth + }) + callback({total:r.length,limit:d.videoLimit,videos:r}) + } + }) + } + getVideos(function(videos){ + getEvents(function(events){ + tx({ + f:'drawPowerVideoMainTimeLine', + videos:videos, + events:events + }) + }) + }) + break; + } + break; + case'control': + s.cameraControl(d,function(resp){ + tx({f:'control',response:resp}) + }) + break; + case'jpeg_off': + delete(cn.jpeg_on); + if(cn.monitorsCurrentlyWatching){ + Object.keys(cn.monitorsCurrentlyWatching).forEach(function(n,v){ + v=cn.monitorsCurrentlyWatching[n]; + cn.join('MON_STREAM_'+n); + }); + } + tx({f:'mode_jpeg_off'}) + break; + case'jpeg_on': + cn.jpeg_on=true; + if(cn.monitorsCurrentlyWatching){ + Object.keys(cn.monitorsCurrentlyWatching).forEach(function(n,v){ + v=cn.monitorsCurrentlyWatching[n]; + cn.leave('MON_STREAM_'+n); + }) + } + tx({f:'mode_jpeg_on'}) + break; + case'watch_on': + if(!d.ke){d.ke=cn.ke} + s.initiateMonitorObject({mid:d.id,ke:d.ke}); + if(!s.group[d.ke]||!s.group[d.ke].mon[d.id]||s.group[d.ke].mon[d.id].isStarted === false){return false} + cn.join('MON_'+d.ke+d.id); + cn.join('DETECTOR_'+d.ke+d.id); + if(cn.jpeg_on !== true){ + cn.join('MON_STREAM_'+d.ke+d.id); + } + tx({f:'monitor_watch_on',id:d.id,ke:d.ke}) + s.camera('watch_on',d,cn) + break; + case'watch_off': + if(!d.ke){d.ke=cn.ke;}; + cn.leave('MON_'+d.ke+d.id); + s.camera('watch_off',d,cn); + tx({f:'monitor_watch_off',ke:d.ke,id:d.id,cnid:cn.id}) + break; + case'start':case'stop': + s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[cn.ke,d.id],function(err,r) { + if(r && r[0]){ + r = r[0] + s.camera(d.ff,{type:r.type,url:s.buildMonitorUrl(r),id:d.id,mode:d.ff,ke:cn.ke}); + } + }) + break; + } + break; + // case'video': + // switch(d.ff){ + // case'fix': + // s.video('fix',d) + // break; + // } + // break; + case'ffprobe': + if(s.group[cn.ke].users[cn.auth]){ + switch(d.ff){ + case'stop': + exec('kill -9 '+s.group[cn.ke].users[cn.auth].ffprobe.pid,{detatched: true}) + break; + default: + if(s.group[cn.ke].users[cn.auth].ffprobe){ + return + } + s.group[cn.ke].users[cn.auth].ffprobe=1; + tx({f:'ffprobe_start'}) + exec('ffprobe '+('-v quiet -print_format json -show_format -show_streams '+d.query),function(err,data){ + tx({f:'ffprobe_data',data:data.toString('utf8')}) + delete(s.group[cn.ke].users[cn.auth].ffprobe) + tx({f:'ffprobe_stop'}) + }) + //auto kill in 30 seconds + setTimeout(function(){ + exec('kill -9 '+d.pid,{detached: true}) + },30000) + break; + } + } + break; + case'onvif': + d.ip=d.ip.replace(/ /g,''); + d.port=d.port.replace(/ /g,''); + if(d.ip===''){ + var interfaces = os.networkInterfaces(); + var addresses = []; + for (var k in interfaces) { + for (var k2 in interfaces[k]) { + var address = interfaces[k][k2]; + if (address.family === 'IPv4' && !address.internal) { + addresses.push(address.address); + } + } + } + d.arr=[] + addresses.forEach(function(v){ + if(v.indexOf('0.0.0')>-1){return false} + v=v.split('.'); + delete(v[3]); + v=v.join('.'); + d.arr.push(v+'1-'+v+'254') + }) + d.ip=d.arr.join(',') + } + if(d.port===''){ + d.port='80,8080,8000,7575,8081,554' + } + d.ip.split(',').forEach(function(v){ + if(v.indexOf('-')>-1){ + v=v.split('-'); + d.IP_RANGE_START = v[0], + d.IP_RANGE_END = v[1]; + }else{ + d.IP_RANGE_START = v; + d.IP_RANGE_END = v; + } + if(!d.IP_LIST){ + d.IP_LIST = s.ipRange(d.IP_RANGE_START,d.IP_RANGE_END); + }else{ + d.IP_LIST=d.IP_LIST.concat(s.ipRange(d.IP_RANGE_START,d.IP_RANGE_END)) + } + //check port + if(d.port.indexOf('-')>-1){ + d.port=d.port.split('-'); + d.PORT_RANGE_START = d.port[0]; + d.PORT_RANGE_END = d.port[1]; + d.PORT_LIST = s.portRange(d.PORT_RANGE_START,d.PORT_RANGE_END); + }else{ + d.PORT_LIST=d.port.split(',') + } + //check user name and pass + d.USERNAME=''; + if(d.user){ + d.USERNAME = d.user + } + d.PASSWORD=''; + if(d.pass){ + d.PASSWORD = d.pass + } + }) + d.cams=[] + d.IP_LIST.forEach(function(ip_entry,n) { + d.PORT_LIST.forEach(function(port_entry,nn) { + var device = new onvif.OnvifDevice({ + xaddr : 'http://' + ip_entry + ':' + port_entry + '/onvif/device_service', + user : d.USERNAME, + pass : d.PASSWORD + }) + device.init().then((info) => { + var data = { + f : 'onvif', + ip : ip_entry, + port : port_entry, + info : info + } + device.services.device.getSystemDateAndTime().then((date) => { + data.date = date + device.services.media.getStreamUri({ + ProfileToken : device.current_profile.token, + Protocol : 'RTSP' + }).then((stream) => { + data.uri = stream.data.GetStreamUriResponse.MediaUri.Uri + tx(data) + }).catch((error) => { + // console.log(error) + }); + }).catch((error) => { + // console.log(error) + }); + }).catch(function(error){ + // console.log(error) + }) + }); + }); + // tx({f:'onvif_end'}) + break; + } + }catch(er){ + s.systemLog('ERROR CATCH 1',er) + } + }else{ + tx({ok:false,msg:lang.NotAuthorizedText1}); + } + }); + // super page socket functions + cn.on('super',function(d){ + if(!cn.init&&d.f=='init'){ + d.ok=s.superAuth({mail:d.mail,pass:d.pass},function(data){ + cn.mail=d.mail + cn.join('$'); + var tempSessionKey = s.gid(30) + cn.superSessionKey = tempSessionKey + s.superUsersApi[tempSessionKey] = data + if(!data.$user.tokens)data.$user.tokens = {} + data.$user.tokens[tempSessionKey] = {} + cn.ip=cn.request.connection.remoteAddress + s.userLog({ke:'$',mid:'$USER'},{type:lang['Websocket Connected'],msg:{for:lang['Superuser'],id:cn.mail,ip:cn.ip}}) + cn.init='super'; + s.tx({f:'init_success',mail:d.mail,superSessionKey:tempSessionKey},cn.id); + }) + if(d.ok===false){ + cn.disconnect(); + } + }else{ + if(cn.mail&&cn.init=='super'){ + switch(d.f){ + case'logs': + switch(d.ff){ + case'delete': + //config.webPaths.superApiPrefix+':auth/logs/delete' + s.sqlQuery('DELETE FROM Logs WHERE ke=?',[d.ke]) + break; + } + break; + case'system': + switch(d.ff){ + case'update': + //config.webPaths.superApiPrefix+':auth/update' + s.ffmpegKill() + s.systemLog('Shinobi ordered to update',{ + by:cn.mail, + ip:cn.ip + }) + var updateProcess = spawn('sh',(s.mainDirectory+'/UPDATE.sh').split(' '),{detached: true}) + updateProcess.stderr.on('data',function(data){ + s.systemLog('Update Info',data.toString()) + }) + updateProcess.stdout.on('data',function(data){ + s.systemLog('Update Info',data.toString()) + }) + break; + case'restart': + //config.webPaths.superApiPrefix+':auth/restart/:script' + d.check=function(x){return d.target.indexOf(x)>-1} + if(d.check('system')){ + s.systemLog('Shinobi ordered to restart',{by:cn.mail,ip:cn.ip}) + s.ffmpegKill() + exec('pm2 restart '+s.mainDirectory+'/camera.js') + } + if(d.check('cron')){ + s.systemLog('Shinobi CRON ordered to restart',{by:cn.mail,ip:cn.ip}) + exec('pm2 restart '+s.mainDirectory+'/cron.js') + } + if(d.check('logs')){ + s.systemLog('Flush PM2 Logs',{by:cn.mail,ip:cn.ip}) + exec('pm2 flush') + } + break; + case'configure': + s.systemLog('conf.json Modified',{by:cn.mail,ip:cn.ip,old:jsonfile.readFileSync(s.location.config)}) + jsonfile.writeFile(s.location.config,d.data,{spaces: 2},function(){ + s.tx({f:'save_configuration'},cn.id) + }) + break; + } + break; + case'accounts': + switch(d.ff){ + case'saveSuper': + var currentSuperUserList = jsonfile.readFileSync(s.location.super) + var currentSuperUser = {} + var currentSuperUserPosition = -1 + //find this user in current list + currentSuperUserList.forEach(function(user,pos){ + if(user.mail === cn.mail){ + currentSuperUser = user + currentSuperUserPosition = pos + } + }) + var logDetails = { + by : cn.mail, + ip : cn.ip + } + //check if pass and pass_again match, if not remove password + if(d.form.pass !== '' && d.form.pass === d.form.pass_again){ + d.form.pass = s.createHash(d.form.pass) + }else{ + delete(d.form.pass) + } + //delete pass_again from object + delete(d.form.pass_again) + //set new values + currentSuperUser = Object.assign(currentSuperUser,d.form) + //reset email and log change of email + if(d.form.mail !== cn.mail){ + logDetails.newEmail = d.form.mail + logDetails.oldEmail = cn.mail + '' + cn.mail = d.form.mail + } + //log this change + s.systemLog('super.json Modified',logDetails) + //modify or add account in temporary master list + if(currentSuperUserList[currentSuperUserPosition]){ + currentSuperUserList[currentSuperUserPosition] = currentSuperUser + }else{ + currentSuperUserList.push(currentSuperUser) + } + //update master list in system + jsonfile.writeFile(s.location.super,currentSuperUserList,{spaces: 2},function(){ + s.tx({f:'save_preferences'},cn.id) + }) + break; + case'register': + if(d.form.mail!==''&&d.form.pass!==''){ + if(d.form.pass===d.form.password_again){ + s.sqlQuery('SELECT * FROM Users WHERE mail=?',[d.form.mail],function(err,r) { + if(r&&r[0]){ + //found address already exists + d.msg=lang['Email address is in use.']; + s.tx({f:'error',ff:'account_register',msg:d.msg},cn.id) + }else{ + //create new + //user id + d.form.uid=s.gid(); + //check to see if custom key set + if(!d.form.ke||d.form.ke===''){ + d.form.ke=s.gid() + }else{ + d.form.ke = d.form.ke.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '') + } + //write user to db + s.sqlQuery('INSERT INTO Users (ke,uid,mail,pass,details) VALUES (?,?,?,?,?)',[d.form.ke,d.form.uid,d.form.mail,s.createHash(d.form.pass),d.form.details]) + s.tx({f:'add_account',details:d.form.details,ke:d.form.ke,uid:d.form.uid,mail:d.form.mail},'$'); + //init user + s.loadGroup(d.form) + } + }) + }else{ + d.msg=lang["Passwords Don't Match"]; + } + }else{ + d.msg=lang['Fields cannot be empty']; + } + if(d.msg){ + s.tx({f:'error',ff:'account_register',msg:d.msg},cn.id) + } + break; + case'edit': + s.sqlQuery('SELECT * FROM Users WHERE mail=?',[d.account.mail],function(err,r) { + if(r && r[0]){ + r = r[0] + var details = JSON.parse(r.details) + if(d.form.pass&&d.form.pass!==''){ + if(d.form.pass===d.form.password_again){ + d.form.pass=s.createHash(d.form.pass); + }else{ + s.tx({f:'error',ff:'edit_account',msg:lang["Passwords Don't Match"]},cn.id) + return + } + }else{ + delete(d.form.pass); + } + delete(d.form.password_again); + d.keys=Object.keys(d.form); + d.set=[]; + d.values=[]; + d.keys.forEach(function(v,n){ + if(d.set==='ke'||d.set==='password_again'||!d.form[v]){return} + d.set.push(v+'=?') + if(v === 'details'){ + d.form[v] = JSON.stringify(Object.assign(details,JSON.parse(d.form[v]))) + } + d.values.push(d.form[v]) + }) + d.values.push(d.account.mail) + s.sqlQuery('UPDATE Users SET '+d.set.join(',')+' WHERE mail=?',d.values,function(err,r) { + if(err){ + console.log(err) + s.tx({f:'error',ff:'edit_account',msg:lang.AccountEditText1},cn.id) + return + } + s.tx({f:'edit_account',form:d.form,ke:d.account.ke,uid:d.account.uid},'$'); + delete(s.group[d.account.ke].init); + s.loadGroupApps(d.account) + }) + } + }) + break; + case'delete': + s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[d.account.uid,d.account.ke,d.account.mail]) + s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[d.account.uid,d.account.ke]) + s.tx({f:'delete_account',ke:d.account.ke,uid:d.account.uid,mail:d.account.mail},'$'); + break; + } + break; + } + } + } + }) + // admin page socket functions + cn.on('a',function(d){ + if(!cn.init&&d.f=='init'){ + s.sqlQuery('SELECT * FROM Users WHERE auth=? AND uid=?',[d.auth,d.uid],function(err,r){ + if(r&&r[0]){ + r=r[0]; + if(!s.group[d.ke]){s.group[d.ke]={users:{}}} + if(!s.group[d.ke].users[d.auth]){s.group[d.ke].users[d.auth]={cnid:cn.id,uid:d.uid,ke:d.ke,auth:d.auth}} + try{s.group[d.ke].users[d.auth].details=JSON.parse(r.details)}catch(er){} + cn.join('ADM_'+d.ke); + cn.ke=d.ke; + cn.uid=d.uid; + cn.auth=d.auth; + cn.init='admin'; + }else{ + cn.disconnect(); + } + }) + }else{ + s.auth({auth:d.auth,ke:d.ke,id:d.id,ip:cn.request.connection.remoteAddress},function(user){ + if(!user.details.sub){ + switch(d.f){ + case'accounts': + switch(d.ff){ + case'edit': + d.keys=Object.keys(d.form); + d.condition=[]; + d.value=[]; + d.keys.forEach(function(v){ + d.condition.push(v+'=?') + d.value.push(d.form[v]) + }) + d.value=d.value.concat([d.ke,d.$uid]) + s.sqlQuery("UPDATE Users SET "+d.condition.join(',')+" WHERE ke=? AND uid=?",d.value) + s.tx({f:'edit_sub_account',ke:d.ke,uid:d.$uid,mail:d.mail,form:d.form},'ADM_'+d.ke); + s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[d.ke,d.$uid],function(err,rows){ + if(rows && rows[0]){ + rows.forEach(function(row){ + delete(s.api[row.code]) + }) + } + }) + break; + case'delete': + s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[d.$uid,d.ke,d.mail]) + s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[d.ke,d.$uid],function(err,rows){ + if(rows && rows[0]){ + rows.forEach(function(row){ + delete(s.api[row.code]) + }) + s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[d.$uid,d.ke]) + } + }) + s.tx({f:'delete_sub_account',ke:d.ke,uid:d.$uid,mail:d.mail},'ADM_'+d.ke); + break; + } + break; + } + } + }) + } + }) + //functions for webcam recorder + cn.on('r',function(d){ + if(!cn.ke&&d.f==='init'){ + s.sqlQuery('SELECT ke,uid,auth,mail,details FROM Users WHERE ke=? AND auth=? AND uid=?',[d.ke,d.auth,d.uid],function(err,r) { + if(r&&r[0]){ + r=r[0] + cn.ke=d.ke,cn.uid=d.uid,cn.auth=d.auth; + if(!s.group[d.ke])s.group[d.ke]={}; + if(!s.group[d.ke].users)s.group[d.ke].users={}; + if(!s.group[d.ke].dashcamUsers)s.group[d.ke].dashcamUsers={}; + s.group[d.ke].users[d.auth]={ + cnid:cn.id, + ke : d.ke, + uid:r.uid, + mail:r.mail, + details:JSON.parse(r.details), + logged_in_at:s.timeObject(new Date).format(), + login_type:'Streamer' + } + s.group[d.ke].dashcamUsers[d.auth] = s.group[d.ke].users[d.auth] + if(s.group[d.ke].mon){ + Object.keys(s.group[d.ke].mon).forEach(function(monitorId){ + var dataToClient = { + f : 'disable_stream', + mid : monitorId, + ke : d.ke + } + var mon = s.group[d.ke].mon[monitorId] + if(s.group[d.ke].mon_conf[monitorId].type === 'dashcam'){ + if(mon.allowStdinWrite === true){ + dataToClient.f = 'enable_stream' + } + s.tx(dataToClient,cn.id) + } + }) + } + } + }) + }else{ + if(s.group[d.ke] && s.group[d.ke].mon[d.mid]){ + if(s.group[d.ke].mon[d.mid].allowStdinWrite === true){ + switch(d.f){ + case'monitor_chunk': + if(s.group[d.ke].mon[d.mid].isStarted !== true || !s.group[d.ke].mon[d.mid].spawn || !s.group[d.ke].mon[d.mid].spawn.stdin){ + s.tx({error:'Not Started'},cn.id); + return false + }; + s.group[d.ke].mon[d.mid].spawn.stdin.write(new Buffer(d.chunk, "binary")); + break; + case'monitor_frame': + if(s.group[d.ke].mon[d.mid].isStarted !== true){ + s.tx({error:'Not Started'},cn.id); + return false + }; + s.group[d.ke].mon[d.mid].spawn.stdin.write(d.frame); + break; + } + }else{ + s.tx({error:'Cannot Write Yet'},cn.id) + } + }else{ + s.tx({error:'Non Existant Monitor'},cn.id) + } + } + }) + //embed functions + cn.on('e', function (d) { + tx=function(z){if(!z.ke){z.ke=cn.ke;};cn.emit('f',z);} + switch(d.f){ + case'init': + if(!s.group[d.ke]||!s.group[d.ke].mon[d.id]||s.group[d.ke].mon[d.id].isStarted === false){return false} + s.auth({auth:d.auth,ke:d.ke,id:d.id,ip:cn.request.connection.remoteAddress},function(user){ + cn.embedded=1; + cn.ke=d.ke; + if(!cn.mid){cn.mid={}} + cn.mid[d.id]={}; + // if(!s.group[d.ke].embed){s.group[d.ke].embed={}} + // if(!s.group[d.ke].embed[d.mid]){s.group[d.ke].embed[d.mid]={}} + // s.group[d.ke].embed[d.mid][cn.id]={} + + s.camera('watch_on',d,cn,tx) + cn.join('MON_'+d.ke+d.id); + cn.join('MON_STREAM_'+d.ke+d.id); + cn.join('DETECTOR_'+d.ke+d.id); + cn.join('STR_'+d.ke); + if(s.group[d.ke]&&s.group[d.ke].mon[d.id]&&s.group[d.ke].mon[d.id].watch){ + + tx({f:'monitor_watch_on',id:d.id,ke:d.ke},'MON_'+d.ke+d.id) + s.tx({viewers:Object.keys(s.group[d.ke].mon[d.id].watch).length,ke:d.ke,id:d.id},'MON_'+d.ke+d.id) + } + }); + break; + } + }) + //functions for retrieving cron announcements + cn.on('cron',function(d){ + if(d.f==='init'){ + if(config.cron.key){ + if(config.cron.key===d.cronKey){ + s.cron={started:moment(),last_run:moment(),id:cn.id}; + }else{ + cn.disconnect() + } + }else{ + s.cron={started:moment(),last_run:moment(),id:cn.id}; + } + }else{ + if(s.cron&&cn.id===s.cron.id){ + delete(d.cronKey) + switch(d.f){ + case'filters': + s.filterEvents(d.ff,d); + break; + case's.tx': + s.tx(d.data,d.to) + break; + case's.deleteVideo': + s.deleteVideo(d.file) + break; + case'start':case'end': + d.mid='_cron';s.userLog(d,{type:'cron',msg:d.msg}) + break; + default: + s.systemLog('CRON : ',d) + break; + } + }else{ + cn.disconnect() + } + } + }) + cn.on('disconnect', function () { + if(cn.socketVideoStream){ + cn.closeSocketVideoStream() + return + } + if(cn.ke){ + if(cn.monitorsCurrentlyWatching){ + cn.monitor_count=Object.keys(cn.monitorsCurrentlyWatching) + if(cn.monitor_count.length>0){ + cn.monitor_count.forEach(function(v){ + s.camera('watch_off',{id:v,ke:cn.monitorsCurrentlyWatching[v].ke},{id:cn.id,ke:cn.ke,uid:cn.uid}) + }) + } + }else if(!cn.embedded){ + if(s.group[cn.ke].users[cn.auth]){ + if(s.group[cn.ke].users[cn.auth].login_type === 'Dashboard'){ + s.tx({f:'user_status_change',ke:cn.ke,uid:cn.uid,status:0}) + } + s.userLog({ke:cn.ke,mid:'$USER'},{type:lang['Websocket Disconnected'],msg:{mail:s.group[cn.ke].users[cn.auth].mail,id:cn.uid,ip:cn.ip}}) + delete(s.group[cn.ke].users[cn.auth]); + } + if(s.group[cn.ke].dashcamUsers && s.group[cn.ke].dashcamUsers[cn.auth])delete(s.group[cn.ke].dashcamUsers[cn.auth]); + } + } + if(cn.pluginEngine){ + s.connectedPlugins[cn.pluginEngine].plugged=false + s.tx({f:'plugin_engine_unplugged',plug:cn.pluginEngine},'CPU') + delete(s.api[cn.pluginEngine]) + } + if(cn.cron){ + delete(s.cron); + } + if(cn.ocv){ + s.tx({f:'detector_unplugged',plug:s.ocv.plug},'CPU') + delete(s.ocv); + delete(s.api[cn.id]) + } + if(cn.superSessionKey){ + delete(s.superUsersApi[cn.superSessionKey]) + } + }) + }); +} diff --git a/libs/sql.js b/libs/sql.js new file mode 100644 index 0000000..398fbeb --- /dev/null +++ b/libs/sql.js @@ -0,0 +1,88 @@ +module.exports = function(s,config){ + //sql/database connection with knex + s.databaseOptions = { + client: config.databaseType, + connection: config.db, + } + if(s.databaseOptions.client.indexOf('sqlite')>-1){ + s.databaseOptions.client = 'sqlite3'; + s.databaseOptions.useNullAsDefault = true; + } + if(s.databaseOptions.client === 'sqlite3' && s.databaseOptions.connection.filename === undefined){ + s.databaseOptions.connection.filename = s.mainDirectory+"/shinobi.sqlite" + } + s.mergeQueryValues = function(query,values){ + if(!values){values=[]} + var valuesNotFunction = true; + if(typeof values === 'function'){ + var values = []; + valuesNotFunction = false; + } + if(values&&valuesNotFunction){ + var splitQuery = query.split('?') + var newQuery = '' + splitQuery.forEach(function(v,n){ + newQuery += v + var value = values[n] + if(value){ + if(isNaN(value) || value instanceof Date){ + newQuery += "'"+value+"'" + }else{ + newQuery += value + } + } + }) + }else{ + newQuery = query + } + return newQuery + } + s.stringToSqlTime = function(value){ + newValue = new Date(value.replace('T',' ')) + return newValue + } + s.sqlQuery = function(query,values,onMoveOn,hideLog){ + if(!values){values=[]} + if(typeof values === 'function'){ + var onMoveOn = values; + var values = []; + } + if(!onMoveOn){onMoveOn=function(){}} + var mergedQuery = s.mergeQueryValues(query,values) + s.debugLog('s.sqlQuery QUERY',mergedQuery) + return s.databaseEngine + .raw(query,values) + .asCallback(function(err,r){ + if(err && !hideLog){ + console.log('s.sqlQuery QUERY ERRORED',query) + console.log('s.sqlQuery ERROR',err) + } + if(onMoveOn && typeof onMoveOn === 'function'){ + switch(s.databaseOptions.client){ + case'sqlite3': + if(!r)r=[] + break; + default: + if(r)r=r[0] + break; + } + onMoveOn(err,r) + } + }) + } + s.preQueries = function(){ + //add Cloud Videos table, will remove in future + s.sqlQuery('CREATE TABLE IF NOT EXISTS `Cloud Videos` (`mid` varchar(50) NOT NULL,`ke` varchar(50) DEFAULT NULL,`href` text NOT NULL,`size` float DEFAULT NULL,`time` timestamp NULL DEFAULT NULL,`end` timestamp NULL DEFAULT NULL,`status` int(1) DEFAULT \'0\' COMMENT \'0:Complete,1:Read,2:Archive\',`details` text) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;',[],function(err){ + // if(err)console.log(err) + },true) + //create Files table + s.sqlQuery('CREATE TABLE IF NOT EXISTS `Files` (`ke` varchar(50) NOT NULL,`mid` varchar(50) NOT NULL,`name` tinytext NOT NULL,`size` float NOT NULL DEFAULT \'0\',`details` text NOT NULL,`status` int(1) NOT NULL DEFAULT \'0\') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;',[],function(err){ + // if(err)console.log(err) + //add time column to Files table + s.sqlQuery('ALTER TABLE `Files` ADD COLUMN `time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER `status`;',[],function(err){ + // if(err)console.log(err) + },true) + },true) + delete(s.preQueries) + } +} diff --git a/libs/startup.js b/libs/startup.js new file mode 100644 index 0000000..f6d796b --- /dev/null +++ b/libs/startup.js @@ -0,0 +1,165 @@ + +var fs = require('fs'); +var moment = require('moment'); +var crypto = require('crypto'); +var exec = require('child_process').exec; +var execSync = require('child_process').execSync; +module.exports = function(s,config,lang,io){ + console.log('FFmpeg version : '+s.ffmpegVersion) + console.log('Node.js version : '+execSync("node -v")) + s.processReady = function(){ + s.systemLog(lang.startUpText5) + process.send('ready') + } + var loadedAccounts = [] + var loadMonitors = function(callback){ + s.systemLog(lang.startUpText4) + //preliminary monitor start + s.sqlQuery('SELECT * FROM Monitors', function(err,monitors) { + if(err){s.systemLog(err)} + if(monitors && monitors[0]){ + var loadCompleted = 0 + var orphanedVideosForMonitors = {} + var loadMonitor = function(monitor){ + if(!orphanedVideosForMonitors[monitor.ke])orphanedVideosForMonitors[monitor.ke] = {} + if(!orphanedVideosForMonitors[monitor.ke][monitor.mid])orphanedVideosForMonitors[monitor.ke][monitor.mid] = 0 + s.initiateMonitorObject(monitor) + s.orphanedVideoCheck(monitor,2,function(orphanedFilesCount){ + if(orphanedFilesCount){ + orphanedVideosForMonitors[monitor.ke][monitor.mid] += orphanedFilesCount + } + s.group[monitor.ke].mon_conf[monitor.mid] = monitor + var monObj = Object.assign(monitor,{id : monitor.mid}) + s.camera(monitor.mode,monObj) + ++loadCompleted + if(monitors[loadCompleted]){ + loadMonitor(monitors[loadCompleted]) + }else{ + s.systemLog(lang.startUpText6+' : '+s.s(orphanedVideosForMonitors)) + callback() + } + }) + } + loadMonitor(monitors[loadCompleted]) + }else{ + callback() + } + }) + } + var loadDiskUseForUser = function(user,callback){ + s.systemLog(user.mail+' : '+lang.startUpText0) + var userDetails = JSON.parse(user.details) + user.size = 0 + user.limit = userDetails.size + s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND status!=?',[user.ke,0],function(err,videos){ + if(videos && videos[0]){ + videos.forEach(function(video){ + user.size += video.size + }) + } + s.systemLog(user.mail+' : '+lang.startUpText1+' : '+videos.length,user.size) + callback() + }) + } + var loadCloudDiskUseForUser = function(user,callback){ + var userDetails = JSON.parse(user.details) + user.cloudDiskUse = {} + user.size = 0 + user.limit = userDetails.size + s.cloudDisksLoaded.forEach(function(storageType){ + user.cloudDiskUse[storageType] = { + usedSpace : 0, + firstCount : 0 + } + if(s.cloudDiskUseStartupExtensions[storageType])s.cloudDiskUseStartupExtensions[storageType](user,userDetails) + }) + s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE ke=? AND status!=?',[user.ke,0],function(err,videos){ + if(videos && videos[0]){ + videos.forEach(function(video){ + var storageType = JSON.parse(video.details).type + if(!storageType)storageType = 's3' + user.cloudDiskUse[storageType].usedSpace += (video.size /1000000) + ++user.cloudDiskUse[storageType].firstCount + }) + s.cloudDisksLoaded.forEach(function(storageType){ + var firstCount = user.cloudDiskUse[storageType].firstCount + s.systemLog(user.mail+' : '+lang.startUpText1+' : '+firstCount,storageType,user.cloudDiskUse[storageType].usedSpace) + delete(user.cloudDiskUse[storageType].firstCount) + }) + } + s.group[user.ke].cloudDiskUse = user.cloudDiskUse + callback() + }) + } + var loadAdminUsers = function(callback){ + //get current disk used for each isolated account (admin user) on startup + s.sqlQuery('SELECT * FROM Users WHERE details NOT LIKE ?',['%"sub"%'],function(err,users){ + if(users && users[0]){ + var loadLocalDiskUse = function(callback){ + var count = users.length + var countFinished = 0 + users.forEach(function(user){ + loadedAccounts.push(user.ke) + loadDiskUseForUser(user,function(){ + s.loadGroup(user) + s.loadGroupApps(user) + ++countFinished + if(countFinished === count){ + callback() + } + }) + }) + } + var loadCloudDiskUse = function(callback){ + var count = users.length + var countFinished = 0 + users.forEach(function(user){ + loadCloudDiskUseForUser(user,function(){ + ++countFinished + if(countFinished === count){ + callback() + } + }) + }) + } + loadLocalDiskUse(function(){ + loadCloudDiskUse(function(){ + callback() + }) + }) + }else{ + s.processReady() + } + }) + } + //check disk space every 20 minutes + if(config.autoDropCache===true){ + setInterval(function(){ + exec('echo 3 > /proc/sys/vm/drop_caches',{detached: true}) + },60000*20) + } + //master node - startup functions + setInterval(function(){ + s.cpuUsage(function(cpu){ + s.ramUsage(function(ram){ + s.tx({f:'os',cpu:cpu,ram:ram},'CPU'); + }) + }) + },10000) + //run prerequsite queries, load users and monitors + if(config.childNodes.mode !== 'child'){ + //sql/database connection with knex + s.databaseEngine = require('knex')(s.databaseOptions) + //run prerequsite queries + s.preQueries() + setTimeout(function(){ + //load administrators (groups) + loadAdminUsers(function(){ + //load monitors (for groups) + loadMonitors(function(){ + s.processReady() + }) + }) + },1500) + } +} diff --git a/libs/user.js b/libs/user.js new file mode 100644 index 0000000..1ed2e34 --- /dev/null +++ b/libs/user.js @@ -0,0 +1,235 @@ +var fs = require('fs'); +var events = require('events'); +var spawn = require('child_process').spawn; +var exec = require('child_process').exec; +module.exports = function(s,config){ + s.purgeDiskForGroup = function(e){ + if(config.cron.deleteOverMax === true){ + s.group[e.ke].sizePurgeQueue.push(1) + if(s.group[e.ke].sizePurging !== true){ + s.group[e.ke].sizePurging = true + var finish = function(){ + //remove value just used from queue + s.group[e.ke].sizePurgeQueue.shift() + //do next one + if(s.group[e.ke].sizePurgeQueue.length > 0){ + checkQueue() + }else{ + s.group[e.ke].sizePurging=false + s.sendDiskUsedAmountToClients(e) + } + } + var checkQueue = function(){ + //get first in queue + var currentPurge = s.group[e.ke].sizePurgeQueue[0] + var deleteVideos = function(){ + //run purge command + if(s.group[e.ke].usedSpace > (s.group[e.ke].sizeLimit*config.cron.deleteOverMaxOffset)){ + s.sqlQuery('SELECT * FROM Videos WHERE status != 0 AND details NOT LIKE \'%"archived":"1"%\' AND ke=? ORDER BY `time` ASC LIMIT 3',[e.ke],function(err,videos){ + var videosToDelete = [] + var queryValues = [e.ke] + var completedCheck = 0 + if(videos){ + videos.forEach(function(video){ + video.dir = s.getVideoDirectory(video) + s.formattedTime(video.time) + '.' + video.ext + videosToDelete.push('(mid=? AND `time`=?)') + queryValues.push(video.mid) + queryValues.push(video.time) + fs.chmod(video.dir,0o777,function(err){ + fs.unlink(video.dir,function(err){ + ++completedCheck + if(err){ + fs.stat(video.dir,function(err){ + if(!err){ + s.file('delete',video.dir) + } + }) + } + if(videosToDelete.length === completedCheck){ + videosToDelete = videosToDelete.join(' OR ') + s.sqlQuery('DELETE FROM Videos WHERE ke =? AND ('+videosToDelete+')',queryValues,function(){ + deleteVideos() + }) + } + }) + }) + s.setDiskUsedForGroup(e,-(video.size/1000000)) + s.tx({ + f: 'video_delete', + ff: 'over_max', + filename: s.formattedTime(video.time)+'.'+video.ext, + mid: video.mid, + ke: video.ke, + time: video.time, + end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') + },'GRP_'+e.ke) + }) + }else{ + console.log(err) + } + if(videosToDelete.length === 0){ + finish() + } + }) + }else{ + finish() + } + } + deleteVideos() + } + checkQueue() + } + }else{ + s.sendDiskUsedAmountToClients(e) + } + } + s.setDiskUsedForGroup = function(e,bytes){ + //`bytes` will be used as the value to add or substract + if(s.group[e.ke] && s.group[e.ke].diskUsedEmitter){ + s.group[e.ke].diskUsedEmitter.emit('set',bytes) + } + } + s.purgeCloudDiskForGroup = function(e,storageType){ + if(s.group[e.ke].diskUsedEmitter){ + s.group[e.ke].diskUsedEmitter.emit('purgeCloud',storageType) + } + } + s.setCloudDiskUsedForGroup = function(e,usage){ + //`bytes` will be used as the value to add or substract + if(s.group[e.ke].diskUsedEmitter){ + s.group[e.ke].diskUsedEmitter.emit('setCloud',usage) + } + } + s.sendDiskUsedAmountToClients = function(e){ + //send the amount used disk space to connected users + if(s.group[e.ke]&&s.group[e.ke].init){ + s.tx({f:'diskUsed',size:s.group[e.ke].usedSpace,limit:s.group[e.ke].sizeLimit},'GRP_'+e.ke); + } + } + //user log + s.userLog = function(e,x){ + if(e.id && !e.mid)e.mid = e.id + if(!x||!e.mid){return} + if((e.details&&e.details.sqllog==='1')||e.mid.indexOf('$')>-1){ + s.sqlQuery('INSERT INTO Logs (ke,mid,info) VALUES (?,?,?)',[e.ke,e.mid,s.s(x)]); + } + s.tx({f:'log',ke:e.ke,mid:e.mid,log:x,time:s.timeObject()},'GRPLOG_'+e.ke); + } + s.loadGroup = function(e){ + if(!s.group[e.ke]){ + s.group[e.ke]={} + } + if(!s.group[e.ke].init){ + s.group[e.ke].init={} + } + if(!s.group[e.ke].fileBin){s.group[e.ke].fileBin={}}; + if(!s.group[e.ke].users){s.group[e.ke].users={}} + if(!s.group[e.ke].dashcamUsers){s.group[e.ke].dashcamUsers={}} + if(!s.group[e.ke].sizePurgeQueue){s.group[e.ke].sizePurgeQueue=[]} + if(!e.limit||e.limit===''){e.limit=10000}else{e.limit=parseFloat(e.limit)} + //save global space limit for group key (mb) + s.group[e.ke].sizeLimit=e.limit; + //save global used space as megabyte value + s.group[e.ke].usedSpace=e.size/1000000; + //emit the changes to connected users + s.sendDiskUsedAmountToClients(e) + } + s.loadGroupApps = function(e){ + // e = user + if(!s.group[e.ke].init){ + s.group[e.ke].init={}; + } + s.sqlQuery('SELECT * FROM Users WHERE ke=? AND details NOT LIKE ?',[e.ke,'%"sub"%'],function(ar,r){ + if(r&&r[0]){ + r=r[0]; + ar=JSON.parse(r.details); + //load extenders + s.loadGroupAppExtensions.forEach(function(extender){ + extender(r) + }) + //disk Used Emitter + if(!s.group[e.ke].diskUsedEmitter){ + s.group[e.ke].diskUsedEmitter = new events.EventEmitter() + s.group[e.ke].diskUsedEmitter.on('setCloud',function(currentChange){ + var amount = currentChange.amount + var storageType = currentChange.storageType + var cloudDisk = s.group[e.ke].cloudDiskUse[storageType] + //validate current values + if(!cloudDisk.usedSpace){ + cloudDisk.usedSpace = 0 + }else{ + cloudDisk.usedSpace = parseFloat(cloudDisk.usedSpace) + } + if(cloudDisk.usedSpace < 0 || isNaN(cloudDisk.usedSpace)){ + cloudDisk.usedSpace = 0 + } + //change global size value + cloudDisk.usedSpace = cloudDisk.usedSpace + amount + }) + s.group[e.ke].diskUsedEmitter.on('purgeCloud',function(storageType){ + if(config.cron.deleteOverMax === true){ + //set queue processor + var finish=function(){ + // s.sendDiskUsedAmountToClients(e) + } + var deleteVideos = function(){ + //run purge command + var cloudDisk = s.group[e.ke].cloudDiskUse[storageType] + if(cloudDisk.sizeLimitCheck && cloudDisk.usedSpace > (cloudDisk.sizeLimit*config.cron.deleteOverMaxOffset)){ + s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE status != 0 AND ke=? AND details LIKE \'%"type":"'+storageType+'"%\' ORDER BY `time` ASC LIMIT 2',[e.ke],function(err,videos){ + var videosToDelete = [] + var queryValues = [e.ke] + if(!videos)return console.log(err) + videos.forEach(function(video){ + video.dir = s.getVideoDirectory(video) + s.formattedTime(video.time) + '.' + video.ext + videosToDelete.push('(mid=? AND `time`=?)') + queryValues.push(video.mid) + queryValues.push(video.time) + s.setCloudDiskUsedForGroup(e,{ + amount : -(video.size/1000000), + storageType : storageType + }) + s.deleteVideoFromCloudExtensionsRunner(e,storageType,video) + }) + if(videosToDelete.length > 0){ + videosToDelete = videosToDelete.join(' OR ') + s.sqlQuery('DELETE FROM `Cloud Videos` WHERE ke =? AND ('+videosToDelete+')',queryValues,function(){ + deleteVideos() + }) + }else{ + finish() + } + }) + }else{ + finish() + } + } + deleteVideos() + }else{ + // s.sendDiskUsedAmountToClients(e) + } + }) + //s.setDiskUsedForGroup + s.group[e.ke].diskUsedEmitter.on('set',function(currentChange){ + //validate current values + if(!s.group[e.ke].usedSpace){ + s.group[e.ke].usedSpace=0 + }else{ + s.group[e.ke].usedSpace=parseFloat(s.group[e.ke].usedSpace) + } + if(s.group[e.ke].usedSpace<0||isNaN(s.group[e.ke].usedSpace)){ + s.group[e.ke].usedSpace=0 + } + //change global size value + s.group[e.ke].usedSpace += currentChange + //remove value just used from queue + s.sendDiskUsedAmountToClients(e) + }) + } + Object.keys(ar).forEach(function(v){ + s.group[e.ke].init[v]=ar[v] + }) + } + }); + } +} diff --git a/libs/videos.js b/libs/videos.js new file mode 100644 index 0000000..614994f --- /dev/null +++ b/libs/videos.js @@ -0,0 +1,329 @@ +var fs = require('fs'); +var exec = require('child_process').exec; +var spawn = require('child_process').spawn; +module.exports = function(s,config,lang){ + /** + * Gets the video directory of the supplied video or monitor database row. + * @constructor + * @param {object} e - Monitor object or Video object. Object is a database row. + */ + s.getVideoDirectory = function(e){ + if(e.mid&&!e.id){e.id=e.mid}; + s.checkDetails(e) + if(e.details&&e.details.dir&&e.details.dir!==''){ + return s.checkCorrectPathEnding(e.details.dir)+e.ke+'/'+e.id+'/' + }else{ + return s.dir.videos+e.ke+'/'+e.id+'/'; + } + } + /** + * Creates available API based URLs for streaming + * @constructor + * @param {object} videos - Array of video objects + * @param {object} options - Contains middle parameter of URL and auth key + * @param [options.auth] {string} - API Key + * @param [options.videoParam] {string} - currently only `videos` and `cloudVideos` available. + */ + s.buildVideoLinks = function(videos,options){ + videos.forEach(function(v){ + var details = JSON.parse(v.details) + var queryString = [] + if(details.isUTC === true){ + queryString.push('isUTC=true') + }else{ + v.time = s.utcToLocal(v.time) + v.end = s.utcToLocal(v.end) + } + if(queryString.length > 0){ + queryString = '?'+queryString.join('&') + }else{ + queryString = '' + } + if(!v.ext && v.href){ + v.ext = v.href.split('.') + v.ext = v.ext[v.ext.length - 1] + } + v.filename = s.formattedTime(v.time)+'.'+v.ext; + if(!options.videoParam)options.videoParam = 'videos' + var href = '/'+options.auth+'/'+options.videoParam+'/'+v.ke+'/'+v.mid+'/'+v.filename; + v.actionUrl = href + v.links = { + deleteVideo : href+'/delete' + queryString, + changeToUnread : href+'/status/1' + queryString, + changeToRead : href+'/status/2' + queryString + } + if(!v.href || options.hideRemote === true)v.href = href + queryString + v.details = details + }) + } + //extender for "s.insertCompletedVideo" + s.insertCompletedVideoExtensions = [] + s.insertCompletedVideoExtender = function(callback){ + s.insertCompletedVideoExtensions.push(callback) + } + s.insertDatabaseRow = function(e,k,callback){ + s.checkDetails(e) + //save database row + k.details = {} + if(e.details&&e.details.dir&&e.details.dir!==''){ + k.details.dir = e.details.dir + } + if(config.useUTC === true)k.details.isUTC = config.useUTC; + var save = [ + e.mid, + e.ke, + k.startTime, + e.ext, + 1, + s.s(k.details), + k.filesize, + k.endTime, + ] + s.sqlQuery('INSERT INTO Videos (mid,ke,time,ext,status,details,size,end) VALUES (?,?,?,?,?,?,?,?)',save,function(err){ + if(callback)callback(err) + fs.chmod(k.dir+k.file,0o777,function(err){ + + }) + }) + } + //on video completion + s.insertCompletedVideo = function(e,k,callback){ + //e = monitor object + //k = video insertion object + s.checkDetails(e) + if(!k)k={}; + e.dir = s.getVideoDirectory(e) + k.dir = e.dir.toString() + if(s.group[e.ke].mon[e.id].childNode){ + s.cx({f:'insertCompleted',d:s.group[e.ke].mon_conf[e.id],k:k},s.group[e.ke].mon[e.id].childNodeId); + }else{ + //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){ + 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.stat = fs.statSync(k.dir+k.file) + k.filesize = k.stat.size + k.filesizeMB = parseFloat((k.filesize/1000000).toFixed(2)) + + k.startTime = new Date(s.nameToTime(k.file)) + k.endTime = new Date(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 + 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({ + f:'created_file_chunk', + mid:e.mid, + ke:e.ke, + chunk:data, + filename:k.filename, + d:s.cleanMonitorObject(e), + filesize:e.filesize, + time:s.timeObject(k.startTime).format(), + end:s.timeObject(k.endTime).format() + }) + }) + .on('close',function(){ + clearTimeout(s.group[e.ke].mon[e.id].recordingChecker) + clearTimeout(s.group[e.ke].mon[e.id].streamChecker) + s.cx({ + f:'created_file', + mid:e.id, + ke:e.ke, + filename:k.filename, + d:s.cleanMonitorObject(e), + filesize:k.filesize, + time:s.timeObject(k.startTime).format(), + end:s.timeObject(k.endTime).format() + }) + }) + }else{ + var href = '/videos/'+e.ke+'/'+e.mid+'/'+k.filename + if(config.useUTC === true)href += '?isUTC=true'; + 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 + },'GRP_'+e.ke,'video_view') + //purge over max + s.purgeDiskForGroup(e) + //send new diskUsage values + s.setDiskUsedForGroup(e,k.filesizeMB) + s.insertDatabaseRow(e,k,callback) + s.insertCompletedVideoExtensions.forEach(function(extender){ + extender(e,k) + }) + } + } + } + } + s.deleteVideo = function(e){ + //e = video object + s.checkDetails(e) + e.dir = s.getVideoDirectory(e) + if(!e.filename && e.time){ + e.filename = s.formattedTime(e.time) + } + var filename, + time + if(e.filename.indexOf('.')>-1){ + filename = e.filename + }else{ + filename = e.filename+'.'+e.ext + } + if(e.filename && !e.time){ + time = s.nameToTime(filename) + }else{ + time = e.time + } + time = new Date(time) + var queryValues = [e.id,e.ke,time]; + s.sqlQuery('SELECT * FROM Videos WHERE `mid`=? AND `ke`=? AND `time`=?',queryValues,function(err,r){ + if(r && r[0]){ + r = r[0] + fs.chmod(e.dir+filename,0o777,function(err){ + s.tx({ + f: 'video_delete', + filename: filename, + mid: e.id, + ke: e.ke, + time: s.nameToTime(filename), + end: s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss') + },'GRP_'+e.ke); + s.setDiskUsedForGroup(e,-(r.size / 1000000)) + s.sqlQuery('DELETE FROM Videos WHERE `mid`=? AND `ke`=? AND `time`=?',queryValues,function(err){ + if(err){ + s.systemLog(lang['File Delete Error'] + ' : '+e.ke+' : '+' : '+e.id,err) + } + }) + fs.unlink(e.dir+filename,function(err){ + fs.stat(e.dir+filename,function(err){ + if(!err){ + s.file('delete',e.dir+filename) + } + }) + }) + }) + var videoSnap = s.dir.videoSnaps + e.ke + '/' + e.mid + '/' + filename.split('.')[0] + '.jpg' + fs.chmod(videoSnap,0o777,function(err){ + if(!err){ + fs.unlink(videoSnap,function(err){}) + } + }) + }else{ + console.log(lang['Database row does not exist'],queryValues) + } + }) + } + s.deleteVideoFromCloudExtensions = {} + s.deleteVideoFromCloudExtensionsRunner = function(e,storageType,video){ + // e = user + if(!storageType){ + var videoDetails = JSON.parse(r.details) + videoDetails.type = videoDetails.type || 's3' + } + if(s.deleteVideoFromCloudExtensions[storageType]){ + s.deleteVideoFromCloudExtensions[storageType](e,video,function(){ + s.tx({ + f: 'video_delete_cloud', + mid: e.mid, + ke: e.ke, + time: e.time, + end: e.end + },'GRP_'+e.ke); + }) + } + } + s.deleteVideoFromCloud = function(e){ + // e = video object + s.checkDetails(e) + var videoSelector = [e.id,e.ke,new Date(e.time)] + s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE `mid`=? AND `ke`=? AND `time`=?',videoSelector,function(err,r){ + if(r&&r[0]){ + r = r[0] + s.sqlQuery('DELETE FROM `Cloud Videos` WHERE `mid`=? AND `ke`=? AND `time`=?',videoSelector,function(){ + s.deleteVideoFromCloudExtensionsRunner(e,r) + }) + }else{ +// console.log('Delete Failed',e) +// console.error(err) + } + }) + } + s.orphanedVideoCheck = function(monitor,checkMax,callback,forceCheck){ + var finish = function(orphanedFilesCount){ + if(callback)callback(orphanedFilesCount) + } + if(forceCheck === true || config.insertOrphans === true){ + if(!checkMax){ + checkMax = config.orphanedVideoCheckMax + } + var videosDirectory = s.getVideoDirectory(monitor)// + s.formattedTime(video.time) + '.' + video.ext + fs.readdir(videosDirectory,function(err,files){ + if(files && files.length > 0){ + var fiveRecentFiles = files.slice(files.length - checkMax,files.length) + var completedFile = 0 + var orphanedFilesCount = 0 + var fileComplete = function(){ + ++completedFile + if(fiveRecentFiles.length === completedFile){ + finish(orphanedFilesCount) + } + } + fiveRecentFiles.forEach(function(filename){ + if(/T[0-9][0-9]-[0-9][0-9]-[0-9][0-9]./.test(filename)){ + var queryValues = [ + monitor.ke, + monitor.mid, + s.nameToTime(filename) + ] + s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND mid=? AND time=? LIMIT 1',queryValues,function(err,rows){ + if(!err && (!rows || !rows[0])){ + ++orphanedFilesCount + var video = rows[0] + s.insertCompletedVideo(monitor,{ + file : filename + },function(){ + fileComplete() + }) + }else{ + fileComplete() + } + }) + } + }) + }else{ + finish() + } + }) + }else{ + finish() + } + } +} diff --git a/libs/webServer.js b/libs/webServer.js new file mode 100644 index 0000000..bc86631 --- /dev/null +++ b/libs/webServer.js @@ -0,0 +1,35 @@ +var fs = require('fs'); +var http = require('http'); +var https = require('https'); +var express = require('express'); +var app = express() +module.exports = function(s,config,lang,io){ + var server = http.createServer(app); + //SSL options + if(config.ssl&&config.ssl.key&&config.ssl.cert){ + config.ssl.key=fs.readFileSync(s.checkRelativePath(config.ssl.key),'utf8') + config.ssl.cert=fs.readFileSync(s.checkRelativePath(config.ssl.cert),'utf8') + if(config.ssl.port === undefined){ + config.ssl.port=443 + } + if(config.ssl.bindip === undefined){ + config.ssl.bindip=config.bindip + } + if(config.ssl.ca&&config.ssl.ca instanceof Array){ + config.ssl.ca.forEach(function(v,n){ + config.ssl.ca[n]=fs.readFileSync(s.checkRelativePath(v),'utf8') + }) + } + var serverHTTPS = https.createServer(config.ssl,app); + serverHTTPS.listen(config.ssl.port,config.bindip,function(){ + console.log('SSL '+lang.Shinobi+' : SSL Web Server Listening on '+config.ssl.port); + }); + io.attach(serverHTTPS); + } + //start HTTP + server.listen(config.port,config.bindip,function(){ + console.log(lang.Shinobi+' : Web Server Listening on '+config.port); + }); + io.attach(server); + return app +} diff --git a/libs/webServerAdminPaths.js b/libs/webServerAdminPaths.js new file mode 100644 index 0000000..3cc62e2 --- /dev/null +++ b/libs/webServerAdminPaths.js @@ -0,0 +1,428 @@ +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; +module.exports = function(s,config,lang,app){ + var closeResponse = function(res,endData){ + res.setHeader('Content-Type', 'application/json') + res.end(s.prettyPrint(endData)) + } + /** + * API : Administrator : Edit Sub-Account (Account to share cameras with) + */ + app.all(config.webPaths.adminApiPrefix+':auth/accounts/:ke/edit', function (req,res){ + s.auth(req.params,function(user){ + var endData = { + ok : false + } + if(user.details.sub){ + endData.msg = user.lang['Not Permitted'] + closeResponse(res,endData) + return + } + var form = s.getPostData(req) + var uid = s.getPostData(req,'uid',false) + var mail = s.getPostData(req,'mail',false) + if(form){ + var keys = Object.keys(form) + var condition = [] + var value = [] + keys.forEach(function(v){ + condition.push(v+'=?') + if(form[v] instanceof Object)form[v] = JSON.stringify(form[v]) + value.push(form[v]) + }) + value = value.concat([req.params.ke,uid]) + s.sqlQuery("UPDATE Users SET "+condition.join(',')+" WHERE ke=? AND uid=?",value) + s.tx({ + f: 'edit_sub_account', + ke: req.params.ke, + uid: uid, + mail: mail, + form: form + },'ADM_'+req.params.ke) + endData.ok = true + s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[req.params.ke,uid],function(err,rows){ + if(rows && rows[0]){ + rows.forEach(function(row){ + delete(s.api[row.code]) + }) + } + }) + }else{ + endData.msg = lang.postDataBroken + } + closeResponse(res,endData) + },res,req) + }) + /** + * API : Administrator : Delete Sub-Account (Account to share cameras with) + */ + app.all(config.webPaths.adminApiPrefix+':auth/accounts/:ke/delete', function (req,res){ + s.auth(req.params,function(user){ + var endData = { + ok : false + } + if(user.details.sub){ + endData.msg = user.lang['Not Permitted'] + closeResponse(res,endData) + return + } + var uid = s.getPostData(req,'uid',false) + var mail = s.getPostData(req,'mail',false) + s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[uid,req.params.ke,mail]) + s.sqlQuery("SELECT * FROM API WHERE ke=? AND uid=?",[req.params.ke,uid],function(err,rows){ + if(rows && rows[0]){ + rows.forEach(function(row){ + delete(s.api[row.code]) + }) + s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[uid,req.params.ke]) + } + }) + s.tx({ + f: 'delete_sub_account', + ke: req.params.ke, + uid: uid, + mail: mail + },'ADM_'+req.params.ke) + endData.ok = true + closeResponse(res,endData) + },res,req) + }) + /** + * API : Administrator : Add Sub-Account (Account to share cameras with) + */ + app.post([ + config.webPaths.adminApiPrefix+':auth/accounts/:ke/register', + //these two routes are for backwards compatibility + config.webPaths.adminApiPrefix+':auth/register/:ke/:uid', + config.webPaths.apiPrefix+':auth/register/:ke/:uid' + ],function (req,res){ + endData = { + ok : false + } + res.setHeader('Content-Type', 'application/json'); + s.auth(req.params,function(user){ + if(user.details.sub){ + endData.msg = user.lang['Not an Administrator Account'] + closeResponse(res,endData) + return + } + var form = s.getPostData(req) + if(form.mail !== '' && form.pass !== ''){ + if(form.pass === form.password_again || form.pass === form.pass_again){ + s.sqlQuery('SELECT * FROM Users WHERE mail=?',[form.mail],function(err,r) { + if(r&&r[0]){ + //found one exist + endData.msg = 'Email address is in use.' + }else{ + //create new + endData.msg = 'New Account Created' + endData.ok = true + var newId = s.gid() + var details = s.s({ + sub: "1", + allmonitors: "1" + }) + s.sqlQuery('INSERT INTO Users (ke,uid,mail,pass,details) VALUES (?,?,?,?,?)',[req.params.ke,newId,form.mail,s.createHash(form.pass),details]) + s.tx({ + f: 'add_sub_account', + details: details, + ke: req.params.ke, + uid: newId, + mail: form.mail + },'ADM_'+req.params.ke) + } + res.end(s.prettyPrint(endData)) + }) + }else{ + endData.msg = user.lang["Passwords Don't Match"] + } + }else{ + endData.msg = user.lang['Fields cannot be empty'] + } + if(endData.msg){ + res.end(s.prettyPrint(endData)) + } + },res,req) + }) + /** + * API : Administrator : Monitor : Add, Edit, and Delete + */ + app.all([ + config.webPaths.apiPrefix+':auth/configureMonitor/:ke/:id', + config.webPaths.apiPrefix+':auth/configureMonitor/:ke/:id/:f', + config.webPaths.adminApiPrefix+':auth/configureMonitor/:ke/:id', + config.webPaths.adminApiPrefix+':auth/configureMonitor/:ke/:id/:f' + ], function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' + if(req.params.f !== 'delete'){ + var form = s.getPostData(req) + if(!form){ + req.ret.msg = user.lang.monitorEditText1; + res.end(s.prettyPrint(req.ret)) + return + } + if(!user.details.sub || + user.details.allmonitors === '1' || + hasRestrictions && user.details.monitor_edit.indexOf(form.mid) >- 1 || + hasRestrictions && user.details.monitor_create === '1'){ + if(form&&form.mid&&form.name){ + req.set=[],req.ar=[]; + form.mid=req.params.id.replace(/[^\w\s]/gi,'').replace(/ /g,''); + try{ + JSON.parse(form.details) + }catch(er){ + if(!form.details||!form.details.stream_type){ + req.ret.msg=user.lang.monitorEditText2; + res.end(s.prettyPrint(req.ret)) + return + }else{ + form.details=JSON.stringify(form.details) + } + } + form.ke=req.params.ke + req.logObject={details:JSON.parse(form.details),ke:req.params.ke,mid:req.params.id} + s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[form.ke,form.mid],function(er,r){ + req.tx={f:'monitor_edit',mid:form.mid,ke:form.ke,mon:form}; + if(r&&r[0]){ + req.tx.new=false; + Object.keys(form).forEach(function(v){ + if(form[v]&&form[v]!==''){ + req.set.push(v+'=?'),req.ar.push(form[v]); + } + }) + req.set=req.set.join(','); + req.ar.push(form.ke),req.ar.push(form.mid); + s.userLog(form,{type:'Monitor Updated',msg:'by user : '+user.uid}); + req.ret.msg=user.lang['Monitor Updated by user']+' : '+user.uid; + s.sqlQuery('UPDATE Monitors SET '+req.set+' WHERE ke=? AND mid=?',req.ar) + req.finish=1; + }else{ + if(!s.group[form.ke].init.max_camera||s.group[form.ke].init.max_camera==''||Object.keys(s.group[form.ke].mon).length <= parseInt(s.group[form.ke].init.max_camera)){ + req.tx.new=true; + req.st=[]; + Object.keys(form).forEach(function(v){ + if(form[v]&&form[v]!==''){ + req.set.push(v),req.st.push('?'),req.ar.push(form[v]); + } + }) + // req.set.push('ke'),req.st.push('?'),req.ar.push(form.ke); + req.set=req.set.join(','),req.st=req.st.join(','); + s.userLog(form,{type:'Monitor Added',msg:'by user : '+user.uid}); + req.ret.msg=user.lang['Monitor Added by user']+' : '+user.uid; + s.sqlQuery('INSERT INTO Monitors ('+req.set+') VALUES ('+req.st+')',req.ar) + req.finish=1; + }else{ + req.tx.f='monitor_edit_failed'; + req.tx.ff='max_reached'; + req.ret.msg=user.lang.monitorEditFailedMaxReached; + } + } + if(req.finish===1){ + form.details=JSON.parse(form.details) + req.ret.ok=true; + s.initiateMonitorObject({mid:form.mid,ke:form.ke}); + s.group[form.ke].mon_conf[form.mid]=s.cleanMonitorObject(form); + if(form.mode==='stop'){ + s.camera('stop',form); + }else{ + s.camera('stop',form);setTimeout(function(){s.camera(form.mode,form);},5000) + }; + s.tx(req.tx,'STR_'+form.ke); + }; + s.tx(req.tx,'GRP_'+form.ke); + res.end(s.prettyPrint(req.ret)) + }) + }else{ + req.ret.msg=user.lang.monitorEditText1; + res.end(s.prettyPrint(req.ret)) + } + }else{ + req.ret.msg=user.lang['Not Permitted']; + res.end(s.prettyPrint(req.ret)) + } + }else{ + if(!user.details.sub || user.details.allmonitors === '1' || user.details.monitor_edit.indexOf(req.params.id) > -1 || hasRestrictions && user.details.monitor_create === '1'){ + s.userLog(s.group[req.params.ke].mon_conf[req.params.id],{type:'Monitor Deleted',msg:'by user : '+user.uid}); + req.params.delete=1;s.camera('stop',req.params); + s.tx({f:'monitor_delete',uid:user.uid,mid:req.params.id,ke:req.params.ke},'GRP_'+req.params.ke); + s.sqlQuery('DELETE FROM Monitors WHERE ke=? AND mid=?',[req.params.ke,req.params.id]) + // s.sqlQuery('DELETE FROM Files WHERE ke=? AND mid=?',[req.params.ke,req.params.id]) + if(req.query.deleteFiles === 'true'){ + //videos + s.dir.addStorage.forEach(function(v,n){ + var videosDir = v.path+req.params.ke+'/'+req.params.id+'/' + fs.stat(videosDir,function(err,stat){ + if(!err){ + s.file('deleteFolder',videosDir) + } + }) + }) + var videosDir = s.dir.videos+req.params.ke+'/'+req.params.id+'/' + fs.stat(videosDir,function(err,stat){ + if(!err){ + s.file('deleteFolder',videosDir) + } + }) + //fileBin + var binDir = s.dir.fileBin+req.params.ke+'/'+req.params.id+'/' + fs.stat(binDir,function(err,stat){ + if(!err){ + s.file('deleteFolder',binDir) + } + }) + } + req.ret.ok=true; + req.ret.msg='Monitor Deleted by user : '+user.uid + res.end(s.prettyPrint(req.ret)) + }else{ + req.ret.msg=user.lang['Not Permitted']; + res.end(s.prettyPrint(req.ret)) + } + } + },res,req) + }) + /** + * API : Add API Key, binded to the user who created it + */ + app.all([ + config.webPaths.adminApiPrefix+':auth/api/:ke/add', + config.webPaths.apiPrefix+':auth/api/:ke/add', + ],function (req,res){ + var endData = {ok:false} + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + var endData = { + ok : false + } + var form = s.getPostData(req) + if(form){ + var insert = { + ke : req.params.ke, + uid : user.uid, + code : s.gid(30), + ip : form.ip, + details : s.stringJSON(form.details) + } + var escapes = [] + Object.keys(insert).forEach(function(column){ + escapes.push('?') + }); + s.sqlQuery('INSERT INTO API ('+Object.keys(insert).join(',')+') VALUES ('+escapes.join(',')+')',Object.values(insert),function(err,r){ + insert.time = s.formattedTime(new Date,'YYYY-DD-MM HH:mm:ss'); + if(!err){ + s.tx({ + f: 'api_key_added', + uid: user.uid, + form: insert + },'GRP_' + req.params.ke) + endData.ok = true + } + closeResponse(res,endData) + }) + }else{ + endData.msg = lang.postDataBroken + closeResponse(res,endData) + } + },res,req) + }) + /** + * API : Delete API Key + */ + app.all([ + config.webPaths.adminApiPrefix+':auth/api/:ke/delete', + config.webPaths.apiPrefix+':auth/api/:ke/delete', + ],function (req,res){ + var endData = {ok:false} + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + var endData = { + ok : false + } + var form = s.getPostData(req) + if(form){ + if(!form.code){ + s.tx({ + f:'form_incomplete', + uid: user.uid, + form:'APIs' + },'GRP_' + req.params.ke) + endData.msg = lang.postDataBroken + closeResponse(res,endData) + return + } + var row = { + ke : req.params.ke, + uid : user.uid, + code : form.code + } + var where = [] + Object.keys(row).forEach(function(column){ + where.push(column+'=?') + }) + s.sqlQuery('DELETE FROM API WHERE '+where.join(' AND '),Object.values(row),function(err,r){ + if(!err){ + s.tx({ + f: 'api_key_deleted', + uid: user.uid, + form: row + },'GRP_' + req.params.ke) + endData.ok = true + delete(s.api[row.code]) + } + closeResponse(res,endData) + }) + }else{ + endData.msg = lang.postDataBroken + closeResponse(res,endData) + } + },res,req) + }) + /** + * API : List API Keys for Authenticated user + */ + app.get([ + config.webPaths.adminApiPrefix+':auth/api/:ke/list', + config.webPaths.apiPrefix+':auth/api/:ke/list', + ],function (req,res){ + var endData = {ok:false} + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + var endData = { + ok : false + } + var row = { + ke : req.params.ke, + uid : user.uid + } + var where = [] + Object.keys(row).forEach(function(column){ + where.push(column+'=?') + }) + s.sqlQuery('SELECT * FROM API WHERE '+where.join(' AND '),Object.values(row),function(err,rows){ + if(rows && rows[0]){ + rows.forEach(function(row){ + row.details = JSON.parse(row.details) + }) + endData.ok = true + endData.uid = user.uid + endData.ke = user.ke + endData.keys = rows + } + closeResponse(res,endData) + }) + },res,req) + }) +} diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js new file mode 100644 index 0000000..316c4ac --- /dev/null +++ b/libs/webServerPaths.js @@ -0,0 +1,1843 @@ +var express = require('express'); +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; +var httpProxy = require('http-proxy'); +var proxy = httpProxy.createProxyServer({}) +var ejs = require('ejs'); +var CircularJSON = require('circular-json'); +module.exports = function(s,config,lang,app){ + if(config.productType==='Pro'){ + var LdapAuth = require('ldapauth-fork'); + } + //get page URL + if(!config.baseURL){ + config.baseURL = "" + }else if(config.baseURL !== ''){ + config.baseURL = s.checkCorrectPathEnding(config.baseURL) + } + //Render Configurations - Web Paths + if(config.webPaths === undefined){config.webPaths={}} + //main access URI + if(config.webPaths.home === undefined){config.webPaths.home='/'} + //Super User URI + if(config.webPaths.super === undefined){config.webPaths.super='/super'} + //Admin URI + if(config.webPaths.admin === undefined){config.webPaths.admin='/admin'} + //API Prefix + if(config.webPaths.apiPrefix === undefined){config.webPaths.apiPrefix='/'}else{config.webPaths.apiPrefix = s.checkCorrectPathEnding(config.webPaths.apiPrefix)} + //Admin API Prefix + if(config.webPaths.adminApiPrefix === undefined){config.webPaths.adminApiPrefix='/admin/'}else{config.webPaths.adminApiPrefix = s.checkCorrectPathEnding(config.webPaths.adminApiPrefix)} + //Super API Prefix + if(config.webPaths.superApiPrefix === undefined){config.webPaths.superApiPrefix='/super/'}else{config.webPaths.superApiPrefix = s.checkCorrectPathEnding(config.webPaths.superApiPrefix)} + //Render Configurations - Page Render Paths + if(config.renderPaths === undefined){config.renderPaths={}} + //login page + if(config.renderPaths.index === undefined){config.renderPaths.index='pages/index'} + //dashboard page + if(config.renderPaths.home === undefined){config.renderPaths.home='pages/home'} + //sub-account administration page + if(config.renderPaths.admin === undefined){config.renderPaths.admin='pages/admin'} + //superuser page + if(config.renderPaths.super === undefined){config.renderPaths.super='pages/super'} + //2-Factor Auth page + if(config.renderPaths.factorAuth === undefined){config.renderPaths.factorAuth='pages/factor'} + //Streamer v1 (Dashcam Prototype) page + if(config.renderPaths.streamer === undefined){config.renderPaths.streamer='pages/streamer'} + //Streamer v2 (Dashcam) page + if(config.renderPaths.dashcam === undefined){config.renderPaths.dashcam='pages/dashcam'} + //embeddable widget page + if(config.renderPaths.embed === undefined){config.renderPaths.embed='pages/embed'} + //mjpeg full screen page + if(config.renderPaths.mjpeg === undefined){config.renderPaths.mjpeg='pages/mjpeg'} + //gridstack only page + if(config.renderPaths.grid === undefined){config.renderPaths.grid='pages/grid'} + //slick.js (cycle) page + if(config.renderPaths.cycle === undefined){config.renderPaths.cycle='pages/cycle'} + //child node proxy check + //params = parameters + //cb = callback + //res = response, only needed for express (http server) + //request = request, only needed for express (http server) + s.checkChildProxy = function(params,cb,res,req){ + if(s.group[params.ke] && s.group[params.ke].mon[params.id] && s.group[params.ke].mon[params.id].childNode){ + var url = 'http://' + s.group[params.ke].mon[params.id].childNode// + req.originalUrl + proxy.web(req, res, { target: url }) + }else{ + cb() + } + } + //get post data + s.getPostData = function(req,target,parseJSON){ + if(!target)target = 'data' + if(!parseJSON)parseJSON = true + var postData = false + if(req.query && req.query[target]){ + postData = req.query[target] + }else{ + postData = req.body[target] + } + if(parseJSON === true){ + postData = s.parseJSON(postData) + } + return postData + } + //get client ip address + s.getClientIp = function(req){ + return req.headers['cf-connecting-ip']||req.headers["CF-Connecting-IP"]||req.headers["'x-forwarded-for"]||req.connection.remoteAddress; + } + ////Pages + app.enable('trust proxy'); + app.use('/libs',express.static(s.mainDirectory + '/web/libs')); + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({extended: true})); + app.set('views', s.mainDirectory + '/web'); + app.set('view engine','ejs'); + //add template handler + if(config.renderPaths.handler!==undefined){require(s.mainDirectory+'/web/'+config.renderPaths.handler+'.js').addHandlers(s,app,io)} + + /** + * API : Logout + */ + app.get(config.webPaths.apiPrefix+':auth/logout/:ke/:id', function (req,res){ + if(s.group[req.params.ke]&&s.group[req.params.ke].users[req.params.auth]){ + delete(s.api[req.params.auth]); + delete(s.group[req.params.ke].users[req.params.auth]); + s.sqlQuery("UPDATE Users SET auth=? WHERE auth=? AND ke=? AND uid=?",['',req.params.auth,req.params.ke,req.params.id]) + res.end(s.prettyPrint({ok:true,msg:'You have been logged out, session key is now inactive.'})) + }else{ + res.end(s.prettyPrint({ok:false,msg:'This group key does not exist or this user is not logged in.'})) + } + }); + /** + * Page : Login Screen + */ + app.get(config.webPaths.home, function (req,res){ + res.render(config.renderPaths.index,{lang:lang,config:config,screen:'dashboard',originalURL:s.getOriginalUrl(req)},function(err,html){ + if(err){ + s.systemLog(err) + } + res.end(html) + }) + }); + /** + * Page : Administrator Login Screen + */ + app.get(config.webPaths.admin, function (req,res){ + res.render(config.renderPaths.index,{lang:lang,config:config,screen:'admin',originalURL:s.getOriginalUrl(req)},function(err,html){ + if(err){ + s.systemLog(err) + } + res.end(html) + }) + }); + /** + * Page : Superuser Login Screen + */ + app.get(config.webPaths.super, function (req,res){ + + res.render(config.renderPaths.index,{lang:lang,config:config,screen:'super',originalURL:s.getOriginalUrl(req)},function(err,html){ + if(err){ + s.systemLog(err) + } + res.end(html) + }) + }); + /** + * API : Get User Info + */ + app.get(config.webPaths.apiPrefix+':auth/userInfo/:ke',function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + req.ret.ok=true + req.ret.user=user + res.end(s.prettyPrint(req.ret)); + },res,req); + }) + //login function + s.deleteFactorAuth=function(r){ + delete(s.factorAuth[r.ke][r.uid]) + if(Object.keys(s.factorAuth[r.ke]).length===0){ + delete(s.factorAuth[r.ke]) + } + } + /** + * API : Login handler. Dashboard, Streamer, Dashcam Administrator, Superuser + */ + app.post([config.webPaths.home,s.checkCorrectPathEnding(config.webPaths.home)+':screen'],function (req,res){ + req.ip = s.getClientIp(req) + if(req.query.json === 'true'){ + res.header("Access-Control-Allow-Origin",req.headers.origin); + } + // brute check + if(s.failedLoginAttempts[req.body.mail] && s.failedLoginAttempts[req.body.mail].failCount >= 5){ + if(req.query.json=='true'){ + res.end(s.prettyPrint({ok:false})) + }else{ + res.render(config.renderPaths.index,{ + failedLogin:true, + message:lang.failedLoginText1, + lang:lang, + config:config, + screen:req.params.screen, + originalURL:s.getOriginalUrl(req) + },function(err,html){ + if(err){ + s.systemLog(err) + } + res.end(html) + }) + } + return false + } + // + renderPage = function(focus,data){ + if(s.failedLoginAttempts[req.body.mail]){ + clearTimeout(s.failedLoginAttempts[req.body.mail].timeout) + delete(s.failedLoginAttempts[req.body.mail]) + } + if(req.query.json=='true'){ + delete(data.config) + data.ok=true; + res.setHeader('Content-Type', 'application/json'); + res.end(s.prettyPrint(data)) + }else{ + data.originalURL = s.getOriginalUrl(req) + data.screen=req.params.screen + res.render(focus,data,function(err,html){ + if(err){ + s.systemLog(err) + } + res.end(html) + }) + } + } + failedAuthentication = function(board){ + // brute protector + if(!s.failedLoginAttempts[req.body.mail]){ + s.failedLoginAttempts[req.body.mail] = { + failCount : 0, + ips : {} + } + } + ++s.failedLoginAttempts[req.body.mail].failCount + if(!s.failedLoginAttempts[req.body.mail].ips[req.ip]){ + s.failedLoginAttempts[req.body.mail].ips[req.ip] = 0 + } + ++s.failedLoginAttempts[req.body.mail].ips[req.ip] + clearTimeout(s.failedLoginAttempts[req.body.mail].timeout) + s.failedLoginAttempts[req.body.mail].timeout = setTimeout(function(){ + delete(s.failedLoginAttempts[req.body.mail]) + },1000 * 60 * 15) + // check if JSON + if(req.query.json === 'true'){ + res.setHeader('Content-Type', 'application/json') + res.end(s.prettyPrint({ok:false})) + }else{ + res.render(config.renderPaths.index,{ + failedLogin:true, + message:lang.failedLoginText2, + lang:lang, + config:config, + screen:req.params.screen, + originalURL:s.getOriginalUrl(req) + },function(err,html){ + if(err){ + s.systemLog(err) + } + res.end(html) + }) + } + var logTo = { + ke: '$', + mid: '$USER' + } + var logData = { + type: lang['Authentication Failed'], + msg: { + for: board, + mail: req.body.mail, + ip: req.ip + } + } + if(board==='super'){ + s.userLog(logTo,logData) + }else{ + s.sqlQuery('SELECT ke,uid,details FROM Users WHERE mail=?',[req.body.mail],function(err,r) { + if(r&&r[0]){ + r = r[0] + r.details=JSON.parse(r.details); + r.lang=s.getLanguageFile(r.details.lang) + logData.id=r.uid + logData.type=r.lang['Authentication Failed'] + logTo.ke = r.ke + } + s.userLog(logTo,logData) + }) + } + } + checkRoute = function(r){ + switch(req.body.function){ + case'cam': + s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND type=?',[r.ke,"dashcam"],function(err,rr){ + req.resp.mons=rr; + renderPage(config.renderPaths.dashcam,{ + // config: config, + $user: req.resp, + lang: r.lang, + define: s.getDefinitonFile(r.details.lang) + }) + }) + break; + case'streamer': + s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND type=?',[r.ke,"socket"],function(err,rr){ + req.resp.mons=rr; + renderPage(config.renderPaths.streamer,{ + // config: config, + $user: req.resp, + lang: r.lang, + define: s.getDefinitonFile(r.details.lang) + }) + }) + break; + case'admin': + if(!r.details.sub){ + s.sqlQuery('SELECT uid,mail,details FROM Users WHERE ke=? AND details LIKE \'%"sub"%\'',[r.ke],function(err,rr) { + s.sqlQuery('SELECT * FROM Monitors WHERE ke=?',[r.ke],function(err,rrr) { + renderPage(config.renderPaths.admin,{ + config: config, + $user: req.resp, + $subs: rr, + $mons: rrr, + lang: r.lang, + define: s.getDefinitonFile(r.details.lang) + }) + }) + }) + }else{ + //not admin user + renderPage(config.renderPaths.home,{$user:req.resp,config:config,lang:r.lang,define:s.getDefinitonFile(r.details.lang),addStorage:s.dir.addStorage,fs:fs,__dirname:s.mainDirectory}); + } + break; + default: + renderPage(config.renderPaths.home,{$user:req.resp,config:config,lang:r.lang,define:s.getDefinitonFile(r.details.lang),addStorage:s.dir.addStorage,fs:fs,__dirname:s.mainDirectory}); + break; + } + s.userLog({ke:r.ke,mid:'$USER'},{type:r.lang['New Authentication Token'],msg:{for:req.body.function,mail:r.mail,id:r.uid,ip:req.ip}}) + // res.end(); + } + if(req.body.mail&&req.body.pass){ + req.default=function(){ + s.sqlQuery('SELECT * FROM Users WHERE mail=? AND pass=?',[req.body.mail,s.createHash(req.body.pass)],function(err,r) { + req.resp={ok:false}; + if(!err&&r&&r[0]){ + r=r[0];r.auth=s.md5(s.gid()); + s.sqlQuery("UPDATE Users SET auth=? WHERE ke=? AND uid=?",[r.auth,r.ke,r.uid]) + req.resp={ok:true,auth_token:r.auth,ke:r.ke,uid:r.uid,mail:r.mail,details:r.details}; + r.details=JSON.parse(r.details); + r.lang=s.getLanguageFile(r.details.lang) + req.factorAuth=function(cb){ + if(r.details.factorAuth==="1"){ + if(!r.details.acceptedMachines||!(r.details.acceptedMachines instanceof Object)){ + r.details.acceptedMachines={} + } + if(!r.details.acceptedMachines[req.body.machineID]){ + req.complete=function(){ + s.factorAuth[r.ke][r.uid].function = req.body.function + s.factorAuth[r.ke][r.uid].info = req.resp + clearTimeout(s.factorAuth[r.ke][r.uid].expireAuth) + s.factorAuth[r.ke][r.uid].expireAuth=setTimeout(function(){ + s.deleteFactorAuth(r) + },1000*60*15) + renderPage(config.renderPaths.factorAuth,{$user:req.resp,lang:r.lang}) + } + if(!s.factorAuth[r.ke]){s.factorAuth[r.ke]={}} + if(!s.factorAuth[r.ke][r.uid]){ + s.factorAuth[r.ke][r.uid]={key:s.nid(),user:r} + s.onTwoFactorAuthCodeNotificationExtensions.forEach(function(extender){ + extender(r) + }) + req.complete() + }else{ + req.complete() + } + }else{ + checkRoute(r) + } + }else{ + checkRoute(r) + } + } + if(r.details.sub){ + s.sqlQuery('SELECT details FROM Users WHERE ke=? AND details NOT LIKE ?',[r.ke,'%"sub"%'],function(err,rr) { + rr=rr[0]; + rr.details=JSON.parse(rr.details); + r.details.mon_groups=rr.details.mon_groups; + req.resp.details=JSON.stringify(r.details); + req.factorAuth() + }) + }else{ + req.factorAuth() + } + }else{ + failedAuthentication(req.body.function) + } + }) + } + if(LdapAuth&&req.body.function==='ldap'&&req.body.key!==''){ + s.sqlQuery('SELECT * FROM Users WHERE ke=? AND details NOT LIKE ?',[req.body.key,'%"sub"%'],function(err,r) { + if(r&&r[0]){ + r=r[0] + r.details=JSON.parse(r.details) + r.lang=s.getLanguageFile(r.details.lang) + if(r.details.use_ldap!=='0'&&r.details.ldap_enable==='1'&&r.details.ldap_url&&r.details.ldap_url!==''){ + req.mailArray={} + req.body.mail.split(',').forEach(function(v){ + v=v.split('=') + req.mailArray[v[0]]=v[1] + }) + if(!r.details.ldap_bindDN||r.details.ldap_bindDN===''){ + r.details.ldap_bindDN=req.body.mail + } + if(!r.details.ldap_bindCredentials||r.details.ldap_bindCredentials===''){ + r.details.ldap_bindCredentials=req.body.pass + } + if(!r.details.ldap_searchFilter||r.details.ldap_searchFilter===''){ + r.details.ldap_searchFilter=req.body.mail + if(req.mailArray.cn){ + r.details.ldap_searchFilter='cn='+req.mailArray.cn + } + if(req.mailArray.uid){ + r.details.ldap_searchFilter='uid='+req.mailArray.uid + } + }else{ + r.details.ldap_searchFilter=r.details.ldap_searchFilter.replace('{{username}}',req.body.mail) + } + if(!r.details.ldap_searchBase||r.details.ldap_searchBase===''){ + r.details.ldap_searchBase='dc=test,dc=com' + } + req.auth = new LdapAuth({ + url:r.details.ldap_url, + bindDN:r.details.ldap_bindDN, + bindCredentials:r.details.ldap_bindCredentials, + searchBase:r.details.ldap_searchBase, + searchFilter:'('+r.details.ldap_searchFilter+')', + reconnect:true + }); + req.auth.on('error', function (err) { + console.error('LdapAuth: ', err); + }); + + req.auth.authenticate(req.body.mail, req.body.pass, function(err, user) { + if(user){ + //found user + if(!user.uid){ + user.uid=s.gid() + } + req.resp={ + ke:req.body.key, + uid:user.uid, + auth:s.createHash(s.gid()), + mail:user.mail, + pass:s.createHash(req.body.pass), + details:JSON.stringify({ + sub:'1', + ldap:'1', + allmonitors:'1', + filter: {} + }) + } + user.post=[] + Object.keys(req.resp).forEach(function(v){ + user.post.push(req.resp[v]) + }) + s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP Success'],msg:{user:user}}) + s.sqlQuery('SELECT * FROM Users WHERE ke=? AND mail=?',[req.body.key,user.cn],function(err,rr){ + if(rr&&rr[0]){ + //already registered + rr=rr[0] + req.resp=rr; + rr.details=JSON.parse(rr.details) + req.resp.lang=s.getLanguageFile(rr.details.lang) + s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP User Authenticated'],msg:{user:user,shinobiUID:rr.uid}}) + s.sqlQuery("UPDATE Users SET auth=? WHERE ke=? AND uid=?",[req.resp.auth,req.resp.ke,rr.uid]) + }else{ + //new ldap login + s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP User is New'],msg:{info:r.lang['Creating New Account'],user:user}}) + req.resp.lang=r.lang + s.sqlQuery('INSERT INTO Users (ke,uid,auth,mail,pass,details) VALUES (?,?,?,?,?,?)',user.post) + } + req.resp.details=JSON.stringify(req.resp.details) + req.resp.auth_token=req.resp.auth + req.resp.ok=true + checkRoute(req.resp) + }) + return + } + s.userLog({ke:req.body.key,mid:'$USER'},{type:r.lang['LDAP Failed'],msg:{err:err}}) + //no user + req.default() + }); + + req.auth.close(function(err) { + + }) + }else{ + req.default() + } + }else{ + req.default() + } + }) + }else{ + if(req.body.function === 'super'){ + if(!fs.existsSync(s.location.super)){ + res.end(lang.superAdminText) + return + } + var ok = s.superAuth({ + mail: req.body.mail, + pass: req.body.pass, + users: true, + md5: true + },function(data){ + s.sqlQuery('SELECT * FROM Logs WHERE ke=? ORDER BY `time` DESC LIMIT 30',['$'],function(err,r) { + if(!r){ + r=[] + } + data.Logs = r + fs.readFile(s.location.config,'utf8',function(err,file){ + data.plainConfig = JSON.parse(file) + renderPage(config.renderPaths.super,data) + }) + }) + }) + if(ok === false){ + failedAuthentication(req.body.function) + } + }else{ + req.default() + } + } + }else{ + if(req.body.machineID&&req.body.factorAuthKey){ + if(s.factorAuth[req.body.ke]&&s.factorAuth[req.body.ke][req.body.id]&&s.factorAuth[req.body.ke][req.body.id].key===req.body.factorAuthKey){ + if(s.factorAuth[req.body.ke][req.body.id].key===req.body.factorAuthKey){ + if(req.body.remember==="1"){ + req.details=JSON.parse(s.factorAuth[req.body.ke][req.body.id].info.details) + req.lang=s.getLanguageFile(req.details.lang) + if(!req.details.acceptedMachines||!(req.details.acceptedMachines instanceof Object)){ + req.details.acceptedMachines={} + } + if(!req.details.acceptedMachines[req.body.machineID]){ + req.details.acceptedMachines[req.body.machineID]={} + s.sqlQuery("UPDATE Users SET details=? WHERE ke=? AND uid=?",[s.prettyPrint(req.details),req.body.ke,req.body.id]) + } + } + req.body.function = s.factorAuth[req.body.ke][req.body.id].function + req.resp = s.factorAuth[req.body.ke][req.body.id].info + checkRoute(s.factorAuth[req.body.ke][req.body.id].user) + }else{ + renderPage(config.renderPaths.factorAuth,{$user:s.factorAuth[req.body.ke][req.body.id].info,lang:req.lang}); + res.end(); + } + }else{ + failedAuthentication(lang['2-Factor Authentication']) + } + }else{ + failedAuthentication(lang['2-Factor Authentication']) + } + } + }) + /** + * API : Brute Protection Lock Reset by API + */ + app.get([config.webPaths.apiPrefix+':auth/resetBruteProtection/:ke'], function (req,res){ + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + if(s.failedLoginAttempts[user.mail]){ + clearTimeout(s.failedLoginAttempts[user.mail].timeout) + delete(s.failedLoginAttempts[user.mail]) + } + res.end(s.prettyPrint({ok:true})) + }) + }) + /** + * Page : Montage - stand alone squished view with gridstackjs + */ + app.get([ + config.webPaths.apiPrefix+':auth/grid/:ke', + config.webPaths.apiPrefix+':auth/grid/:ke/:group', + config.webPaths.apiPrefix+':auth/cycle/:ke', + config.webPaths.apiPrefix+':auth/cycle/:ke/:group' + ], function(req,res) { + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + if(user.permissions.get_monitors==="0"){ + res.end(user.lang['Not Permitted']) + return + } + + req.params.protocol=req.protocol; + req.sql='SELECT * FROM Monitors WHERE mode!=? AND mode!=? AND ke=?';req.ar=['stop','idle',req.params.ke]; + if(!req.params.id){ + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + } + }else{ + if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ + req.sql+=' and mid=?';req.ar.push(req.params.id) + }else{ + res.end(user.lang['There are no monitors that you can view with this account.']); + return; + } + } + s.sqlQuery(req.sql,req.ar,function(err,r){ + if(req.params.group){ + var filteredByGroupCheck = {}; + var filteredByGroup = []; + r.forEach(function(v,n){ + var details = JSON.parse(r[n].details); + try{ + req.params.group.split('|').forEach(function(group){ + var groups = JSON.parse(details.groups); + if(groups.indexOf(group) > -1 && !filteredByGroupCheck[v.mid]){ + filteredByGroupCheck[v.mid] = true; + filteredByGroup.push(v) + } + }) + }catch(err){ + + } + }) + r = filteredByGroup; + } + r.forEach(function(v,n){ + if(s.group[v.ke]&&s.group[v.ke].mon[v.mid]&&s.group[v.ke].mon[v.mid].watch){ + r[n].currentlyWatching=Object.keys(s.group[v.ke].mon[v.mid].watch).length + } + r[n].subStream={} + var details = JSON.parse(r[n].details) + if(details.snap==='1'){ + r[n].subStream.jpeg = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg' + } + if(details.stream_channels&&details.stream_channels!==''){ + try{ + details.stream_channels=JSON.parse(details.stream_channels) + r[n].channels=[] + details.stream_channels.forEach(function(b,m){ + var streamURL + switch(b.stream_type){ + case'mjpeg': + streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+'/'+m + break; + case'hls': + streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+'/'+m+'/s.m3u8' + break; + case'h264': + streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+'/'+m + break; + case'flv': + streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+'/'+m+'/s.flv' + break; + case'mp4': + streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+'/'+m+'/s.mp4' + break; + } + r[n].channels.push(streamURL) + }) + }catch(err){ + s.userLog(req.params,{type:'Broken Monitor Object',msg:'Stream Channels Field is damaged. Skipping.'}) + } + } + }) + var page = config.renderPaths.grid + if(req.path.indexOf('/cycle/') > -1){ + page = config.renderPaths.cycle + } + res.render(page,{ + data:Object.assign(req.params,req.query), + baseUrl:req.protocol+'://'+req.hostname, + config:config, + lang:user.lang, + $user:user, + monitors:r, + originalURL:s.getOriginalUrl(req), + query:req.query + }); + }) + },res,req) + }); + /** + * API : Get TV Channels (Monitor Streams) json + */ + app.get([config.webPaths.apiPrefix+':auth/tvChannels/:ke',config.webPaths.apiPrefix+':auth/tvChannels/:ke/:id','/get.php'], function (req,res){ + req.ret={ok:false}; + if(req.query.username&&req.query.password){ + req.params.username = req.query.username + req.params.password = req.query.password + } + var output = ['h264','hls','mp4'] + if(req.query.output&&req.query.output!==''){ + output = req.query.output.split(',') + output.forEach(function(type,n){ + if(type==='ts'){ + output[n]='h264' + if(output.indexOf('hls')===-1){ + output.push('hls') + } + } + }) + } + var isM3u8 = false; + if(req.query.type==='m3u8'||req.query.type==='m3u_plus'){ + //is m3u8 + isM3u8 = true; + }else{ + res.setHeader('Content-Type', 'application/json'); + } + res.header("Access-Control-Allow-Origin",req.headers.origin); + req.fn=function(user){ + if(user.permissions.get_monitors==="0"){ + res.end(s.prettyPrint([])) + return + } + if(!req.params.ke){ + req.params.ke = user.ke; + } + if(req.query.id&&!req.params.id){ + req.params.id = req.query.id; + } + req.sql='SELECT * FROM Monitors WHERE mode!=? AND ke=?';req.ar=['stop',req.params.ke]; + if(!req.params.id){ + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + } + }else{ + if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ + req.sql+=' and mid=?';req.ar.push(req.params.id) + }else{ + res.end('[]'); + return; + } + } + s.sqlQuery(req.sql,req.ar,function(err,r){ + var tvChannelMonitors = []; + r.forEach(function(v,n){ + var buildStreamURL = function(channelRow,type,channelNumber){ + var streamURL + if(channelNumber){channelNumber = '/'+channelNumber}else{channelNumber=''} + switch(type){ + case'mjpeg': + streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+channelNumber + break; + case'hls': + streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+channelNumber+'/s.m3u8' + break; + case'h264': + streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+channelNumber + break; + case'flv': + streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+channelNumber+'/s.flv' + break; + case'mp4': + streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+channelNumber+'/s.ts' + break; + } + if(streamURL){ + if(!channelRow.streamsSortedByType[type]){ + channelRow.streamsSortedByType[type]=[] + } + channelRow.streamsSortedByType[type].push(streamURL) + channelRow.streams.push(streamURL) + } + return streamURL + } + var details = JSON.parse(r[n].details); + if(!details.tv_channel_id||details.tv_channel_id==='')details.tv_channel_id = 'temp_'+s.gid(5) + var channelRow = { + ke:v.ke, + mid:v.mid, + type:v.type, + groupTitle:details.tv_channel_group_title, + channel:details.tv_channel_id, + }; + if(details.snap==='1'){ + channelRow.snapshot = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg' + } + channelRow.streams=[] + channelRow.streamsSortedByType={} + buildStreamURL(channelRow,details.stream_type) + if(details.stream_channels&&details.stream_channels!==''){ + details.stream_channels=JSON.parse(details.stream_channels) + details.stream_channels.forEach(function(b,m){ + buildStreamURL(channelRow,b.stream_type,m.toString()) + }) + } + if(details.tv_channel==='1'){ + tvChannelMonitors.push(channelRow) + } + }) + if(isM3u8){ + var m3u8 = '#EXTM3U'+'\n' + tvChannelMonitors.forEach(function(channelRow,n){ + output.forEach(function(type){ + if(channelRow.streamsSortedByType[type]){ + if(req.query.type==='m3u_plus'){ + m3u8 +='#EXTINF-1 tvg-id="'+channelRow.mid+'" tvg-name="'+channelRow.channel+'" tvg-logo="'+req.protocol+'://'+req.headers.host+channelRow.snapshot+'" group-title="'+channelRow.groupTitle+'",'+channelRow.channel+'\n' + }else{ + m3u8 +='#EXTINF:-1,'+channelRow.channel+' ('+type.toUpperCase()+') \n' + } + m3u8 += req.protocol+'://'+req.headers.host+channelRow.streamsSortedByType[type][0]+'\n' + } + }) + }) + res.end(m3u8) + }else{ + if(tvChannelMonitors.length===1){tvChannelMonitors=tvChannelMonitors[0];} + res.end(s.prettyPrint(tvChannelMonitors)); + } + }) + } + s.auth(req.params,req.fn,res,req); + }); + /** + * API : Get Monitors + */ + app.get([config.webPaths.apiPrefix+':auth/monitor/:ke',config.webPaths.apiPrefix+':auth/monitor/:ke/:id'], function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + req.fn=function(user){ + if(user.permissions.get_monitors==="0"){ + res.end(s.prettyPrint([])) + return + } + req.sql='SELECT * FROM Monitors WHERE ke=?';req.ar=[req.params.ke]; + if(!req.params.id){ + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + } + }else{ + if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ + req.sql+=' and mid=?';req.ar.push(req.params.id) + }else{ + res.end('[]'); + return; + } + } + s.sqlQuery(req.sql,req.ar,function(err,r){ + r.forEach(function(v,n){ + if(s.group[v.ke] && s.group[v.ke].mon[v.mid]){ + r[n].currentlyWatching = Object.keys(s.group[v.ke].mon[v.mid].watch).length + r[n].currentCpuUsage = s.group[v.ke].mon[v.mid].currentCpuUsage + r[n].status = s.group[v.ke].mon[v.mid].monitorStatus + } + var buildStreamURL = function(type,channelNumber){ + var streamURL + if(channelNumber){channelNumber = '/'+channelNumber}else{channelNumber=''} + switch(type){ + case'mjpeg': + streamURL='/'+req.params.auth+'/mjpeg/'+v.ke+'/'+v.mid+channelNumber + break; + case'hls': + streamURL='/'+req.params.auth+'/hls/'+v.ke+'/'+v.mid+channelNumber+'/s.m3u8' + break; + case'h264': + streamURL='/'+req.params.auth+'/h264/'+v.ke+'/'+v.mid+channelNumber + break; + case'flv': + streamURL='/'+req.params.auth+'/flv/'+v.ke+'/'+v.mid+channelNumber+'/s.flv' + break; + case'mp4': + streamURL='/'+req.params.auth+'/mp4/'+v.ke+'/'+v.mid+channelNumber+'/s.mp4' + break; + } + if(streamURL){ + if(!r[n].streamsSortedByType[type]){ + r[n].streamsSortedByType[type]=[] + } + r[n].streamsSortedByType[type].push(streamURL) + r[n].streams.push(streamURL) + } + return streamURL + } + var details = JSON.parse(r[n].details); + if(!details.tv_channel_id||details.tv_channel_id==='')details.tv_channel_id = 'temp_'+s.gid(5) + if(details.snap==='1'){ + r[n].snapshot = '/'+req.params.auth+'/jpeg/'+v.ke+'/'+v.mid+'/s.jpg' + } + r[n].streams=[] + r[n].streamsSortedByType={} + buildStreamURL(details.stream_type) + if(details.stream_channels&&details.stream_channels!==''){ + details.stream_channels=JSON.parse(details.stream_channels) + details.stream_channels.forEach(function(b,m){ + buildStreamURL(b.stream_type,m.toString()) + }) + } + }) + if(r.length===1){r=r[0];} + res.end(s.prettyPrint(r)); + }) + } + s.auth(req.params,req.fn,res,req); + }); + /** + * API : Get Videos + */ + app.get([ + config.webPaths.apiPrefix+':auth/videos/:ke', + config.webPaths.apiPrefix+':auth/videos/:ke/:id', + config.webPaths.apiPrefix+':auth/cloudVideos/:ke', + config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id' + ], function (req,res){ + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + var hasRestrictions = user.details.sub && user.details.allmonitors !== '1' + if( + user.permissions.watch_videos==="0" || + hasRestrictions && (!user.details.video_view || user.details.video_view.indexOf(req.params.id)===-1) + ){ + res.end(s.prettyPrint([])) + return + } + var origURL = req.originalUrl.split('/') + var videoParam = origURL[origURL.indexOf(req.params.auth) + 1] + var videoSet = 'Videos' + switch(videoParam){ + case'cloudVideos': + videoSet = 'Cloud Videos' + break; + } + req.sql='SELECT * FROM `'+videoSet+'` WHERE ke=?';req.ar=[req.params.ke]; + req.count_sql='SELECT COUNT(*) FROM `'+videoSet+'` WHERE ke=?';req.count_ar=[req.params.ke]; + if(req.query.archived=='1'){ + req.sql+=' AND details LIKE \'%"archived":"1"\'' + req.count_sql+=' AND details LIKE \'%"archived":"1"\'' + } + if(!req.params.id){ + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + req.count_sql+=' AND ('+req.or.join(' OR ')+')' + } + }else{ + if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ + req.sql+=' and mid=?';req.ar.push(req.params.id) + req.count_sql+=' and mid=?';req.count_ar.push(req.params.id) + }else{ + res.end('[]'); + return; + } + } + if(req.query.start||req.query.end){ + if(req.query.start && req.query.start !== ''){ + req.query.start = s.stringToSqlTime(req.query.start) + } + if(req.query.end && req.query.end !== ''){ + req.query.end = s.stringToSqlTime(req.query.end) + } + if(!req.query.startOperator||req.query.startOperator==''){ + req.query.startOperator='>=' + } + if(!req.query.endOperator||req.query.endOperator==''){ + req.query.endOperator='<=' + } + var endIsStartTo + var theEndParameter = '`end`' + if(req.query.endIsStartTo){ + endIsStartTo = true + theEndParameter = '`time`' + } + switch(true){ + case(req.query.start&&req.query.start!==''&&req.query.end&&req.query.end!==''): + req.sql+=' AND `time` '+req.query.startOperator+' ? AND '+theEndParameter+' '+req.query.endOperator+' ?'; + req.count_sql+=' AND `time` '+req.query.startOperator+' ? AND '+theEndParameter+' '+req.query.endOperator+' ?'; + req.ar.push(req.query.start) + req.ar.push(req.query.end) + req.count_ar.push(req.query.start) + req.count_ar.push(req.query.end) + break; + case(req.query.start&&req.query.start!==''): + req.sql+=' AND `time` '+req.query.startOperator+' ?'; + req.count_sql+=' AND `time` '+req.query.startOperator+' ?'; + req.ar.push(req.query.start) + req.count_ar.push(req.query.start) + break; + case(req.query.end&&req.query.end!==''): + req.sql+=' AND '+theEndParameter+' '+req.query.endOperator+' ?'; + req.count_sql+=' AND '+theEndParameter+' '+req.query.endOperator+' ?'; + req.ar.push(req.query.end) + req.count_ar.push(req.query.end) + break; + } + } + req.sql+=' ORDER BY `time` DESC'; + if(!req.query.limit||req.query.limit==''){ + req.query.limit='100' + } + if(req.query.limit!=='0'){ + req.sql+=' LIMIT '+req.query.limit + } + s.sqlQuery(req.sql,req.ar,function(err,r){ + if(!r){ + res.end(s.prettyPrint({total:0,limit:req.query.limit,skip:0,videos:[]})); + return + } + s.sqlQuery(req.count_sql,req.count_ar,function(err,count){ + s.buildVideoLinks(r,{ + auth : req.params.auth, + videoParam : videoParam, + hideRemote : config.hideCloudSaveUrls + }) + if(req.query.limit.indexOf(',')>-1){ + req.skip=parseInt(req.query.limit.split(',')[0]) + req.query.limit=parseInt(req.query.limit.split(',')[1]) + }else{ + req.skip=0 + req.query.limit=parseInt(req.query.limit) + } + res.end(s.prettyPrint({isUTC:config.useUTC,total:count[0]['COUNT(*)'],limit:req.query.limit,skip:req.skip,videos:r,endIsStartTo:endIsStartTo})); + }) + }) + },res,req); + }); + /** + * API : Get Events + */ + app.get([config.webPaths.apiPrefix+':auth/events/:ke',config.webPaths.apiPrefix+':auth/events/:ke/:id',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit/:start',config.webPaths.apiPrefix+':auth/events/:ke/:id/:limit/:start/:end'], function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_view.indexOf(req.params.id)===-1){ + res.end(s.prettyPrint([])) + return + } + req.sql='SELECT * FROM Events WHERE ke=?';req.ar=[req.params.ke]; + if(!req.params.id){ + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + } + }else{ + if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1){ + req.sql+=' and mid=?';req.ar.push(req.params.id) + }else{ + res.end('[]'); + return; + } + } + if(req.params.start&&req.params.start!==''){ + req.params.start = s.stringToSqlTime(req.params.start) + if(req.params.end&&req.params.end!==''){ + req.params.end = s.stringToSqlTime(req.params.end) + req.sql+=' AND `time` >= ? AND `time` <= ?'; + req.ar.push(decodeURIComponent(req.params.start)) + req.ar.push(decodeURIComponent(req.params.end)) + }else{ + req.sql+=' AND `time` >= ?'; + req.ar.push(decodeURIComponent(req.params.start)) + } + } + if(!req.params.limit||req.params.limit==''){req.params.limit=100} + req.sql+=' ORDER BY `time` DESC LIMIT '+req.params.limit+''; + s.sqlQuery(req.sql,req.ar,function(err,r){ + if(err){ + err.sql=req.sql; + res.end(s.prettyPrint(err)); + return + } + if(!r){r=[]} + r.forEach(function(v,n){ + r[n].details=JSON.parse(v.details); + }) + res.end(s.prettyPrint(r)); + }) + },res,req); + }); + /** + * API : Get Logs + */ + app.get([config.webPaths.apiPrefix+':auth/logs/:ke',config.webPaths.apiPrefix+':auth/logs/:ke/:id'], function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + if(user.permissions.get_logs==="0" || user.details.sub && user.details.view_logs !== '1'){ + res.end(s.prettyPrint([])) + return + } + req.sql='SELECT * FROM Logs WHERE ke=?';req.ar=[req.params.ke]; + if(!req.params.id){ + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + } + }else{ + if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1||req.params.id.indexOf('$')>-1){ + req.sql+=' and mid=?';req.ar.push(req.params.id) + }else{ + res.end('[]'); + return; + } + } + if(req.query.start||req.query.end){ + if(!req.query.startOperator||req.query.startOperator==''){ + req.query.startOperator='>=' + } + if(!req.query.endOperator||req.query.endOperator==''){ + req.query.endOperator='<=' + } + if(req.query.start && req.query.start !== '' && req.query.end && req.query.end !== ''){ + req.query.start = s.stringToSqlTime(req.query.start) + req.query.end = s.stringToSqlTime(req.query.end) + req.sql+=' AND `time` '+req.query.startOperator+' ? AND `time` '+req.query.endOperator+' ?'; + req.ar.push(req.query.start) + req.ar.push(req.query.end) + }else if(req.query.start && req.query.start !== ''){ + req.query.start = s.stringToSqlTime(req.query.start) + req.sql+=' AND `time` '+req.query.startOperator+' ?'; + req.ar.push(req.query.start) + } + } + if(!req.query.limit||req.query.limit==''){req.query.limit=50} + req.sql+=' ORDER BY `time` DESC LIMIT '+req.query.limit+''; + s.sqlQuery(req.sql,req.ar,function(err,r){ + if(err){ + err.sql=req.sql; + res.end(s.prettyPrint(err)); + return + } + if(!r){r=[]} + r.forEach(function(v,n){ + r[n].info=JSON.parse(v.info) + }) + res.end(s.prettyPrint(r)); + }) + },res,req); + }) + /** + * API : Get Monitors Online + */ + app.get(config.webPaths.apiPrefix+':auth/smonitor/:ke', function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + req.fn=function(user){ + if(user.permissions.get_monitors==="0"){ + res.end(s.prettyPrint([])) + return + } + req.sql='SELECT * FROM Monitors WHERE ke=?';req.ar=[req.params.ke]; + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + } + s.sqlQuery(req.sql,req.ar,function(err,r){ + if(r&&r[0]){ + req.ar=[]; + r.forEach(function(v){ + if(s.group[req.params.ke]&&s.group[req.params.ke].mon[v.mid]&&s.group[req.params.ke].mon[v.mid].isStarted === true){ + req.ar.push(v) + } + }) + }else{ + req.ar=[]; + } + res.end(s.prettyPrint(req.ar)); + }) + } + s.auth(req.params,req.fn,res,req); + }); + /** + * API : Monitor Mode Controller + */ + app.get([config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f',config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f/:ff',config.webPaths.apiPrefix+':auth/monitor/:ke/:id/:f/:ff/:fff'], function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + if(user.permissions.control_monitors==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitor_edit.indexOf(req.params.id)===-1){ + res.end(user.lang['Not Permitted']) + return + } + if(req.params.f===''){req.ret.msg=user.lang.monitorGetText1;res.end(s.prettyPrint(req.ret));return} + if(req.params.f!=='stop'&&req.params.f!=='start'&&req.params.f!=='record'){ + req.ret.msg='Mode not recognized.'; + res.end(s.prettyPrint(req.ret)); + return; + } + s.sqlQuery('SELECT * FROM Monitors WHERE ke=? AND mid=?',[req.params.ke,req.params.id],function(err,r){ + if(r&&r[0]){ + r=r[0]; + if(req.query.reset==='1'||(s.group[r.ke]&&s.group[r.ke].mon_conf[r.mid].mode!==req.params.f)||req.query.fps&&(!s.group[r.ke].mon[r.mid].currentState||!s.group[r.ke].mon[r.mid].currentState.trigger_on)){ + if(req.query.reset!=='1'||!s.group[r.ke].mon[r.mid].trigger_timer){ + if(!s.group[r.ke].mon[r.mid].currentState)s.group[r.ke].mon[r.mid].currentState={} + s.group[r.ke].mon[r.mid].currentState.mode=r.mode.toString() + s.group[r.ke].mon[r.mid].currentState.fps=r.fps.toString() + if(!s.group[r.ke].mon[r.mid].currentState.trigger_on){ + s.group[r.ke].mon[r.mid].currentState.trigger_on=true + }else{ + s.group[r.ke].mon[r.mid].currentState.trigger_on=false + } + r.mode=req.params.f; + try{r.details=JSON.parse(r.details);}catch(er){} + if(req.query.fps){ + r.fps=parseFloat(r.details.detector_trigger_record_fps) + s.group[r.ke].mon[r.mid].currentState.detector_trigger_record_fps=r.fps + } + r.id=r.mid; + s.sqlQuery('UPDATE Monitors SET mode=? WHERE ke=? AND mid=?',[r.mode,r.ke,r.mid]); + s.group[r.ke].mon_conf[r.mid]=r; + s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'GRP_'+r.ke); + s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'STR_'+r.ke); + s.camera('stop',s.cleanMonitorObject(r)); + if(req.params.f!=='stop'){ + s.camera(req.params.f,s.cleanMonitorObject(r)); + } + req.ret.msg=user.lang['Monitor mode changed']+' : '+req.params.f; + }else{ + req.ret.msg=user.lang['Reset Timer']; + } + req.ret.cmd_at=s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss'); + req.ret.ok=true; + if(req.params.ff&&req.params.f!=='stop'){ + req.params.ff=parseFloat(req.params.ff); + clearTimeout(s.group[r.ke].mon[r.mid].trigger_timer) + switch(req.params.fff){ + case'day':case'days': + req.timeout=req.params.ff*1000*60*60*24 + break; + case'hr':case'hour':case'hours': + req.timeout=req.params.ff*1000*60*60 + break; + case'min':case'minute':case'minutes': + req.timeout=req.params.ff*1000*60 + break; + default://seconds + req.timeout=req.params.ff*1000 + break; + } + s.group[r.ke].mon[r.mid].trigger_timer=setTimeout(function(){ + delete(s.group[r.ke].mon[r.mid].trigger_timer) + s.sqlQuery('UPDATE Monitors SET mode=? WHERE ke=? AND mid=?',[s.group[r.ke].mon[r.mid].currentState.mode,r.ke,r.mid]); + r.neglectTriggerTimer=1; + r.mode=s.group[r.ke].mon[r.mid].currentState.mode; + r.fps=s.group[r.ke].mon[r.mid].currentState.fps; + s.camera('stop',s.cleanMonitorObject(r),function(){ + if(s.group[r.ke].mon[r.mid].currentState.mode!=='stop'){ + s.camera(s.group[r.ke].mon[r.mid].currentState.mode,s.cleanMonitorObject(r)); + } + s.group[r.ke].mon_conf[r.mid]=r; + }); + s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'GRP_'+r.ke); + s.tx({f:'monitor_edit',mid:r.mid,ke:r.ke,mon:r},'STR_'+r.ke); + },req.timeout); + // req.ret.end_at=s.formattedTime(new Date,'YYYY-MM-DD HH:mm:ss').add(req.timeout,'milliseconds'); + } + }else{ + req.ret.msg=user.lang['Monitor mode is already']+' : '+req.params.f; + } + }else{ + req.ret.msg=user.lang['Monitor or Key does not exist.']; + } + res.end(s.prettyPrint(req.ret)); + }) + },res,req); + }) + /** + * API : Get fileBin files + */ + app.get([config.webPaths.apiPrefix+':auth/fileBin/:ke',config.webPaths.apiPrefix+':auth/fileBin/:ke/:id'],function (req,res){ + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + req.fn=function(user){ + req.sql='SELECT * FROM Files WHERE ke=?';req.ar=[req.params.ke]; + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + }else{ + if(req.params.id&&(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1)){ + req.sql+=' and mid=?';req.ar.push(req.params.id) + } + } + s.sqlQuery(req.sql,req.ar,function(err,r){ + if(!r){ + r=[] + }else{ + r.forEach(function(v){ + v.details=JSON.parse(v.details) + v.href='/'+req.params.auth+'/fileBin/'+req.params.ke+'/'+req.params.id+'/'+v.details.year+'/'+v.details.month+'/'+v.details.day+'/'+v.name; + }) + } + res.end(s.prettyPrint(r)); + }) + } + s.auth(req.params,req.fn,res,req); + }); + /** + * API : Get fileBin file + */ + app.get(config.webPaths.apiPrefix+':auth/fileBin/:ke/:id/:year/:month/:day/:file', function (req,res){ + res.header("Access-Control-Allow-Origin",req.headers.origin); + req.fn=function(user){ + req.failed=function(){ + res.end(user.lang['File Not Found']) + } + if (!s.group[req.params.ke].fileBin[req.params.id+'/'+req.params.file]){ + s.sqlQuery('SELECT * FROM Files WHERE ke=? AND mid=? AND name=?',[req.params.ke,req.params.id,req.params.file],function(err,r){ + if(r&&r[0]){ + r=r[0] + r.details=JSON.parse(r.details) + req.dir=s.dir.fileBin+req.params.ke+'/'+req.params.id+'/'+r.details.year+'/'+r.details.month+'/'+r.details.day+'/'+req.params.file; + if(fs.existsSync(req.dir)){ + res.on('finish',function(){res.end();}); + fs.createReadStream(req.dir).pipe(res); + }else{ + req.failed() + } + }else{ + req.failed() + } + }) + }else{ + res.end(user.lang['Please Wait for Completion']) + } + } + s.auth(req.params,req.fn,res,req); + }); + /** + * API : Zip Videos and Get Link from fileBin + */ + app.get(config.webPaths.apiPrefix+':auth/zipVideos/:ke', function (req,res){ + res.header("Access-Control-Allow-Origin",req.headers.origin); + var failed = function(resp){ + res.setHeader('Content-Type', 'application/json'); + res.end(s.prettyPrint(resp)) + } + if(req.query.videos && req.query.videos !== ''){ + s.auth(req.params,function(user){ + var videosSelected = JSON.parse(req.query.videos) + var where = [] + var values = [] + videosSelected.forEach(function(video){ + where.push("(ke=? AND mid=? AND `time`=?)") + if(!video.ke)video.ke = req.params.ke + values.push(video.ke) + values.push(video.mid) + var time = s.nameToTime(video.filename) + if(req.query.isUTC === 'true'){ + time = s.utcToLocal(time) + } + time = new Date(time) + values.push(time) + }) + s.sqlQuery('SELECT * FROM Videos WHERE '+where.join(' OR '),values,function(err,r){ + var resp = {ok:false} + if(r && r[0]){ + resp.ok = true + var zipDownload = null + var tempFiles = [] + var fileId = s.gid() + var fileBinDir = s.dir.fileBin+req.params.ke+'/' + var tempScript = s.dir.streams+req.params.ke+'/'+fileId+'.sh' + var zippedFilename = s.formattedTime()+'-'+fileId+'-Shinobi_Recordings.zip' + var zippedFile = fileBinDir+zippedFilename + var script = 'cd '+fileBinDir+' && zip -9 -r '+zippedFile + res.on('close', () => { + if(zipDownload && zipDownload.destroy){ + zipDownload.destroy() + } + fs.unlink(zippedFile); + }) + if(!fs.existsSync(fileBinDir)){ + fs.mkdirSync(fileBinDir); + } + r.forEach(function(video){ + timeFormatted = s.formattedTime(video.time) + video.filename = timeFormatted+'.'+video.ext + var dir = s.getVideoDirectory(video)+video.filename + var tempVideoFile = timeFormatted+' - '+video.mid+'.'+video.ext + fs.writeFileSync(fileBinDir+tempVideoFile, fs.readFileSync(dir)) + tempFiles.push(fileBinDir+tempVideoFile) + script += ' "'+tempVideoFile+'"' + }) + fs.writeFileSync(tempScript,script,'utf8') + var zipCreate = spawn('sh',(tempScript).split(' '),{detached: true}) + zipCreate.stderr.on('data',function(data){ + s.userLog({ke:req.params.ke,mid:'$USER'},{title:'Zip Create Error',msg:data.toString()}) + }) + zipCreate.on('exit',function(data){ + fs.unlinkSync(tempScript) + tempFiles.forEach(function(file){ + fs.unlink(file,function(){}) + }) + res.setHeader('Content-Disposition', 'attachment; filename="'+zippedFilename+'"') + var zipDownload = fs.createReadStream(zippedFile) + zipDownload.pipe(res) + zipDownload.on('error', function (error) { + s.userLog({ke:req.params.ke,mid:'$USER'},{title:'Zip Download Error',msg:error.toString()}) + if(zipDownload && zipDownload.destroy){ + zipDownload.destroy() + } + }); + zipDownload.on('close', function () { + res.end() + zipDownload.destroy(); + fs.unlinkSync(zippedFile); + }); + }) + }else{ + failed({ok:false,msg:'No Videos Found'}) + } + }) + },res,req); + }else{ + failed({ok:false,msg:'"videos" query variable is missing from request.'}) + } + }); + /** + * API : Get Cloud Video File (proxy) + */ + app.get(config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file', function (req,res){ + s.auth(req.params,function(user){ + if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ + res.end(user.lang['Not Permitted']) + return + } + var time = s.nameToTime(req.params.file) + if(req.query.isUTC === 'true'){ + time = s.utcToLocal(time) + } + time = new Date(time) + s.sqlQuery('SELECT * FROM `Cloud Videos` WHERE ke=? AND mid=? AND `time`=? LIMIT 1',[req.params.ke,req.params.id,time],function(err,r){ + if(r&&r[0]){ + r = r[0] + req.pipe(request(r.href)).pipe(res) + }else{ + res.end(user.lang['File Not Found in Database']) + } + }) + },res,req); + }); + /** + * API : Get Video File + */ + app.get(config.webPaths.apiPrefix+':auth/videos/:ke/:id/:file', function (req,res){ + s.auth(req.params,function(user){ + if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors.indexOf(req.params.id)===-1){ + res.end(user.lang['Not Permitted']) + return + } + var time = s.nameToTime(req.params.file) + if(req.query.isUTC === 'true'){ + time = s.utcToLocal(time) + } + time = new Date(time) + s.sqlQuery('SELECT * FROM Videos WHERE ke=? AND mid=? AND `time`=? LIMIT 1',[req.params.ke,req.params.id,time],function(err,r){ + if(r&&r[0]){ + req.dir=s.getVideoDirectory(r[0])+req.params.file + if (fs.existsSync(req.dir)){ + req.ext=req.params.file.split('.')[1]; + var total = fs.statSync(req.dir).size; + if (req.headers['range']) { + var range = req.headers.range; + var parts = range.replace(/bytes=/, "").split("-"); + var partialstart = parts[0]; + var partialend = parts[1]; + + var start = parseInt(partialstart, 10); + var end = partialend ? parseInt(partialend, 10) : total-1; + var chunksize = (end-start)+1; + var file = fs.createReadStream(req.dir, {start: start, end: end}); + req.headerWrite={ 'Content-Range': 'bytes ' + start + '-' + end + '/' + total, 'Accept-Ranges': 'bytes', 'Content-Length': chunksize, 'Content-Type': 'video/'+req.ext } + req.writeCode=206 + } else { + req.headerWrite={ 'Content-Length': total, 'Content-Type': 'video/'+req.ext}; + var file=fs.createReadStream(req.dir) + req.writeCode=200 + } + if(req.query.downloadName){ + req.headerWrite['content-disposition']='attachment; filename="'+req.query.downloadName+'"'; + } + res.writeHead(req.writeCode,req.headerWrite); + file.on('close',function(){ + res.end(); + }) + file.pipe(res); + }else{ + res.end(user.lang['File Not Found in Filesystem']) + } + }else{ + res.end(user.lang['File Not Found in Database']) + } + }) + },res,req); + }); + /** + * API : Motion Trigger via GET request + */ + app.get(config.webPaths.apiPrefix+':auth/motion/:ke/:id', function (req,res){ + s.auth(req.params,function(user){ + if(req.query.data){ + try{ + var d={id:req.params.id,ke:req.params.ke,details:JSON.parse(req.query.data)}; + }catch(err){ + res.end('Data Broken',err); + return; + } + }else{ + res.end('No Data'); + return; + } + if(!d.ke||!d.id||!s.group[d.ke]){ + res.end(user.lang['No Group with this key exists']); + return; + } + s.triggerEvent(d) + res.end(user.lang['Trigger Successful']) + },res,req); + }) + /** + * API : WebHook Tester + */ + app.get(config.webPaths.apiPrefix+':auth/hookTester/:ke/:id', function (req,res){ + res.setHeader('Content-Type', 'application/json'); + s.auth(req.params,function(user){ + s.userLog(req.params,{type:'Test',msg:'Hook Test'}) + res.end(s.prettyPrint({ok:true})) + },res,req); + }) + /** + * API : Camera PTZ Controller + */ + app.get(config.webPaths.apiPrefix+':auth/control/:ke/:id/:direction', function (req,res){ + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + s.cameraControl(req.params,function(resp){ + res.end(s.prettyPrint(resp)) + }); + },res,req); + }) + /** + * API : Modify Video File + */ + app.get([ + config.webPaths.apiPrefix+':auth/videos/:ke/:id/:file/:mode', + config.webPaths.apiPrefix+':auth/videos/:ke/:id/:file/:mode/:f', + config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file/:mode', + config.webPaths.apiPrefix+':auth/cloudVideos/:ke/:id/:file/:mode/:f' + ], function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + if(user.permissions.watch_videos==="0"||user.details.sub&&user.details.allmonitors!=='1'&&user.details.video_delete.indexOf(req.params.id)===-1){ + res.end(user.lang['Not Permitted']) + return + } + var time = s.nameToTime(req.params.file) + if(req.query.isUTC === 'true'){ + time = s.utcToLocal(time) + } + time = new Date(time) + var origURL = req.originalUrl.split('/') + var videoParam = origURL[origURL.indexOf(req.params.auth) + 1] + var videoSet = 'Videos' + switch(videoParam){ + case'cloudVideos': + videoSet = 'Cloud Videos' + break; + } + req.sql='SELECT * FROM `'+videoSet+'` WHERE ke=? AND mid=? AND `time`=?'; + req.ar=[req.params.ke,req.params.id,time]; + s.sqlQuery(req.sql,req.ar,function(err,r){ + if(r&&r[0]){ + r=r[0];r.filename=s.formattedTime(r.time)+'.'+r.ext; + switch(req.params.mode){ + case'fix': + req.ret.ok=true; + s.video('fix',r) + break; + case'status': + r.f = 'video_edit' + switch(videoParam){ + case'cloudVideos': + r.f += '_cloud' + break; + } + r.status = parseInt(req.params.f) + if(isNaN(req.params.f)||req.params.f===0){ + req.ret.msg='Not a valid value.'; + }else{ + req.ret.ok=true; + s.sqlQuery('UPDATE `'+videoSet+'` SET status=? WHERE ke=? AND mid=? AND `time`=?',[req.params.f,req.params.ke,req.params.id,time]) + s.tx(r,'GRP_'+r.ke); + } + break; + case'delete': + req.ret.ok=true; + switch(videoParam){ + case'cloudVideos': + s.deleteVideoFromCloud(r) + break; + default: + s.deleteVideo(r) + break; + } + break; + default: + req.ret.msg=user.lang.modifyVideoText1; + break; + } + }else{ + req.ret.msg=user.lang['No such file']; + } + res.end(s.prettyPrint(req.ret)); + }) + },res,req); + }) + /** + * API : Stream In to push data to ffmpeg by HTTP + */ + app.all(['/streamIn/:ke/:id','/streamIn/:ke/:id/:feed'], function (req, res) { + var checkOrigin = function(search){return req.headers.host.indexOf(search)>-1} + if(checkOrigin('127.0.0.1')){ + if(!req.params.feed){req.params.feed='1'} + if(!s.group[req.params.ke].mon[req.params.id].streamIn[req.params.feed]){ + s.group[req.params.ke].mon[req.params.id].streamIn[req.params.feed] = new events.EventEmitter().setMaxListeners(0) + } + //req.params.feed = Feed Number + res.connection.setTimeout(0); + req.on('data', function(buffer){ + s.group[req.params.ke].mon[req.params.id].streamIn[req.params.feed].emit('data',buffer) + }); + req.on('end',function(){ + // console.log('streamIn closed',req.params); + }); + }else{ + res.end('Local connection is only allowed.') + } + }) + /** + * API : FFprobe + */ + app.get(config.webPaths.apiPrefix+':auth/probe/:ke',function (req,res){ + req.ret={ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + switch(req.query.action){ + // case'stop': + // exec('kill -9 '+user.ffprobe.pid,{detatched: true}) + // break; + default: + if(!req.query.url){ + req.ret.error = 'Missing URL' + res.end(s.prettyPrint(req.ret)); + return + } + if(user.ffprobe){ + req.ret.error = 'Account is already probing' + res.end(s.prettyPrint(req.ret)); + return + } + user.ffprobe=1; + if(req.query.flags==='default'){ + req.query.flags = '-v quiet -print_format json -show_format -show_streams' + }else{ + if(!req.query.flags){ + req.query.flags = '' + } + } + req.probeCommand = s.splitForFFPMEG(req.query.flags+' -i '+req.query.url).join(' ') + exec('ffprobe '+req.probeCommand+' | echo ',function(err,stdout,stderr){ + delete(user.ffprobe) + if(err){ + req.ret.error=(err) + }else{ + req.ret.ok=true + req.ret.result = stdout+stderr + } + req.ret.probe = req.probeCommand + res.end(s.prettyPrint(req.ret)); + }) + break; + } + },res,req); + }) + /** + * API : ONVIF Method Controller + */ + app.all([config.webPaths.apiPrefix+':auth/onvif/:ke/:id/:action',config.webPaths.apiPrefix+':auth/onvif/:ke/:id/:service/:action'],function (req,res){ + var response = {ok:false}; + res.setHeader('Content-Type', 'application/json'); + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + var errorMessage = function(msg,error){ + response.ok = false + response.msg = msg + response.error = error + res.end(s.prettyPrint(response)) + } + var actionCallback = function(onvifActionResponse){ + response.ok = true + if(onvifActionResponse.data){ + response.responseFromDevice = onvifActionResponse.data + }else{ + response.responseFromDevice = onvifActionResponse + } + if(onvifActionResponse.soap)response.soap = onvifActionResponse.soap + res.end(s.prettyPrint(response)) + } + var isEmpty = function(obj) { + for(var key in obj) { + if(obj.hasOwnProperty(key)) + return false; + } + return true; + } + var doAction = function(Camera){ + var completeAction = function(command){ + if(command.then){ + command.then(actionCallback).catch(function(error){ + errorMessage('Device responded with an error',error) + }) + }else if(command){ + response.ok = true + response.repsonseFromDevice = command + res.end(s.prettyPrint(response)) + }else{ + response.error = 'Big Errors, Please report it to Shinobi Development' + res.end(s.prettyPrint(response)) + } + } + var action + if(req.params.service){ + if(Camera.services[req.params.service] === undefined){ + return errorMessage('This is not an available service. Please use one of the following : '+Object.keys(Camera.services).join(', ')) + } + if(Camera.services[req.params.service] === null){ + return errorMessage('This service is not activated. Maybe you are not connected through ONVIF. You can test by attempting to use the "Control" feature with ONVIF in Shinobi.') + } + action = Camera.services[req.params.service][req.params.action] + }else{ + action = Camera[req.params.action] + } + if(!action || typeof action !== 'function'){ + errorMessage(req.params.action+' is not an available ONVIF function. See https://github.com/futomi/node-onvif for functions.') + }else{ + var argNames = s.getFunctionParamNames(action) + var options + var command + if(argNames[0] === 'options' || argNames[0] === 'params'){ + options = {} + if(req.query.options){ + var jsonRevokedText = 'JSON not formated correctly' + try{ + options = JSON.parse(req.query.options) + }catch(err){ + return errorMessage(jsonRevokedText,err) + } + }else if(req.body.options){ + try{ + options = JSON.parse(req.body.options) + }catch(err){ + return errorMessage(jsonRevokedText,err) + } + }else if(req.query.params){ + try{ + options = JSON.parse(req.query.params) + }catch(err){ + return errorMessage(jsonRevokedText,err) + } + }else if(req.body.params){ + try{ + options = JSON.parse(req.body.params) + }catch(err){ + return errorMessage(jsonRevokedText,err) + } + } + } + if(req.params.service){ + command = Camera.services[req.params.service][req.params.action](options) + }else{ + command = Camera[req.params.action](options) + } + completeAction(command) + } + } + if(!s.group[req.params.ke].mon[req.params.id].onvifConnection){ + //prepeare onvif connection + var controlURL + var monitorConfig = s.group[req.params.ke].mon_conf[req.params.id] + if(!monitorConfig.details.control_base_url||monitorConfig.details.control_base_url===''){ + controlURL = s.buildMonitorUrl(monitorConfig, true) + }else{ + controlURL = monitorConfig.details.control_base_url + } + var controlURLOptions = s.cameraControlOptionsFromUrl(controlURL,monitorConfig) + //create onvif connection + s.group[req.params.ke].mon[req.params.id].onvifConnection = new onvif.OnvifDevice({ + xaddr : 'http://' + controlURLOptions.host + ':' + controlURLOptions.port + '/onvif/device_service', + user : controlURLOptions.username, + pass : controlURLOptions.password + }) + var device = s.group[req.params.ke].mon[req.params.id].onvifConnection + device.init().then((info) => { + if(info)doAction(device) + }).catch(function(error){ + return errorMessage('Device responded with an error',error) + }) + }else{ + doAction(s.group[req.params.ke].mon[req.params.id].onvifConnection) + } + },res,req); + }) +} diff --git a/libs/webServerStreamPaths.js b/libs/webServerStreamPaths.js new file mode 100644 index 0000000..7ac6509 --- /dev/null +++ b/libs/webServerStreamPaths.js @@ -0,0 +1,358 @@ +var express = require('express'); +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; +var httpProxy = require('http-proxy'); +var proxy = httpProxy.createProxyServer({}) +var ejs = require('ejs'); +var CircularJSON = require('circular-json'); +module.exports = function(s,config,lang,app){ + /** + * Page : Get Embed Stream + */ + app.get([config.webPaths.apiPrefix+':auth/embed/:ke/:id',config.webPaths.apiPrefix+':auth/embed/:ke/:id/:addon'], function (req,res){ + res.header("Access-Control-Allow-Origin",req.headers.origin); + req.params.protocol=req.protocol; + s.auth(req.params,function(user){ + 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 + } + if(s.group[req.params.ke]&&s.group[req.params.ke].mon[req.params.id]){ + if(s.group[req.params.ke].mon[req.params.id].isStarted === true){ + req.params.uid=user.uid; + res.render(config.renderPaths.embed,{data:req.params,baseUrl:req.protocol+'://'+req.hostname,config:config,lang:user.lang,mon:CircularJSON.parse(CircularJSON.stringify(s.group[req.params.ke].mon_conf[req.params.id])),originalURL:s.getOriginalUrl(req)}); + res.end() + }else{ + res.end(user.lang['Cannot watch a monitor that isn\'t running.']) + } + }else{ + res.end(user.lang['No Monitor Exists with this ID.']) + } + },res,req); + }); + /** + * API : Get Poseidon MP4 Stream + */ + app.get([config.webPaths.apiPrefix+':auth/mp4/:ke/:id/:channel/s.mp4',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/s.mp4',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/:channel/s.ts',config.webPaths.apiPrefix+':auth/mp4/:ke/:id/s.ts'], function (req, res) { + s.auth(req.params,function(user){ + if(!s.group[req.params.ke] || !s.group[req.params.ke].mon[req.params.id]){ + res.status(404); + res.end('404 : Monitor not found'); + return + } + s.checkChildProxy(req.params,function(){ + var Channel = 'MAIN' + if(req.params.channel){ + Channel = parseInt(req.params.channel)+config.pipeAddition + } + var mp4frag = s.group[req.params.ke].mon[req.params.id].mp4frag[Channel]; + var errorMessage = 'MP4 Stream is not enabled' + if(!mp4frag){ + res.status(503); + res.end('503 : initialization : '+errorMessage); + }else{ + var init = mp4frag.initialization; + if (!init) { + res.status(503); + res.end('404 : Not Found : '+errorMessage); + } else { + res.locals.mp4frag = mp4frag + res.set('Access-Control-Allow-Origin', '*') + res.set('Connection', 'close') + res.set('Cache-Control', 'private, no-cache, no-store, must-revalidate') + res.set('Expires', '-1') + res.set('Pragma', 'no-cache') + res.set('Content-Type', 'video/mp4') + res.status(200); + res.write(init); + mp4frag.pipe(res); + 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'] + }) + res.on('close', () => { + try{ + mp4frag.unpipe(res) + }catch(err){} + 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 and Page : Get MJPEG Stream or Page + * @param {string} full - if `true` page will load the MJPEG iframe page + */ + app.get([config.webPaths.apiPrefix+':auth/mjpeg/:ke/:id',config.webPaths.apiPrefix+':auth/mjpeg/:ke/:id/:channel'], function(req,res) { + res.header("Access-Control-Allow-Origin",req.headers.origin); + if(req.query.full=='true'){ + res.render(config.renderPaths.mjpeg,{url:'/'+req.params.auth+'/mjpeg/'+req.params.ke+'/'+req.params.id,originalURL:s.getOriginalUrl(req)}); + res.end() + }else{ + s.auth(req.params,function(user){ + s.checkChildProxy(req.params,function(){ + if(s.group[req.params.ke]&&s.group[req.params.ke].mon[req.params.id]){ + 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 + } + + var Emitter + if(!req.params.channel){ + Emitter = s.group[req.params.ke].mon[req.params.id].emitter + }else{ + Emitter = s.group[req.params.ke].mon[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition] + } + res.writeHead(200, { + 'Content-Type': 'multipart/x-mixed-replace; boundary=shinobi', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'Pragma': 'no-cache' + }); + var contentWriter,content = fs.readFileSync(config.defaultMjpeg,'binary'); + res.write("--shinobi\r\n"); + res.write("Content-Type: image/jpeg\r\n"); + res.write("Content-Length: " + content.length + "\r\n"); + res.write("\r\n"); + res.write(content,'binary'); + res.write("\r\n"); + 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'] + }) + Emitter.on('data',contentWriter=function(d){ + content = d; + res.write(content,'binary'); + }) + 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'] + }) + }); + }else{ + res.end(); + } + },res,req); + },res,req); + } + }); + /** + * 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){ + res.header("Access-Control-Allow-Origin",req.headers.origin); + req.fn=function(user){ + s.checkChildProxy(req.params,function(){ + 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; + }else{ + req.dir+=req.params.file; + } + res.on('finish',function(){res.end();}); + if (fs.existsSync(req.dir)){ + fs.createReadStream(req.dir).pipe(res); + }else{ + res.end(lang['File Not Found']) + } + },res,req) + } + s.auth(req.params,req.fn,res,req); + }) + /** + * API : Get JPEG Snapshot + */ + app.get(config.webPaths.apiPrefix+':auth/jpeg/:ke/:id/s.jpg', function(req,res){ + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + s.checkChildProxy(req.params,function(){ + if(user.details.sub&&user.details.allmonitors!=='1'&&user.details.monitors&&user.details.monitors.indexOf(req.params.id)===-1){ + res.end(user.lang['Not Permitted']) + return + } + req.dir=s.dir.streams+req.params.ke+'/'+req.params.id+'/s.jpg'; + res.writeHead(200, { + 'Content-Type': 'image/jpeg', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache' + }); + res.on('finish',function(){res.end();}); + if (fs.existsSync(req.dir)){ + fs.createReadStream(req.dir).pipe(res); + }else{ + fs.createReadStream(config.defaultMjpeg).pipe(res); + } + },res,req); + },res,req); + }); + /** + * API : Get FLV Stream + */ + app.get([config.webPaths.apiPrefix+':auth/flv/:ke/:id/s.flv',config.webPaths.apiPrefix+':auth/flv/:ke/:id/:channel/s.flv'], function(req,res) { + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + s.checkChildProxy(req.params,function(){ + var Emitter,chunkChannel + if(!req.params.channel){ + Emitter = s.group[req.params.ke].mon[req.params.id].emitter + chunkChannel = 'MAIN' + }else{ + Emitter = s.group[req.params.ke].mon[req.params.id].emitterChannel[parseInt(req.params.channel)+config.pipeAddition] + chunkChannel = parseInt(req.params.channel)+config.pipeAddition + } + if(s.group[req.params.ke].mon[req.params.id].firstStreamChunk[chunkChannel]){ + //variable name of contentWriter + var contentWriter + //set headers + res.setHeader('Content-Type', 'video/x-flv'); + res.setHeader('Access-Control-Allow-Origin','*'); + //write first frame on stream + res.write(s.group[req.params.ke].mon[req.params.id].firstStreamChunk[chunkChannel]) + 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'] + }) + }) + }else{ + res.setHeader('Content-Type', 'application/json'); + res.end(s.prettyPrint({ok:false,msg:'FLV not started or not ready'})) + } + },res,req) + },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) { + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + s.checkChildProxy(req.params,function(){ + var Emitter,chunkChannel + if(!req.params.channel){ + Emitter = s.group[req.params.ke].mon[req.params.id].emitter + chunkChannel = 'MAIN' + }else{ + Emitter = s.group[req.params.ke].mon[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([ + config.webPaths.apiPrefix+':auth/mpegts/:ke/:id/:feed/:file', + config.webPaths.apiPrefix+':auth/mpegts/:ke/:id/:feed/', + config.webPaths.apiPrefix+':auth/h264/:ke/:id/:feed/:file', + config.webPaths.apiPrefix+':auth/h264/:ke/:id/:feed', + config.webPaths.apiPrefix+':auth/h264/:ke/:id' + ], function (req, res) { + res.header("Access-Control-Allow-Origin",req.headers.origin); + s.auth(req.params,function(user){ + s.checkChildProxy(req.params,function(){ + if(!req.query.feed){req.query.feed='1'} + var Emitter + if(!req.params.feed){ + Emitter = s.group[req.params.ke].mon[req.params.id].streamIn[req.query.feed] + }else{ + Emitter = s.group[req.params.ke].mon[req.params.id].emitterChannel[parseInt(req.params.feed)+config.pipeAddition] + } + var contentWriter + var date = new Date(); + res.writeHead(200, { + 'Date': date.toUTCString(), + 'Connection': 'keep-alive', + 'Cache-Control': 'no-cache', + 'Pragma': 'no-cache', + 'Content-Type': 'video/mp4', + 'Server': 'Shinobi H.264 Test Stream', + }) + 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'] + }) + Emitter.on('data',contentWriter=function(buffer){ + res.write(buffer) + }) + 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); + }); +} diff --git a/libs/webServerSuperPaths.js b/libs/webServerSuperPaths.js new file mode 100644 index 0000000..78936e9 --- /dev/null +++ b/libs/webServerSuperPaths.js @@ -0,0 +1,411 @@ +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; +module.exports = function(s,config,lang,app){ + /** + * API : Superuser : Get Logs + */ + app.all([config.webPaths.supersuperApiPrefix+':auth/logs'], function (req,res){ + req.ret={ok:false}; + s.superAuth(req.params,function(resp){ + req.sql='SELECT * FROM Logs WHERE ke=?';req.ar=['$']; + if(!req.params.id){ + if(user.details.sub&&user.details.monitors&&user.details.allmonitors!=='1'){ + try{user.details.monitors=JSON.parse(user.details.monitors);}catch(er){} + req.or=[]; + user.details.monitors.forEach(function(v,n){ + req.or.push('mid=?');req.ar.push(v) + }) + req.sql+=' AND ('+req.or.join(' OR ')+')' + } + }else{ + if(!user.details.sub||user.details.allmonitors!=='0'||user.details.monitors.indexOf(req.params.id)>-1||req.params.id.indexOf('$')>-1){ + req.sql+=' and mid=?';req.ar.push(req.params.id) + }else{ + res.end('[]'); + return; + } + } + if(req.query.start||req.query.end){ + if(!req.query.startOperator||req.query.startOperator==''){ + req.query.startOperator='>=' + } + if(!req.query.endOperator||req.query.endOperator==''){ + req.query.endOperator='<=' + } + if(req.query.start && req.query.start !== '' && req.query.end && req.query.end !== ''){ + req.query.start = s.stringToSqlTime(req.query.start) + req.query.end = s.stringToSqlTime(req.query.end) + req.sql+=' AND `time` '+req.query.startOperator+' ? AND `time` '+req.query.endOperator+' ?'; + req.ar.push(req.query.start) + req.ar.push(req.query.end) + }else if(req.query.start && req.query.start !== ''){ + req.query.start = s.stringToSqlTime(req.query.start) + req.sql+=' AND `time` '+req.query.startOperator+' ?'; + req.ar.push(req.query.start) + } + } + if(!req.query.limit||req.query.limit==''){req.query.limit=50} + req.sql+=' ORDER BY `time` DESC LIMIT '+req.query.limit+''; + s.sqlQuery(req.sql,req.ar,function(err,r){ + if(err){ + err.sql=req.sql; + res.end(s.prettyPrint(err)); + return + } + if(!r){r=[]} + r.forEach(function(v,n){ + r[n].info=JSON.parse(v.info) + }) + res.end(s.prettyPrint(r)) + }) + },res,req) + }) + /** + * API : Superuser : Log delete. + */ + app.all(config.webPaths.superApiPrefix+':auth/logs/delete', function (req,res){ + s.superAuth(req.params,function(resp){ + s.sqlQuery('DELETE FROM Logs WHERE ke=?',['$'],function(){ + var endData = { + ok : true + } + res.end(s.prettyPrint(endData)) + }) + },res,req) + }) + /** + * API : Superuser : Update Shinobi + */ + app.all(config.webPaths.superApiPrefix+':auth/system/update', function (req,res){ + s.superAuth(req.params,function(resp){ + s.ffmpegKill() + s.systemLog('Shinobi ordered to update',{ + by: resp.$user.mail, + ip: resp.ip + }) + var updateProcess = spawn('sh',(s.mainDirectory+'/UPDATE.sh').split(' '),{detached: true}) + updateProcess.stderr.on('data',function(data){ + s.systemLog('Update Info',data.toString()) + }) + updateProcess.stdout.on('data',function(data){ + s.systemLog('Update Info',data.toString()) + }) + var endData = { + ok : true + } + res.end(s.prettyPrint(endData)) + },res,req) + }) + /** + * API : Superuser : Restart Shinobi + */ + app.all(config.webPaths.superApiPrefix+':auth/system/restart/:script', function (req,res){ + s.superAuth(req.params,function(resp){ + var check = function(x){return req.params.script.indexOf(x)>-1} + var endData = { + ok : true + } + if(check('system')){ + s.systemLog('Shinobi ordered to restart',{by:resp.$user.mail,ip:resp.ip}) + s.ffmpegKill() + endData.systemOuput = execSync('pm2 restart '+s.mainDirectory+'/camera.js') + } + if(check('cron')){ + s.systemLog('Shinobi CRON ordered to restart',{by:resp.$user.mail,ip:resp.ip}) + endData.cronOuput = execSync('pm2 restart '+s.mainDirectory+'/cron.js') + } + if(check('logs')){ + s.systemLog('Flush PM2 Logs',{by:resp.$user.mail,ip:resp.ip}) + endData.logsOuput = execSync('pm2 flush') + } + res.end(s.prettyPrint(endData)) + },res,req) + }) + /** + * API : Superuser : Modify Configuration (conf.json) + */ + app.all(config.webPaths.superApiPrefix+':auth/system/configure', function (req,res){ + s.superAuth(req.params,function(resp){ + var endData = { + ok : true + } + var postBody = s.getPostData(req) + if(!postBody){ + endData.ok = false + endData.msg = lang.postDataBroken + }else{ + s.systemLog('conf.json Modified',{ + by: resp.$user.mail, + ip: resp.ip, + old:jsonfile.readFileSync(s.location.config) + }) + jsonfile.writeFile(s.location.config,postBody,{spaces: 2},function(){ + s.tx({f:'save_configuration'},'$') + }) + } + res.end(s.prettyPrint(endData)) + },res,req) + }) + /** + * API : Superuser : Get users in system + */ + app.all([ + config.webPaths.superApiPrefix+':auth/accounts/list', + config.webPaths.superApiPrefix+':auth/accounts/list/:type', + ], function (req,res){ + s.superAuth(req.params,function(resp){ + var endData = { + ok : true + } + searchQuery = 'SELECT ke,uid,auth,mail,details FROM Users' + queryVals = [] + switch(req.params.type){ + case'admin':case'administrator': + searchQuery += ' WHERE details NOT LIKE ?' + queryVals.push('%"sub"%') + break; + case'sub':case'subaccount': + searchQuery += ' WHERE details LIKE ?' + queryVals.push('%"sub"%') + break; + } + // ' WHERE details NOT LIKE ?' + s.sqlQuery(searchQuery,queryVals,function(err,users) { + endData.users = users + res.end(s.prettyPrint(endData)) + }) + },res,req) + }) + /** + * API : Superuser : Save Superuser Preferences + */ + app.all(config.webPaths.superApiPrefix+':auth/accounts/saveSettings', function (req,res){ + s.superAuth(req.params,function(resp){ + var endData = { + ok : true + } + var form = s.getPostData(req) + if(form){ + var currentSuperUserList = jsonfile.readFileSync(s.location.super) + var currentSuperUser = {} + var currentSuperUserPosition = -1 + //find this user in current list + currentSuperUserList.forEach(function(user,pos){ + if(user.mail === resp.$user.mail){ + currentSuperUser = user + currentSuperUserPosition = pos + } + }) + var logDetails = { + by : resp.$user.mail, + ip : resp.ip + } + //check if pass and pass_again match, if not remove password + if(form.pass !== '' && form.pass === form.pass_again){ + form.pass = s.createHash(form.pass) + }else{ + delete(form.pass) + } + //delete pass_again from object + delete(form.pass_again) + //set new values + currentSuperUser = Object.assign(currentSuperUser,form) + //reset email and log change of email + if(form.mail !== resp.$user.mail){ + logDetails.newEmail = form.mail + logDetails.oldEmail = resp.$user.mail + } + //log this change + s.systemLog('super.json Modified',logDetails) + //modify or add account in temporary master list + if(currentSuperUserList[currentSuperUserPosition]){ + currentSuperUserList[currentSuperUserPosition] = currentSuperUser + }else{ + currentSuperUserList.push(currentSuperUser) + } + //update master list in system + jsonfile.writeFile(s.location.super,currentSuperUserList,{spaces: 2},function(){ + s.tx({f:'save_preferences'},'$') + }) + }else{ + endData.ok = false + endData.msg = lang.postDataBroken + } + res.end(s.prettyPrint(endData)) + },res,req) + }) + /** + * API : Superuser : Create Admin account (Account to manage cameras) + */ + app.all(config.webPaths.superApiPrefix+':auth/accounts/registerAdmin', function (req,res){ + s.superAuth(req.params,function(resp){ + var endData = { + ok : false + } + var close = function(){ + res.end(s.prettyPrint(endData)) + } + var isCallbacking = false + var form = s.getPostData(req) + if(form){ + if(form.mail !== '' && form.pass !== ''){ + if(form.pass === form.password_again || form.pass === form.pass_again){ + isCallbacking = true + s.sqlQuery('SELECT * FROM Users WHERE mail=?',[form.mail],function(err,r) { + if(r&&r[0]){ + //found address already exists + endData.msg = lang['Email address is in use.']; + }else{ + endData.ok = true + //create new + //user id + form.uid = s.gid() + //check to see if custom key set + if(!form.ke||form.ke===''){ + form.ke=s.gid() + }else{ + form.ke = form.ke.replace(/[`~!@#$%^&*()_|+\-=?;:'",.<>\{\}\[\]\\\/]/gi, '') + } + //check if "details" is object + if(form.details instanceof Object){ + form.details = JSON.stringify(form.details) + } + //write user to db + s.sqlQuery( + 'INSERT INTO Users (ke,uid,mail,pass,details) VALUES (?,?,?,?,?)', + [ + form.ke, + form.uid, + form.mail, + s.createHash(form.pass), + form.details + ] + ) + s.tx({f:'add_account',details:form.details,ke:form.ke,uid:form.uid,mail:form.mail},'$') + //init user + s.loadGroup(form) + } + close() + }) + }else{ + endData.msg = lang["Passwords Don't Match"] + } + }else{ + endData.msg = lang['Email and Password fields cannot be empty'] + } + }else{ + endData.msg = lang.postDataBroken + } + if(isCallbacking === false)close() + },res,req) + }) + /** + * API : Superuser : Edit Admin account (Account to manage cameras) + */ + app.all(config.webPaths.superApiPrefix+':auth/accounts/editAdmin', function (req,res){ + s.superAuth(req.params,function(resp){ + var endData = { + ok : false + } + var close = function(){ + res.end(s.prettyPrint(endData)) + } + var form = s.getPostData(req) + if(form){ + var account = s.getPostData(req,'account') + s.sqlQuery('SELECT * FROM Users WHERE mail=?',[account.mail],function(err,r) { + if(r && r[0]){ + r = r[0] + var details = JSON.parse(r.details) + if(form.pass && form.pass !== ''){ + if(form.pass === form.password_again){ + form.pass = s.createHash(form.pass); + }else{ + endData.msg = lang["Passwords Don't Match"] + close() + return + } + }else{ + delete(form.pass); + } + delete(form.password_again); + var keys = Object.keys(form) + var set = [] + var values = [] + keys.forEach(function(v,n){ + if(set==='ke'||set==='password_again'||!form[v]){return} + set.push(v+'=?') + if(v === 'details'){ + form[v] = s.stringJSON(Object.assign(details,s.parseJSON(form[v]))) + } + values.push(form[v]) + }) + values.push(account.mail) + s.sqlQuery('UPDATE Users SET '+set.join(',')+' WHERE mail=?',values,function(err,r) { + if(err){ + console.log(err) + endData.error = err + endData.msg = lang.AccountEditText1 + }else{ + endData.ok = true + s.tx({f:'edit_account',form:form,ke:account.ke,uid:account.uid},'$') + delete(s.group[account.ke].init); + s.loadGroupApps(account) + } + close() + }) + } + }) + }else{ + endData.msg = lang.postDataBroken + close() + } + },res,req) + }) + /** + * API : Superuser : Delete Admin account (Account to manage cameras) + */ + app.all(config.webPaths.superApiPrefix+':auth/accounts/deleteAdmin', function (req,res){ + s.superAuth(req.params,function(resp){ + var endData = { + ok : true + } + var close = function(){ + res.end(s.prettyPrint(endData)) + } + var account = s.getPostData(req,'account') + s.sqlQuery('DELETE FROM Users WHERE uid=? AND ke=? AND mail=?',[account.uid,account.ke,account.mail]) + s.sqlQuery('DELETE FROM API WHERE uid=? AND ke=?',[account.uid,account.ke]) + if(s.getPostData(req,'deleteSubAccounts',false) === '1'){ + s.sqlQuery('DELETE FROM Users WHERE ke=?',[account.ke]) + } + if(s.getPostData(req,'deleteMonitors',false) == '1'){ + s.sqlQuery('SELECT * FROM Monitors WHERE ke=?',[account.ke],function(err,monitors){ + if(monitors && monitors[0]){ + monitors.forEach(function(monitor){ + s.camera('stop',monitor) + }) + s.sqlQuery('DELETE FROM Monitors WHERE ke=?',[account.ke]) + } + }) + } + if(s.getPostData(req,'deleteVideos',false) == '1'){ + s.sqlQuery('DELETE FROM Videos WHERE ke=?',[account.ke]) + fs.chmod(s.dir.videos+account.ke,0o777,function(err){ + fs.unlink(s.dir.videos+account.ke,function(err){}) + }) + } + if(s.getPostData(req,'deleteEvents',false) == '1'){ + s.sqlQuery('DELETE FROM Events WHERE ke=?',[account.ke]) + } + s.tx({f:'delete_account',ke:account.ke,uid:account.uid,mail:account.mail},'$') + close() + },res,req) + }) +} diff --git a/package.json b/package.json index 61cebcf..67f2e33 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "camera.js", "bin": "camera.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node test.js", "start": "chmod +x INSTALL/start.sh && INSTALL/start.sh" }, "repository": { @@ -34,14 +34,14 @@ "pipe2pam": "^0.6.2", "nodemailer": "^4.0.1", "node-onvif": "^0.1.4", - "onvif-nvt": "0.2.8", "path": "^0.12.7", "request": "^2.79.0", "socket.io": "^1.7.1", "socket.io-client": "^1.7.2", "http-proxy": "^1.17.0", - "webdav": "^0.3.1", + "webdav-fs": "^1.11.0", "discord.js": "^11.3.2", + "backblaze-b2": "^1.0.4", "ldapauth-fork": "^4.0.2" }, "devDependencies": {} diff --git a/test.js b/test.js new file mode 100644 index 0000000..38f650d --- /dev/null +++ b/test.js @@ -0,0 +1,77 @@ +// +// Shinobi +// Copyright (C) 2016 Moe Alam, moeiscool +// +// +// # Donate +// +// If you like what I am doing here and want me to continue please consider donating :) +// PayPal : paypal@m03.ca +// +var io = new (require('socket.io'))() +//library loader +var loadLib = function(lib){ + return require(__dirname+'/libs/'+lib+'.js') +} +//process handlers +var s = loadLib('process')(process,__dirname) +//configuration loader +var config = loadLib('config')(s) +//********* test.js > +config.port = 9999 +if(config.childNodes && config.childNodes.enabled === true && config.childNodes.mode === 'master'){ + config.childNodes.port = 9998 +} +//********* test.js /> +//language loader +var lang = loadLib('language')(s,config) +//basic functions +loadLib('basic')(s,config) +//load extender functions +loadLib('extenders')(s,config) +//video processing engine +loadLib('ffmpeg')(s,config,function(ffmpeg){ + //********* test.js > + s.ffmpegFunctions = ffmpeg + //********* test.js /> + //database connection : mysql, sqlite3.. + loadLib('sql')(s,config) + //working directories : videos, streams, fileBin.. + loadLib('folders')(s,config) + //authenticator functions : API, dashboard login.. + loadLib('auth')(s,config,lang) + //express web server with ejs + var app = loadLib('webServer')(s,config,lang,io) + //web server routes : page handling.. + loadLib('webServerPaths')(s,config,lang,app) + //web server routes for streams : streams.. + loadLib('webServerStreamPaths')(s,config,lang,app) + //web server admin routes : create sub accounts, share monitors, share videos + loadLib('webServerAdminPaths')(s,config,lang,app) + //web server superuser routes : create admin accounts and manage system functions + loadLib('webServerSuperPaths')(s,config,lang,app) + //websocket connection handlers : login and streams.. + loadLib('socketio')(s,config,lang,io) + //user and group functions + loadLib('user')(s,config) + //monitor/camera handlers + loadLib('monitor')(s,config,lang) + //event functions : motion, object matrix handler + loadLib('events')(s,config,lang) + //built-in detector functions : pam-diff.. + loadLib('detector')(s,config) + //recording functions + loadLib('videos')(s,config,lang) + //plugins : websocket connected services.. + loadLib('plugins')(s,config,lang) + //health : cpu and ram trackers.. + loadLib('health')(s,config,lang,io) + //cluster module + loadLib('childNode')(s,config,lang,app,io) + //cloud uploaders : amazon s3, webdav, backblaze b2.. + loadLib('cloudUploaders')(s,config,lang) + //notifiers : discord.. + loadLib('notification')(s,config,lang) + //on-start actions, daemon(s) starter + require(__dirname+'/test/run.js')(s,config,lang,app,io) +}) diff --git a/test/run.js b/test/run.js new file mode 100644 index 0000000..0ca943f --- /dev/null +++ b/test/run.js @@ -0,0 +1,130 @@ +module.exports = function(s,config,lang,app,io){ + var checkResult = function(functionName,expectedResult,testResult){ + if(expectedResult !== testResult){ + console.log(expectedResult,testResult) + throw new Error('x ' + functionName + ' : Failed!') + }else{ + console.log('- ' + functionName + ' : Success') + } + } + var sampleMonitorObject = require('./testMonitor-WatchOnly.json') + var test = { + "basic.js" : { + checkRelativePath : function(){ + var expectedResult = s.mainDirectory + '/' + var testResult = s.checkRelativePath('') + checkResult('checkRelativePath',expectedResult,testResult) + }, + parseJSON : function(){ + var expectedResult = {} + var testResult = s.parseJSON('{}') + checkResult('parseJSON',JSON.stringify(expectedResult),JSON.stringify(testResult)) + }, + stringJSON : function(){ + var expectedResult = '{}' + var testResult = s.stringJSON({}) + checkResult('stringJSON',expectedResult,testResult) + }, + addUserPassToUrl : function(){ + var expectedResult = 'http://user:pass@url.com' + var testResult = s.addUserPassToUrl('http://url.com','user','pass') + checkResult('addUserPassToUrl',expectedResult,testResult) + }, + checkCorrectPathEnding : function(){ + var expectedResult = '/' + var testResult = s.checkCorrectPathEnding('') + checkResult('checkCorrectPathEnding',expectedResult,testResult) + }, + md5 : function(){ + var expectedResult = '5f4dcc3b5aa765d61d8327deb882cf99' + var testResult = s.md5('password') + checkResult('md5',expectedResult,testResult) + }, + sha256 : function(){ + var expectedResult = '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' + var testResult = require('crypto').createHash('sha256').update('test').digest("hex") + checkResult('createHash/sha256',expectedResult,testResult) + }, + nameToTime : function(){ + var expectedResult = '2018-10-22 23:00:00' + var testResult = s.nameToTime('2018-10-22T23-00-00.mp4') + checkResult('nameToTime',expectedResult,testResult) + }, + ipRange : function(){ + var expectedResult = [ + '192.168.1.1', + '192.168.1.2', + '192.168.1.3' + ] + var testResult = s.ipRange('192.168.1.1','192.168.1.3') + checkResult('ipRange',JSON.stringify(expectedResult),JSON.stringify(testResult)) + }, + portRange : function(){ + var expectedResult = [ + 8000, + 8001, + 8002, + ] + var testResult = s.portRange(8000,8002) + checkResult('portRange',JSON.stringify(expectedResult),JSON.stringify(testResult)) + }, + getFunctionParamNames : function(){ + var testing = function(arg1,arg2){} + var expectedResult = [ + 'arg1', + 'arg2', + ] + var testResult = s.getFunctionParamNames(testing) + checkResult('getFunctionParamNames',JSON.stringify(expectedResult),JSON.stringify(testResult)) + } + }, + "ffmpeg.js" : { + splitForFFPMEG : function(){ + var expectedResult = [ + 'flag1', + 'flag2', + 'fl ag3', + ] + var testResult = s.splitForFFPMEG('flag1 flag2 "fl ag3"') + checkResult('splitForFFPMEG',JSON.stringify(expectedResult),JSON.stringify(testResult)) + }, + "ffmpeg" : function(){ + //command string builder + var x = {tmp : ''} + s.checkDetails(sampleMonitorObject) + sampleMonitorObject.url = s.buildMonitorUrl(sampleMonitorObject) + var expectedResult = '-loglevel warning -progress pipe:5 -analyzeduration 1000000 -probesize 1000000 -stream_loop -1 -fflags +igndts -re -i "https://cdn.shinobi.video:/videos/bears.mp4" -f mp4 -an -c:v copy -movflags +frag_keyframe+empty_moov+default_base_moof -metadata title="Poseidon Stream" -reset_timestamps 1 pipe:1' + s.ffmpegFunctions.buildMainInput(sampleMonitorObject,x) + s.ffmpegFunctions.buildMainStream(sampleMonitorObject,x) + s.ffmpegFunctions.buildMainRecording(sampleMonitorObject,x) + s.ffmpegFunctions.buildMainDetector(sampleMonitorObject,x) + s.ffmpegFunctions.assembleMainPieces(sampleMonitorObject,x) + var testResult = x.ffmpegCommandString + checkResult('ffmpeg',expectedResult,testResult) + //check pipe builder + var expectedResult = [] + var times = config.pipeAddition + if(sampleMonitorObject.details.stream_channels){ + times += sampleMonitorObject.details.stream_channels.length + } + for(var i=0; i < times; i++){ + expectedResult.push('pipe') + } + s.ffmpegFunctions.createPipeArray(sampleMonitorObject,x) + var testResult = x.stdioPipes + checkResult('ffmpeg.createPipeArray',JSON.stringify(expectedResult),JSON.stringify(testResult)) + } + } + } + console.log('----- Function Test Starting') + Object.keys(test).forEach(function(libkey){ + var library = test[libkey] + console.log('--- Testing ' + libkey + '...') + Object.keys(library).forEach(function(key){ + var functionTest = library[key] + functionTest() + }) + console.log('-- Completed ' + libkey + '...') + }) + console.log('---- Function Test Ended') +} diff --git a/test/testAdminUser.json b/test/testAdminUser.json new file mode 100644 index 0000000..b23e9f5 --- /dev/null +++ b/test/testAdminUser.json @@ -0,0 +1,7 @@ +{ + "mail": "user@shinobi.video", + "ke": "", + "pass": "password", + "password_again": "password", + "details": "{\"factorAuth\":\"0\",\"size\":\"\",\"days\":\"\",\"event_days\":\"\",\"log_days\":\"\",\"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_webdav\":\"1\",\"use_discordbot\":\"1\",\"use_ldap\":\"1\"}" +} diff --git a/test/testAdminUserDelete.json b/test/testAdminUserDelete.json new file mode 100644 index 0000000..414cad3 --- /dev/null +++ b/test/testAdminUserDelete.json @@ -0,0 +1,11 @@ +{ + "account": { + "mail": "[LOGIN ADDRESS]", + "ke": "[GROUP KEY]", + "uid": "[USER ID]" + }, + "deleteSubAccounts": "1", + "deleteMonitors": "1", + "deleteVideos": "1", + "deleteEvents": "1" +} diff --git a/test/testAdminUserEdit.json b/test/testAdminUserEdit.json new file mode 100644 index 0000000..47b35ce --- /dev/null +++ b/test/testAdminUserEdit.json @@ -0,0 +1,31 @@ +{ + "data": { + "mail": "[GROUP KEY]", + "ke": "[GROUP KEY]", + "pass": "[PASSWORD]", + "password_again": "[PASSWORD AGAIN]", + "details": { + "factorAuth": "0", + "size": "10000", + "days": "5", + "event_days": "10", + "log_days": "10", + "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_webdav": "1", + "use_discordbot": "1", + "use_ldap": "1" + } + }, + "account": { + "mail": "[GROUP KEY]", + "ke": "[GROUP KEY]", + "uid": "[USER ID]" + } +} diff --git a/test/testAdminUserRegister.json b/test/testAdminUserRegister.json new file mode 100644 index 0000000..6cd1a04 --- /dev/null +++ b/test/testAdminUserRegister.json @@ -0,0 +1,26 @@ +{ + "data": { + "mail": "[LOGIN ADDRESS]", + "ke": "[GROUP KEY]", + "pass": "[PASSWORD]", + "password_again": "[PASSWORD AGAIN]", + "details": { + "factorAuth": "0", + "size": "10000", + "days": "5", + "event_days": "10", + "log_days": "10", + "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_webdav": "1", + "use_discordbot": "1", + "use_ldap": "1" + } + } +} diff --git a/test/testApiAdd.json b/test/testApiAdd.json new file mode 100644 index 0000000..4ca075a --- /dev/null +++ b/test/testApiAdd.json @@ -0,0 +1,15 @@ +{ + "data": { + "ip": "[IP ADDRESS]", + "details": { + "auth_socket": "1", + "get_monitors": "1", + "control_monitors": "1", + "get_logs": "1", + "watch_stream": "1", + "watch_snapshot": "1", + "watch_videos": "1", + "delete_videos": "1" + } + } +} diff --git a/test/testApiDelete.json b/test/testApiDelete.json new file mode 100644 index 0000000..977a20b --- /dev/null +++ b/test/testApiDelete.json @@ -0,0 +1,5 @@ +{ + "data": { + "code": "[API KEY]" + } +} diff --git a/test/testMonitor-WatchOnly.json b/test/testMonitor-WatchOnly.json new file mode 100644 index 0000000..f291e26 --- /dev/null +++ b/test/testMonitor-WatchOnly.json @@ -0,0 +1 @@ +{"mode":"start","mid":"ZO7Gj0RSh3","name":"Some Stream","type":"mp4","protocol":"https","host":"cdn.shinobi.video","port":"","path":"/videos/bears.mp4","ext":"mp4","fps":"1","width":"640","height":"480","details":"{\"max_keep_days\":\"\",\"notes\":\"\",\"dir\":\"\",\"auto_host_enable\":\"1\",\"auto_host\":\"https://cdn.shinobi.video/videos/bears.mp4\",\"rtsp_transport\":\"tcp\",\"muser\":\"\",\"mpass\":\"\",\"port_force\":\"0\",\"fatal_max\":\"\",\"skip_ping\":\"0\",\"is_onvif\":\"0\",\"onvif_port\":\"80\",\"aduration\":\"1000000\",\"probesize\":\"1000000\",\"stream_loop\":\"1\",\"sfps\":\"\",\"accelerator\":\"0\",\"hwaccel\":\"auto\",\"hwaccel_vcodec\":\"\",\"hwaccel_device\":\"\",\"stream_type\":\"mp4\",\"stream_flv_type\":\"ws\",\"stream_flv_maxLatency\":\"\",\"stream_mjpeg_clients\":\"20\",\"stream_vcodec\":\"copy\",\"stream_acodec\":\"no\",\"hls_time\":\"2\",\"hls_list_size\":\"3\",\"preset_stream\":\"ultrafast\",\"signal_check\":\"10\",\"signal_check_log\":\"0\",\"stream_quality\":\"1\",\"stream_fps\":\"10\",\"stream_scale_x\":\"\",\"stream_scale_y\":\"\",\"rotate_stream\":\"no\",\"svf\":\"\",\"tv_channel\":\"0\",\"tv_channel_id\":\"\",\"tv_channel_group_title\":\"\",\"stream_timestamp\":\"0\",\"stream_timestamp_font\":\"\",\"stream_timestamp_font_size\":\"\",\"stream_timestamp_color\":\"\",\"stream_timestamp_box_color\":\"\",\"stream_timestamp_x\":\"\",\"stream_timestamp_y\":\"\",\"stream_watermark\":\"0\",\"stream_watermark_location\":\"\",\"stream_watermark_position\":\"tr\",\"snap\":\"0\",\"snap_fps\":\"1\",\"snap_scale_x\":\"640\",\"snap_scale_y\":\"480\",\"snap_vf\":\"\",\"vcodec\":\"copy\",\"crf\":\"1\",\"preset_record\":\"\",\"acodec\":\"no\",\"dqf\":\"0\",\"cutoff\":\"0.5\",\"rotate_record\":\"no\",\"vf\":\"\",\"timestamp\":\"0\",\"timestamp_font\":\"\",\"timestamp_font_size\":\"10\",\"timestamp_color\":\"white\",\"timestamp_box_color\":\"0x00000000@1\",\"timestamp_x\":\"(w-tw)/2\",\"timestamp_y\":\"0\",\"watermark\":\"0\",\"watermark_location\":\"\",\"watermark_position\":\"tr\",\"cust_input\":\"\",\"cust_snap\":\"\",\"cust_rtmp\":\"\",\"cust_rawh264\":\"\",\"cust_detect\":\"\",\"cust_stream\":\"\",\"cust_stream_server\":\"\",\"cust_record\":\"\",\"custom_output\":\"\",\"detector\":\"0\",\"detector_send_frames\":\"1\",\"detector_lock_timeout\":\"\",\"detector_save\":\"0\",\"detector_fps\":\"5\",\"detector_scale_x\":\"1280\",\"detector_scale_y\":\"720\",\"detector_record_method\":\"sip\",\"detector_trigger\":\"0\",\"detector_trigger_record_fps\":\"\",\"detector_timeout\":\"0.1\",\"watchdog_reset\":\"0\",\"detector_delete_motionless_videos\":\"1\",\"detector_webhook\":\"0\",\"detector_webhook_url\":\"\",\"detector_command_enable\":\"0\",\"detector_command\":\"\",\"detector_command_timeout\":\"\",\"detector_mail\":\"0\",\"detector_mail_send_video\":null,\"detector_mail_timeout\":\"\",\"detector_discordbot\":\"0\",\"detector_discordbot_send_video\":\"1\",\"detector_discordbot_timeout\":\"\",\"use_detector_filters\":\"0\",\"use_detector_filters_object\":null,\"cords\":\"[]\",\"detector_filters\":\"{\\\"NDXjf\\\":{\\\"id\\\":\\\"NDXjf\\\",\\\"filter_name\\\":\\\"\\\",\\\"where\\\":[{\\\"p1\\\":\\\"time\\\",\\\"p2\\\":\\\">=\\\",\\\"p3\\\":\\\"22\\\",\\\"p4\\\":\\\"&&\\\"},{\\\"p1\\\":\\\"time\\\",\\\"p2\\\":\\\"<=\\\",\\\"p3\\\":\\\"23\\\",\\\"p4\\\":\\\"&&\\\"}],\\\"actions\\\":{\\\"halt\\\":\\\"0\\\",\\\"save\\\":\\\"\\\",\\\"mail\\\":\\\"\\\",\\\"webhook\\\":\\\"\\\",\\\"discord\\\":\\\"\\\",\\\"command\\\":\\\"\\\",\\\"record\\\":\\\"\\\",\\\"indifference\\\":\\\"40\\\"}}}\",\"detector_pam\":\"0\",\"detector_sensitivity\":\"1\",\"detector_max_sensitivity\":\"\",\"detector_threshold\":\"\",\"detector_color_threshold\":\"\",\"detector_frame\":\"1\",\"detector_noise_filter\":\"0\",\"detector_noise_filter_range\":\"6\",\"detector_notrigger\":\"0\",\"detector_notrigger_mail\":\"0\",\"detector_notrigger_timeout\":\"\",\"detector_use_detect_object\":\"1\",\"detector_use_motion\":\"0\",\"detector_fps_object\":\"\",\"detector_scale_x_object\":\"\",\"detector_scale_y_object\":\"\",\"detector_lisence_plate\":\"0\",\"detector_lisence_plate_country\":\"us\",\"detector_buffer_vcodec\":\"auto\",\"detector_buffer_acodec\":\"no\",\"detector_buffer_fps\":\"\",\"detector_buffer_hls_time\":\"\",\"detector_buffer_hls_list_size\":\"\",\"detector_buffer_start_number\":\"\",\"detector_buffer_live_start_index\":\"\",\"control\":\"0\",\"control_base_url\":\"\",\"control_url_method\":null,\"control_digest_auth\":null,\"control_stop\":\"0\",\"control_url_stop_timeout\":\"\",\"control_url_center\":\"\",\"control_url_left\":\"\",\"control_url_left_stop\":\"\",\"control_url_right\":\"\",\"control_url_right_stop\":\"\",\"control_url_up\":\"\",\"control_url_up_stop\":\"\",\"control_url_down\":\"\",\"control_url_down_stop\":\"\",\"control_url_enable_nv\":\"\",\"control_url_disable_nv\":\"\",\"control_url_zoom_out\":\"\",\"control_url_zoom_out_stop\":\"\",\"control_url_zoom_in\":\"\",\"control_url_zoom_in_stop\":\"\",\"groups\":\"[\\\"wg3mS\\\"]\",\"loglevel\":\"warning\",\"sqllog\":\"0\",\"detector_cascades\":\"\",\"stream_channels\":\"\",\"input_maps\":\"\",\"input_map_choices\":\"\"}","shto":"[]","shfr":"[]"} \ No newline at end of file diff --git a/test/testSubUserDelete.json b/test/testSubUserDelete.json new file mode 100644 index 0000000..7ab5a0f --- /dev/null +++ b/test/testSubUserDelete.json @@ -0,0 +1,4 @@ +{ + "uid" : "[SUB-ACCOUNT USER ID]", + "mail" : "[SUB-ACCOUNT LOGIN ADDRESS]" +} diff --git a/test/testSubUserEdit.json b/test/testSubUserEdit.json new file mode 100644 index 0000000..c55f018 --- /dev/null +++ b/test/testSubUserEdit.json @@ -0,0 +1,10 @@ +{ + "uid": "[SUB-ACCOUNT USER ID]", + "mail": "[SUB-ACCOUNT LOGIN ADDRESS]", + "data": { + "details": { + "sub": "1", + "allmonitors": "1" + } + } +} diff --git a/test/testSubUserRegister.json b/test/testSubUserRegister.json new file mode 100644 index 0000000..06f0363 --- /dev/null +++ b/test/testSubUserRegister.json @@ -0,0 +1,7 @@ +{ + "data": { + "mail": "[SUB-ACCOUNT LOGIN ADDRESS]", + "pass": "[SUB-ACCOUNT PASSWORD]", + "password_again": "[SUB-ACCOUNT PASSWORD]" + } +} diff --git a/web/libs/css/admin-page.css b/web/libs/css/admin-page.css new file mode 100644 index 0000000..109e929 --- /dev/null +++ b/web/libs/css/admin-page.css @@ -0,0 +1,24 @@ +.shinobi-bg { + background-color: #f7f9ff; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='600' viewBox='0 0 600 600'%3E%3Cpath fill='%23d1d5e8' fill-opacity='0.4' d='M600 325.1v-1.17c-6.5 3.83-13.06 7.64-14.68 8.64-10.6 6.56-18.57 12.56-24.68 19.09-5.58 5.95-12.44 10.06-22.42 14.15-1.45.6-2.96 1.2-4.83 1.9l-4.75 1.82c-9.78 3.75-14.8 6.27-18.98 10.1-4.23 3.88-9.65 6.6-16.77 8.84-1.95.6-3.99 1.17-6.47 1.8l-6.14 1.53c-5.29 1.35-8.3 2.37-10.54 3.78-3.08 1.92-6.63 3.26-12.74 5.03a384.1 384.1 0 0 1-4.82 1.36c-2.04.58-3.6 1.04-5.17 1.52a110.03 110.03 0 0 0-11.2 4.05c-2.7 1.15-5.5 3.93-8.78 8.4a157.68 157.68 0 0 0-6.15 9.2c-5.75 9.07-7.58 11.74-10.24 14.51a50.97 50.97 0 0 1-4.6 4.22c-2.33 1.9-10.39 7.54-11.81 8.74a14.68 14.68 0 0 0-3.67 4.15c-1.24 2.3-1.9 4.57-2.78 8.87-2.17 10.61-3.52 14.81-8.2 22.1-4.07 6.33-6.8 9.88-9.83 12.99-.47.48-.95.96-1.5 1.48l-3.75 3.56c-1.67 1.6-3.18 3.12-4.86 4.9a42.44 42.44 0 0 0-9.89 16.94c-2.5 8.13-2.72 15.47-1.76 27.22.47 5.82.51 6.36.51 8.18 0 10.51.12 17.53.63 25.78.24 4.05.56 7.8.97 11.22h.9c-1.13-9.58-1.5-21.83-1.5-37 0-1.86-.04-2.4-.52-8.26-.94-11.63-.72-18.87 1.73-26.85a41.44 41.44 0 0 1 9.65-16.55c1.67-1.76 3.18-3.27 4.83-4.85.63-.6 3.13-2.96 3.75-3.57a71.6 71.6 0 0 0 1.52-1.5c3.09-3.16 5.86-6.76 9.96-13.15 4.77-7.42 6.15-11.71 8.34-22.44.86-4.21 1.5-6.4 2.68-8.6.68-1.25 1.79-2.48 3.43-3.86 1.38-1.15 9.43-6.8 11.8-8.72 1.71-1.4 3.26-2.81 4.7-4.3 2.72-2.85 4.56-5.54 10.36-14.67a156.9 156.9 0 0 1 6.1-9.15c3.2-4.33 5.9-7.01 8.37-8.07 3.5-1.5 7.06-2.77 11.1-4.02a233.84 233.84 0 0 1 7.6-2.2l2.38-.67c6.19-1.79 9.81-3.16 12.98-5.15 2.14-1.33 5.08-2.33 10.27-3.65l6.14-1.53c2.5-.63 4.55-1.2 6.52-1.82 7.24-2.27 12.79-5.06 17.15-9.05 4.05-3.72 9-6.2 18.66-9.9l4.75-1.82c1.87-.72 3.39-1.31 4.85-1.91 10.1-4.15 17.07-8.32 22.76-14.4 6.05-6.45 13.95-12.4 24.49-18.92 1.56-.96 7.82-4.6 14.15-8.33v-64.58c-4 8.15-8.52 14.85-12.7 17.9-2.51 1.82-5.38 4.02-9.04 6.92a1063.87 1063.87 0 0 0-6.23 4.98l-1.27 1.02a2309.25 2309.25 0 0 1-4.87 3.9c-7.55 6-12.9 10.05-17.61 13.19-3.1 2.06-3.86 2.78-8.06 7.13-5.84 6.07-11.72 8.62-29.15 10.95-11.3 1.5-20.04 4.91-30.75 11.07-1.65.94-7.27 4.27-6.97 4.1-2.7 1.58-4.69 2.69-6.64 3.66-5.63 2.8-10.47 4.17-15.71 4.17-17.13 0-41.44 11.51-51.63 22.83-12.05 13.4-31.42 27.7-45.25 31.16-7.4 1.85-11.85 7.05-14.04 14.69-1.26 4.4-1.58 8.28-1.58 13.82 0 .82.01.98.24 3.63.45 5.18.35 8.72-.77 13.26-1.53 6.2-4.89 12.6-10.59 19.43-13.87 16.65-22.88 46.58-22.88 71.68 0 2.39.02 4.26.06 8.75.12 10.8.1 15.8-.22 21.95-.56 11.18-2.09 20.73-5 29.3h-1.05c2.94-8.56 4.49-18.12 5.05-29.35.31-6.13.34-11.1.22-21.9-.04-4.48-.06-6.36-.06-8.75 0-25.32 9.07-55.47 23.12-72.32 5.6-6.72 8.88-12.99 10.38-19.03 1.09-4.4 1.18-7.85.74-12.93-.23-2.7-.24-2.86-.24-3.72 0-5.62.32-9.57 1.62-14.1 2.28-7.95 6.97-13.44 14.76-15.39 13.6-3.4 32.82-17.59 44.75-30.84C409 360.14 433.58 348.5 451 348.5c5.07 0 9.77-1.33 15.26-4.07 1.93-.96 3.9-2.05 6.58-3.62-.3.18 5.33-3.16 6.98-4.11 10.82-6.21 19.66-9.67 31.11-11.2 17.23-2.3 22.9-4.75 28.57-10.64 4.25-4.41 5.04-5.16 8.22-7.28 4.68-3.11 10.01-7.14 17.55-13.14a1113.33 1113.33 0 0 0 4.86-3.89l1.28-1.02a4668.54 4668.54 0 0 1 6.23-4.98c3.67-2.9 6.55-5.12 9.07-6.95 4.37-3.19 9.16-10.56 13.29-19.4v66.9zm0-116.23c-.62.01-1.27.06-1.95.13-6.13.63-13.83 3.45-21.83 7.45-3.64 1.82-8.46 2.67-14.17 2.71-4.7.04-9.72-.47-14.73-1.33-1.7-.3-3.26-.61-4.67-.93a31.55 31.55 0 0 0-3.55-.57 273.4 273.4 0 0 0-16.66-.88c-10.42-.16-17.2.74-17.97 2.73-.38.97.6 2.55 3.03 4.87 1.01.97 2.22 2.03 4.04 3.55a1746.07 1746.07 0 0 0 4.79 4.02c1.39 1.2 3.1 1.92 5.5 2.5.7.16.86.2 2.64.54 3.53.7 5.03 1.25 6.15 2.63 1.41 1.76 1.4 4.54-.15 8.88-2.44 6.83-5.72 10.05-10.19 10.33-3.63.23-7.6-1.29-14.52-5.06-4.53-2.47-6.82-7.3-8.32-15.26-.17-.87-.32-1.78-.5-2.86l-.43-2.76c-1.05-6.58-1.9-9.2-3.73-10.11-.81-.4-1.59-.74-2.36-1-2.27-.77-4.6-1.02-8.1-.92-2.29.07-14.7 1-13.77.93-20.55 1.37-28.8 5.05-37.09 14.99a133.07 133.07 0 0 0-4.25 5.44l-2.3 3.09-2.51 3.32c-4.1 5.36-7.06 8.48-10.39 11.12-.65.52-1.33 1.04-2.13 1.62l-4.11 2.94a106.8 106.8 0 0 0-5.16 3.99c-4.55 3.74-9.74 8.6-16.25 15.38-8.25 8.58-11.78 13.54-11.7 15.95.07 1.65 1.64 2.11 6.79 2.38 1.61.09 2.15.12 2.98.2 2.95.24 5.09.73 6.81 1.68 7.48 4.15 11.63 7.26 13.95 11.58 3.3 6.15.8 12.88-8.89 20.26-8.28 6.3-11.1 10.37-11.31 14.96-.06 1.17 0 1.93.26 4.43.69 6.47.25 10.65-2.8 17.42a44.23 44.23 0 0 1-4.16 7.53c-2.82 3.97-5.47 5.74-10.6 7.69-.43.16-3.34 1.23-4.27 1.59-1.8.68-3.38 1.36-5.01 2.14-4.18 2-8.4 4.6-13.1 8.24-8.44 6.51-13.23 14.56-15.98 25.06-1.1 4.2-1.55 6.81-2.8 15.21-1.26 8.6-2.17 12.64-4.08 16.55-2.1 4.28-11.93 26.59-12.97 28.88a382.7 382.7 0 0 1-6.37 13.41c-4.07 8.11-7.61 14.07-10.73 17.81-5.38 6.46-8.98 14.37-13.77 28.42a810.14 810.14 0 0 0-1.89 5.6c-1.8 5.35-2.96 8.6-4.26 11.85-6.13 15.32-25.43 26.31-46.46 26.31-11.2 0-20.58-2.74-31.02-8.55-5.6-3.13-4.55-2.42-22.26-14.54-14.33-9.8-17.7-10.73-20.47-6.9-.37.5-1.81 2.74-1.83 2.77a52.24 52.24 0 0 1-4.94 5.9c-.73.79-5.52 5.87-6.97 7.45-2.38 2.6-4.3 4.81-5.98 6.93a45.6 45.6 0 0 0-5.08 7.66c-1.29 2.57-1.9 5.25-2.66 10.6a997.6 997.6 0 0 1-.46 3.18h-1l.47-3.32c.77-5.45 1.4-8.2 2.75-10.9a46.54 46.54 0 0 1 5.2-7.84c1.7-2.14 3.63-4.38 6.03-6.98 1.45-1.59 6.24-6.68 6.96-7.46a51.58 51.58 0 0 0 4.84-5.78s1.47-2.26 1.86-2.8c3.25-4.5 7.08-3.44 21.84 6.67 17.67 12.08 16.62 11.38 22.19 14.48 10.3 5.73 19.5 8.43 30.53 8.43 20.65 0 39.57-10.77 45.54-25.69a219.7 219.7 0 0 0 4.24-11.8 6752.32 6752.32 0 0 0 1.88-5.6c4.83-14.16 8.47-22.14 13.96-28.73 3.05-3.66 6.56-9.57 10.6-17.61 1.97-3.93 4.04-8.31 6.35-13.38 1.03-2.28 10.88-24.61 12.98-28.91 1.85-3.79 2.75-7.76 4-16.25 1.24-8.44 1.7-11.07 2.81-15.32 2.8-10.7 7.71-18.94 16.33-25.6a73.18 73.18 0 0 1 13.29-8.35c1.66-.8 3.27-1.48 5.08-2.18.94-.36 3.86-1.43 4.28-1.59 4.95-1.88 7.44-3.55 10.14-7.33 1.35-1.9 2.68-4.3 4.06-7.37 2.97-6.58 3.39-10.59 2.72-16.9a27.13 27.13 0 0 1-.27-4.58c.22-4.94 3.21-9.24 11.7-15.7 9.33-7.11 11.66-13.34 8.62-19-2.2-4.09-6.25-7.12-13.55-11.17-1.57-.88-3.6-1.33-6.42-1.57-.8-.07-1.34-.1-2.95-.19-5.77-.3-7.63-.85-7.72-3.34-.1-2.81 3.5-7.87 11.97-16.69 6.53-6.8 11.75-11.69 16.33-15.45 1.79-1.47 3.42-2.72 5.2-4.03l4.12-2.94c.79-.58 1.46-1.08 2.1-1.59 3.26-2.6 6.16-5.65 10.21-10.94a383.2 383.2 0 0 0 2.5-3.32l2.31-3.09c1.8-2.39 3.04-4 4.29-5.48 8.47-10.17 16.98-13.96 37.27-15.3-.44.02 12-.9 14.32-.98 3.62-.1 6.05.16 8.46.98.8.27 1.62.62 2.47 1.04 2.27 1.14 3.17 3.87 4.27 10.85l.44 2.76c.17 1.07.33 1.97.5 2.83 1.44 7.69 3.62 12.29 7.8 14.57 6.76 3.68 10.6 5.15 13.99 4.94 4-.25 6.99-3.17 9.3-9.67 1.45-4.04 1.46-6.49.32-7.92-.9-1.12-2.28-1.62-5.57-2.27a55.8 55.8 0 0 1-2.67-.55c-2.54-.6-4.39-1.4-5.93-2.71a252.63 252.63 0 0 0-4.78-4.01 84.35 84.35 0 0 1-4.08-3.6c-2.73-2.6-3.86-4.43-3.28-5.95 1.02-2.64 7.82-3.54 18.93-3.37a230.56 230.56 0 0 1 16.73.88c2.76.39 3.2.49 3.68.6 1.4.3 2.95.62 4.62.91a82.9 82.9 0 0 0 14.56 1.32c5.56-.04 10.24-.86 13.73-2.6 8.1-4.05 15.89-6.9 22.17-7.56.7-.07 1.4-.11 2.05-.13v1zm0-100.94v1.5c-8.62 16.05-17.27 29.55-23.65 35.92-3.19 3.2-7.62 4.9-13.54 5.56-4.45.48-8.28.4-19.18-.2-9.91-.55-15.32-.44-20.52.78a84.05 84.05 0 0 1-15 2.11l-2.25.14c-12.49.75-19.37 1.78-32.72 5.74-4.5 1.33-9.27 2.49-14.3 3.48a246.27 246.27 0 0 1-32.6 3.97c-7.56.45-13.21.57-20.24.57-5.4 0-11.9 1.61-18 5.18-8.3 4.87-15.06 12.87-19.53 24.5a68.57 68.57 0 0 1-4.56 9.8c-3.6 6.2-6.92 8.99-13.38 12.18l-4.03 1.96a64.48 64.48 0 0 0-15.16 10.25c-8.2 7.33-13.72 16.63-22.54 35.6l-2.08 4.49c-7.3 15.7-11.5 23.3-17.35 29.87-7.7 8.66-20.25 14.42-40.31 20.08-4.37 1.23-19.04 5.08-19.24 5.13-6.92 1.87-11.68 3.34-15.63 4.92-10.55 4.22-18.71 10.52-36.38 26.52l-1.7 1.54c-8.58 7.76-13.41 11.9-18.81 15.88-3.95 2.9-8 5.67-12.97 8.91-2.06 1.34-10.3 6.6-12.33 7.94-11.52 7.5-18.53 13.04-24.62 20.08a62.01 62.01 0 0 0-6.44 8.85c-4.13 6.91-6.27 13.15-9.2 25.11l-1.54 6.26c-.6 2.45-1.15 4.54-1.72 6.58-2.97 10.7-6.9 17.36-14.78 26.91L69.6 491a148.51 148.51 0 0 0-4.19 5.3 23.9 23.9 0 0 0-3.44 6.28c-1.16 3.23-1.52 5.9-1.87 11.94-.58 10.05-1.42 15.04-4.63 22.67-1.57 3.72-5.66 14.02-6.41 15.8a73.46 73.46 0 0 1-3.57 7.4c-2.88 5.14-6.71 10.12-13.12 16.95-5.96 6.36-8.87 10.9-10.61 16a56.88 56.88 0 0 0-1.38 4.82l-.46 1.84h-1.03l.52-2.08c.52-2.09.92-3.49 1.4-4.9 1.8-5.25 4.78-9.9 10.84-16.36 6.35-6.78 10.13-11.7 12.97-16.77a72.5 72.5 0 0 0 3.52-7.29c.75-1.76 4.84-12.06 6.4-15.8 3.17-7.5 3.99-12.4 4.56-22.33.35-6.14.72-8.88 1.93-12.23a24.9 24.9 0 0 1 3.58-6.54c1.27-1.7 2.6-3.37 4.22-5.34l4.11-4.95c7.8-9.46 11.66-16 14.59-26.54.56-2.04 1.1-4.12 1.71-6.56l1.53-6.26c2.96-12.04 5.13-18.36 9.32-25.39 1.84-3.08 4-6.05 6.54-8.99 6.17-7.12 13.24-12.7 24.83-20.26 2.05-1.33 10.28-6.6 12.33-7.94 4.96-3.22 9-5.98 12.92-8.87 5.37-3.95 10.19-8.08 18.74-15.82l1.7-1.54c17.76-16.09 25.98-22.43 36.67-26.7 4-1.6 8.8-3.09 15.75-4.96.21-.06 14.87-3.9 19.22-5.13 19.9-5.61 32.32-11.31 39.85-19.78 5.76-6.48 9.93-14.02 17.18-29.64l2.09-4.5c8.87-19.07 14.44-28.46 22.77-35.9a65.48 65.48 0 0 1 15.38-10.4l4.04-1.97c6.3-3.1 9.47-5.77 12.96-11.77a67.6 67.6 0 0 0 4.48-9.67c4.56-11.84 11.47-20.02 19.97-25 6.25-3.66 12.93-5.32 18.5-5.32 7.01 0 12.65-.12 20.17-.57a245.3 245.3 0 0 0 32.47-3.96c5-.98 9.75-2.13 14.22-3.45 13.43-3.98 20.38-5.02 32.94-5.78l2.24-.14c5.76-.37 9.8-.9 14.85-2.09 5.31-1.25 10.79-1.35 22.6-.7 9.04.5 12.84.58 17.21.1 5.71-.62 9.94-2.26 12.95-5.26 6.44-6.45 15.3-20.37 24.35-36.72zm0 450.21c-1.28-4.6-2.2-10.55-3.33-20.25l-.24-2.04-.23-2.03c-1.82-15.7-3.07-21.98-5.55-24.47-2.46-2.46-3.04-5.03-2.52-8.64.1-.6.18-1.1.39-2.15.69-3.54.77-5.04.08-6.84-.91-2.38-3.31-4.41-7.79-6.26-5.08-2.09-6.52-4.84-4.89-8.44.66-1.45 1.79-3.02 3.52-5.01 1.04-1.2 5.48-5.96 5.08-5.53 6.15-6.7 8.98-11.34 8.98-16.48a15.2 15.2 0 0 1 6.5-12.89v1.26a14.17 14.17 0 0 0-5.5 11.63c0 5.47-2.93 10.29-9.24 17.16.38-.42-4.04 4.33-5.07 5.5-1.67 1.93-2.75 3.43-3.36 4.77-1.37 3.04-.23 5.22 4.36 7.1 4.71 1.95 7.32 4.16 8.34 6.83.78 2.04.7 3.67-.03 7.4-.2 1.03-.3 1.51-.38 2.09-.48 3.33.03 5.59 2.23 7.8 2.74 2.74 3.98 8.96 5.84 25.06l.24 2.03.23 2.04c.82 7.01 1.53 12.06 2.34 16.03v4.33zm0-62.16c-1.4-3.13-4.43-9.9-4.95-11.17-1.02-2.53-1.25-3.8-.91-5.18.2-.84 2.05-4.68 2.32-5.33a70.79 70.79 0 0 0 3.54-11.2v3.99a62.82 62.82 0 0 1-2.62 7.6c-.31.75-2.09 4.46-2.27 5.18-.28 1.12-.08 2.22.87 4.57.41 1.02 2.5 5.7 4.02 9.09v2.45zm0-85.09c-1.65 1.66-3.66 2.9-6.4 4.13-.25.1-13.97 5.47-20.4 8.43-9.35 4.32-16.7 5.9-23.03 5.25-5.08-.53-9.02-2.25-14.77-5.92l-3.2-2.07a77.4 77.4 0 0 0-5.44-3.27c-4.05-2.18-3.25-5.8 1.47-10.47 3.71-3.68 9.6-7.93 18.73-13.8l4.46-2.82c17.95-11.33 18.22-11.5 22.27-14.74 11.25-9 19.69-14.02 26.31-15.1v1.02c-6.37 1.1-14.62 6-25.69 14.86-4.1 3.28-4.34 3.44-22.36 14.8a652.4 652.4 0 0 0-4.45 2.83c-9.07 5.83-14.92 10.05-18.57 13.66-4.31 4.28-4.95 7.13-1.7 8.88 1.7.91 3.29 1.88 5.5 3.3l3.2 2.08c5.64 3.59 9.45 5.25 14.34 5.76 6.13.64 13.32-.9 22.52-5.15 6.46-2.98 20.18-8.35 20.4-8.44 3.04-1.37 5.1-2.71 6.81-4.69v1.47zm0-41.37v1c-6.56.26-12.11 3.13-19.71 9.08l-4.63 3.68a51.87 51.87 0 0 1-4.4 3.14c-.82.52-5.51 3.33-6.22 3.76-3.31 2-6.15 3.8-8.87 5.6a112.61 112.61 0 0 0-8.16 5.92c-4.61 3.72-7.4 6.9-7.97 9.35-.63 2.67 1.48 4.53 7.05 5.46 10.7 1.78 20.92-.05 30.45-4.65a61.96 61.96 0 0 0 17.1-12.2 41.8 41.8 0 0 0 5.36-7.42v1.92a38.94 38.94 0 0 1-4.64 6.19 62.95 62.95 0 0 1-17.39 12.41c-9.7 4.68-20.13 6.55-31.05 4.73-6.06-1-8.65-3.29-7.85-6.67.64-2.74 3.53-6.05 8.31-9.9 2.35-1.9 5.1-3.88 8.24-5.97 2.73-1.82 5.58-3.61 8.9-5.62.72-.44 5.4-3.24 6.22-3.75 1.26-.8 2.6-1.76 4.3-3.09.8-.62 3.9-3.1 4.63-3.67 7.77-6.1 13.49-9.04 20.33-9.3zm0-154.6v1c-1.75-.24-4.3.23-7.82 1.55-10.01 3.75-13.8 5.07-19.15 6.76-1.78.56-2.63.83-3.87 1.24-1.48.5-3.16.76-6.74 1.16a1550.34 1550.34 0 0 0-2.64.3c-7.8.94-11.28 2.47-11.28 6.07 0 4.45 2.89 13.18 7.96 25.81a57.34 57.34 0 0 1 2.33 7.6 258.32 258.32 0 0 1 .84 3.46c1.86 7.62 3.17 10.71 5.56 11.67 2.21.88 4.7.6 7.47-.72 3.48-1.69 7.22-4.94 11.2-9.47 1.52-1.7 2.97-3.49 4.59-5.57l3.16-4.1c2.59-3.23 6.07-12.21 8.39-20.23v3.45c-2.29 7.2-5.27 14.5-7.61 17.41-.44.55-2.67 3.46-3.15 4.09-1.63 2.1-3.1 3.9-4.62 5.62-4.08 4.61-7.9 7.94-11.53 9.7-2.99 1.44-5.77 1.75-8.28.74-2.84-1.13-4.2-4.34-6.15-12.35a2097.48 2097.48 0 0 1-.84-3.46c-.8-3.2-1.47-5.45-2.28-7.46-5.14-12.8-8.04-21.55-8.04-26.19 0-4.37 3.84-6.06 12.16-7.07a160.9 160.9 0 0 1 2.65-.3c3.5-.39 5.15-.64 6.53-1.1 1.26-.42 2.1-.7 3.88-1.26 5.34-1.68 9.11-3 19.1-6.74 3.53-1.32 6.22-1.84 8.18-1.61zM0 292c10.13-11.31 18.13-23.2 23.07-35.39 3.3-8.14 6.09-16.12 10.81-30.55l1.59-4.84c6.53-19.94 10.11-29.82 14.77-39.56 6.07-12.72 12.55-21.18 20.27-25.54 6.66-3.76 10.2-7.86 12.22-13.15a46.6 46.6 0 0 0 1.86-6.58c1.23-5.2 2.05-7.59 3.93-10.36 2.45-3.62 6.27-6.53 12.1-8.96 15.78-6.58 16.73-7.04 18.05-9.01.65-.98.83-2.15.74-4.51-.03-.73-.23-3.82-.24-4A93.8 93.8 0 0 1 119 94c0-10.04.18-11.37 2.37-13.15.52-.42 1.13-.8 2.07-1.3.27-.14 2.18-1.12 2.84-1.48a68.4 68.4 0 0 0 9.12-5.87c2.06-1.54 2.64-2.14 8.01-7.93 3.78-4.09 6.21-6.36 8.96-8.12 3.64-2.33 7.2-3.12 10.9-2.11 4.4 1.2 10.81 2 18.78 2.46 6.9.4 12.9.5 21.95.5 4.87 0 8.97.47 15.4 1.57 7.77 1.33 9.3 1.54 12.38 1.54 4.05 0 7.43-.88 10.68-2.95 5.06-3.22 8.11-4.67 11.2-5.2 3.62-.64 4.77-.46 16.55 2.06 17.26 3.7 30.85 1.36 41.06-9.7 5.1-5.53 5.48-8.9 3.48-14.8-.83-2.42-1.03-3.1-1.17-4.3-.29-2.52.5-4.71 2.71-6.93 2.65-2.65 4.72-9.17 6.22-18.29h2.03c-1.56 9.71-3.77 16.65-6.83 19.7-1.79 1.8-2.36 3.39-2.14 5.28.11 1 .3 1.63 1.07 3.9 2.22 6.53 1.76 10.66-3.9 16.8-10.77 11.66-25.07 14.13-42.95 10.3-11.42-2.45-12.55-2.62-15.78-2.06-2.77.48-5.62 1.84-10.47 4.92a20.93 20.93 0 0 1-11.76 3.27c-3.25 0-4.81-.22-12.73-1.57C212.74 59.46 208.73 59 204 59c-9.1 0-15.11-.1-22.07-.5-8.09-.47-14.62-1.29-19.2-2.54-5.62-1.53-10.17 1.38-17.85 9.66-5.5 5.94-6.08 6.53-8.28 8.18a70.38 70.38 0 0 1-9.38 6.03c-.68.37-2.58 1.35-2.84 1.49-.84.44-1.35.76-1.75 1.08C121.16 83.6 121 84.8 121 94c0 1.85.06 3.54.17 5.44 0 .17.2 3.28.24 4.03.1 2.75-.13 4.29-1.08 5.71-1.67 2.5-2.27 2.8-18.95 9.74-5.48 2.29-8.99 4.96-11.2 8.24-1.71 2.51-2.47 4.73-3.64 9.7-.83 3.5-1.21 4.92-1.94 6.83-2.18 5.73-6.05 10.19-13.1 14.18-7.3 4.12-13.55 12.28-19.46 24.66-4.6 9.64-8.17 19.46-14.67 39.32l-1.58 4.84c-4.75 14.47-7.54 22.48-10.86 30.69-5.28 13.01-13.95 25.65-24.93 37.6v-2.97zm0 78v-.5l1-.01c6.32 0 7.47 5.2 4.6 13.36a60.36 60.36 0 0 1-5.6 11.3v-1.92a57.76 57.76 0 0 0 4.65-9.72c2.69-7.6 1.71-12.02-3.65-12.02-.34 0-.67 0-1 .02v-46.59a340.96 340.96 0 0 0 13.71-8.34c13.66-9.46 29.79-37.6 29.79-53.59 0-18.1 21.57-72.64 32.23-79.42 12.71-8.09 32.24-27.96 35.8-37.75 1.93-5.3 5.5-7.27 14.42-9.37 6.15-1.44 8.64-2.42 10.67-4.79 1.5-1.74 2.72-4.79 4.33-10.3.23-.78 1.9-6.68 2.43-8.46 3.62-12.08 7.3-18.49 13.47-20.39 2.5-.76 3.03-.98 9.74-3.7 7.49-3.03 11.97-4.43 17.12-4.92 6.75-.65 13.13.75 19.55 4.67 5.43 3.32 12.19 4.72 20.17 4.56 6.03-.12 12.2-1.07 19.83-2.8 1.82-.4 7.38-1.74 8.26-1.94 2.69-.6 4.34-.89 5.48-.89 4.97 0 8.93-.05 14.2-.27 7.9-.32 15.56-.92 22.75-1.88 8.5-1.14 15.9-2.73 21.88-4.82 18.9-6.62 32.64-18.3 33.67-27.59.29-2.56.4-2.96 2.79-11.11 2.33-7.95 3.21-12.93 2.72-18.23-.2-2.24-.69-4.38-1.48-6.42-1.5-3.92-2.63-9.4-3.43-16.18h.9c.77 6.47 1.89 11.72 3.47 15.82a24.93 24.93 0 0 1 1.54 6.69c.5 5.46-.4 10.54-2.77 18.6-2.36 8.06-2.47 8.47-2.74 10.95-1.09 9.75-15.1 21.68-34.33 28.41-6.06 2.12-13.52 3.72-22.09 4.87-7.22.96-14.92 1.57-22.83 1.89-5.3.21-9.27.27-14.25.27-1.04 0-2.64.27-5.26.87-.87.2-6.43 1.53-8.26 1.94-7.68 1.73-13.92 2.7-20.03 2.82-8.15.17-15.1-1.27-20.71-4.7-6.23-3.81-12.4-5.16-18.93-4.54-5.04.48-9.44 1.86-16.84 4.86-6.75 2.74-7.29 2.95-9.82 3.73-5.73 1.76-9.28 7.96-12.81 19.72-.53 1.77-2.2 7.66-2.43 8.46-1.66 5.65-2.91 8.78-4.53 10.67-2.22 2.58-4.84 3.62-12.01 5.3-7.8 1.83-11.13 3.66-12.9 8.54-3.65 10.04-23.32 30.06-36.2 38.25C65.94 190 44.5 244.2 44.5 262c0 16.34-16.3 44.78-30.22 54.41-2.14 1.48-8.24 5.12-14.28 8.68v-1.16 46.09zm0-173.7v-1.11c7.42-3.82 14.55-10.23 21.84-18.98 3.8-4.56 14.21-18.78 15.79-20.55 1.8-2.04 4.06-3.96 7.42-6.45 1.08-.8 4.92-3.57 5.49-3.99 9.36-6.85 14-11.96 15.98-19.36.8-2.98 1.54-6.78 2.46-12.3.23-1.44 2-12.46 2.56-15.79 2.87-16.77 5.73-26.79 10.07-32.1C92.46 52.43 101.5 38.13 101.5 33c0-2.54.34-3.35 6.05-15.71.68-1.49 1.25-2.74 1.77-3.93 2.5-5.75 3.9-10.04 4.14-13.36h1c-.23 3.48-1.66 7.87-4.23 13.76-.52 1.2-1.09 2.45-1.78 3.95-5.54 12.01-5.95 12.99-5.95 15.29 0 5.47-9.09 19.84-20.11 33.31-4.2 5.12-7.03 15.06-9.86 31.64-.57 3.33-2.33 14.33-2.57 15.78-.92 5.56-1.67 9.38-2.48 12.4-2.05 7.68-6.82 12.93-16.35 19.91l-5.49 3.98c-3.3 2.45-5.51 4.34-7.27 6.31-1.53 1.73-11.94 15.93-15.76 20.53-7.52 9.02-14.88 15.6-22.61 19.46zm0 361.83v-4.33c.48 2.36 1 4.35 1.6 6.15 2 6.03 4.6 8.26 8.19 6.59C28.76 557.69 43.5 542.4 43.5 527c0-16.2 6.37-31.99 17.1-46.3 1.88-2.5 3.66-4.4 5.53-6 .73-.62 1.45-1.18 2.3-1.8l2-1.43c3.68-2.68 5.32-5.28 7.08-12.59.75-3.07 1.38-5.02 4.2-13.26l.63-1.88c3.24-9.58 4.56-14.97 4.17-18.65-.48-4.43-3.8-5.23-11.3-1.64a81.12 81.12 0 0 1-9.15 3.7c-13.89 4.67-26.96 5.8-42.66 5.42l-1.95-.05-1.45-.02a39.8 39.8 0 0 0-15.05 2.96A21.81 21.81 0 0 0 0 438.37v-1.26a23.55 23.55 0 0 1 4.55-2.57 40.77 40.77 0 0 1 16.92-3.02l1.95.05c15.6.38 28.57-.75 42.32-5.37a80.12 80.12 0 0 0 9.04-3.65c8.04-3.84 12.16-2.85 12.72 2.43.42 3.89-.92 9.34-4.21 19.08l-.64 1.88c-2.8 8.2-3.43 10.15-4.16 13.18-1.82 7.52-3.59 10.34-7.47 13.16l-2 1.43c-.84.6-1.54 1.15-2.25 1.75a35.45 35.45 0 0 0-5.37 5.84c-10.61 14.15-16.9 29.74-16.9 45.7 0 15.88-15 31.45-34.29 40.45-4.3 2.01-7.39-.66-9.56-7.18-.23-.68-.44-1.39-.65-2.13zm0-62.16v-2.45l1.46 3.27c2.1 4.8 3.46 10.33 4.26 16.77.66 5.3.84 9.3 1.04 18.5.2 9.32.5 12.75 1.63 15.05 1.28 2.6 3.67 2.35 8.29-1.5 17.14-14.3 21.82-22.9 21.82-38.62 0-7.17 1.1-12.39 3.7-17.68 2.27-4.67 3.65-6.62 13.4-19.62a69.8 69.8 0 0 1 7.6-8.79 44.76 44.76 0 0 1 3.54-3.06c.38-.3.64-.52.89-.74a10.47 10.47 0 0 0 2.63-3.32 35.78 35.78 0 0 0 2.26-5.94l.37-1.2.36-1.15c.29-.91.48-1.55.66-2.16.45-1.53.74-2.68.91-3.66.38-2.2.12-3.49-.85-4.15-2.35-1.61-9.28-.24-23.8 4.94-9.54 3.4-16.12 4.17-27.85 4.26-7.71.06-10.43.4-13.25 2.12-3.48 2.12-5.84 6.4-7.58 14.26-.5 2.2-.99 4.19-1.49 5.98v-3.98l.51-2.22c1.8-8.1 4.28-12.6 8.04-14.9 3.04-1.85 5.86-2.2 13.77-2.26 11.61-.09 18.1-.84 27.51-4.2 14.93-5.32 21.95-6.71 24.7-4.83 1.38.94 1.71 2.6 1.28 5.15a33.69 33.69 0 0 1-.94 3.78l-.66 2.17-.36 1.15-.37 1.2a36.64 36.64 0 0 1-2.33 6.1c-.8 1.53-1.61 2.52-2.86 3.61l-.92.77-1.02.83c-.9.74-1.65 1.4-2.47 2.18a68.84 68.84 0 0 0-7.48 8.66c-9.7 12.93-11.07 14.87-13.31 19.46-2.52 5.15-3.59 10.22-3.59 17.24 0 16.04-4.82 24.91-22.18 39.38-5.04 4.2-8.18 4.55-9.83 1.18-1.22-2.5-1.52-5.94-1.73-15.47-.2-9.16-.38-13.15-1.03-18.4-.79-6.34-2.12-11.8-4.19-16.49L0 495.98zM379.27 0h1.04l1.5 5.26c3.28 11.56 4.89 19.33 5.26 27.8.49 11.01-1.52 21.26-6.63 31.17-7.8 15.13-20.47 26.5-36.22 34.1-12.38 5.96-26.12 9.17-36.22 9.17-6.84 0-17.24 1.38-37.27 4.62l-2.27.37c-24.5 3.99-31.65 5-37.46 5-3.49 0-4.08-.08-19.54-2.8-3.56-.64-6.32-1.1-9-1.5-20.23-2.96-31-1.2-31.96 7.86-.1.85-.18 1.72-.29 2.81l-.27 2.73c-1.1 10.9-2.02 15.73-4.31 19.96-2.9 5.34-7.77 7.95-15.63 7.95-10.2 0-12.92.6-15.5 3.17.52-.51-5.03 5.85-8.16 8.7-2.75 2.5-14.32 12.55-15.77 13.83a341.27 341.27 0 0 0-6.54 5.92c-6.97 6.49-11.81 11.76-14.6 16.15-5.92 9.3-10.48 18.04-11.69 24.08-1.66 8.3 3.67 9.54 19.02 1.21a626.23 626.23 0 0 1 44.54-21.9c3.5-1.56 14.04-6.2 15.68-6.95 5.05-2.25 8.3-3.8 10.78-5.15l1.95-1.07 2.18-1.18c1.76-.94 3.38-1.76 5-2.55 18.1-8.72 34.48-10.46 50.33-1.2 22.89 13.34 38.28 37.02 38.28 56.44 0 19.12-.73 25.13-5.18 33.2a45.32 45.32 0 0 1-4.94 7.12c-6.47 7.77-11.81 16.2-12.76 21.27-1.2 6.34 4.69 7.03 20.17-.05 13.31-6.08 22.4-14.95 28.5-26.32a80.51 80.51 0 0 0 6.1-15.13c.9-2.98 3.17-11.65 3.41-12.48a29.02 29.02 0 0 1 1.75-4.83c7.47-14.93 21.09-30.5 36.25-37.24 7.61-3.38 13-9.65 19.4-20.79.84-1.48 4.26-7.64 5.14-9.17 3.52-6.1 6.22-9.7 9.37-11.98 10.15-7.4 28.7-11.1 50.29-11.1 7.52 0 16.54-1.24 27.51-3.58a420.1 420.1 0 0 0 14.96-3.52c-1.3.33 15.54-3.98 19.42-4.89 14.15-3.33 41.07-5.01 64.11-5.01 17.36 0 27.82-9.23 38.53-38.67 6.62-18.21 6.62-26.37 2.69-34.35l-1.18-2.37A13.36 13.36 0 0 1 587.5 58c0-4.03 0-4.01 2.5-24.56.46-3.73.8-6.74 1.12-9.64.9-8.45 1.38-15.2 1.38-20.8 0-.94-.02-1.94-.04-3h1c.03 1.06.04 2.06.04 3 0 5.65-.48 12.43-1.39 20.9-.3 2.91-.66 5.93-1.11 9.66-2.5 20.45-2.5 20.47-2.5 24.44 0 1.97.45 3.57 1.45 5.68.24.51 1.16 2.35 1.17 2.36 4.06 8.24 4.06 16.68-2.65 35.13-10.84 29.8-21.63 39.33-39.47 39.33-22.96 0-49.83 1.68-63.89 4.99-3.86.9-20.69 5.2-19.4 4.88a421.05 421.05 0 0 1-14.99 3.53c-11.04 2.35-20.11 3.6-27.72 3.6-21.4 0-39.76 3.67-49.7 10.9-3 2.19-5.64 5.7-9.1 11.68-.87 1.52-4.29 7.68-5.14 9.17-6.49 11.3-12 17.71-19.86 21.2-14.9 6.63-28.38 22.03-35.75 36.77a28.17 28.17 0 0 0-1.69 4.67c-.23.8-2.5 9.49-3.4 12.5a81.48 81.48 0 0 1-6.19 15.3c-6.2 11.56-15.44 20.58-28.96 26.76-16.1 7.36-23 6.55-21.58-1.04 1-5.29 6.4-13.83 12.99-21.73a44.33 44.33 0 0 0 4.82-6.96c4.35-7.88 5.06-13.77 5.06-32.72 0-19.04-15.19-42.4-37.72-55.55-15.57-9.08-31.62-7.38-49.45 1.21a132.9 132.9 0 0 0-7.14 3.71l-1.95 1.07a158.83 158.83 0 0 1-10.85 5.19c-1.65.74-12.18 5.38-15.69 6.95a625.25 625.25 0 0 0-44.46 21.86c-15.95 8.66-22.37 7.16-20.48-2.29 1.24-6.2 5.83-15.02 11.82-24.42 2.85-4.48 7.74-9.8 14.77-16.34 1.98-1.85 4.12-3.79 6.56-5.94 1.46-1.29 13.02-11.33 15.75-13.82 3.09-2.8 8.6-9.14 8.14-8.67 2.82-2.82 5.75-3.46 16.2-3.46 7.5 0 12.04-2.43 14.75-7.42 2.2-4.07 3.11-8.84 4.2-19.59l.26-2.73.3-2.81c.56-5.42 4.47-8.5 11.23-9.6 5.44-.88 12.51-.51 21.86.86 2.7.4 5.47.86 9.04 1.49 15.33 2.7 15.96 2.8 19.36 2.8 5.73 0 12.9-1.03 37.3-5l2.27-.36c20.1-3.26 30.52-4.64 37.43-4.64 9.95 0 23.54-3.18 35.78-9.08 15.57-7.5 28.09-18.73 35.78-33.65 5.02-9.75 7-19.82 6.51-30.67-.37-8.37-1.96-16.08-5.23-27.57L379.27 0zm13.68 0h1.02c.78 3.9 1.92 8.7 3.51 14.88 3.63 14.05 3.06 27.03-.75 38.77a61 61 0 0 1-11.35 20.68 138.36 138.36 0 0 1-19.32 18.77c-11.32 9.02-23.36 15.49-35.95 18.39a258.63 258.63 0 0 1-22.57 4.07c-3.17.44-6.36.85-10.3 1.32l-9.39 1.12c-11.53 1.41-17.45 2.55-21.64 4.46-9.28 4.21-28.35 6.04-49.21 6.04-1.37 0-2.8-.12-4.3-.35-2.62-.41-5-1.03-9.14-2.29-7.34-2.21-9.63-2.75-12.63-2.56-3.9.23-6.63 2.29-8.47 6.89-1.86 4.66-2.42 7.53-3.34 14.98-1.1 8.98-2.87 12.12-9.97 14.3a40.12 40.12 0 0 0-6.8 2.66c-.63.33-1.16.64-1.76 1.02l-1.34.86c-1.9 1.14-3.86 1.49-9.25 1.49-3.2 0-8.83-.55-9.51-.39-1.22.28-.75-.14-7.14 6.24-1.5 1.5-3.49 3.18-6.32 5.37-1.52 1.18-7.16 5.43-7.94 6.03-4.96 3.78-8.33 6.6-11.06 9.38-4.88 4.98-6.85 9.15-5.56 12.7 1.34 3.67 4.07 4.42 8.9 2.82a55.72 55.72 0 0 0 7.77-3.48c1.5-.77 7.78-4.13 9.37-4.96a116.8 116.8 0 0 1 12.31-5.68 162.2 162.2 0 0 0 11.04-4.84c2.04-.97 10.74-5.16 13-6.22 4.41-2.1 8.1-3.78 11.65-5.29 17.14-7.3 29.32-9.9 37.67-6.65l5.43 2.1c2.3.88 4.17 1.62 6.02 2.38a150.9 150.9 0 0 1 13.07 6c18.34 9.63 30.35 22.13 34.79 39.87 6.96 27.85 3.6 45.53-8.08 62.4-3.97 5.75-3.52 9.2.06 8.97 4.14-.28 10.21-4.95 15.11-12.52 3.1-4.8 5.1-10.45 8.05-21.53l1.69-6.35c.66-2.47 1.24-4.52 1.83-6.5 4.93-16.56 11-27.28 21.56-34.76 7.15-5.06 23.73-15.5 25.48-16.75 6.74-4.81 10.53-9.44 14.34-18 7.74-17.44 21.09-24.34 44.47-24.34 9.36 0 17.91-1.13 29.53-3.49a624.86 624.86 0 0 0 6.2-1.28c2.4-.5 4.07-.84 5.66-1.13 4.03-.74 7.04-1.1 9.61-1.1 4.44 0 9.39-1 31.39-5.99l2.95-.66c16.34-3.67 25.64-5.35 31.66-5.35 1.54 0 2.4.01 6.4.1 7.8.15 12.27.13 17.33-.2 16.41-1.06 26.73-5.36 29.8-14.56a87.1 87.1 0 0 1 3.55-8.83c-.15.31 2.29-4.96 2.9-6.38 5.38-12.3 5.57-21.92-1.44-39.44a86.4 86.4 0 0 1-5.26-20.72c-1.61-11.98-1.38-23.14.1-40.35l.2-2.12h1l-.2 2.2c-1.48 17.15-1.7 28.24-.11 40.14a85.4 85.4 0 0 0 5.2 20.47c7.1 17.78 6.91 27.67 1.43 40.22-.62 1.43-3.06 6.72-2.91 6.4a86.17 86.17 0 0 0-3.52 8.73c-3.23 9.72-13.9 14.15-30.68 15.24-5.1.33-9.58.35-17.42.2-3.98-.09-4.84-.1-6.37-.1-5.91 0-15.18 1.67-31.44 5.32l-2.95.67c-22.16 5.02-27.05 6.01-31.61 6.01-2.5 0-5.45.36-9.43 1.09-1.58.29-3.25.62-5.64 1.11a4894.21 4894.21 0 0 0-6.2 1.29c-11.68 2.37-20.3 3.51-29.73 3.51-23.02 0-36 6.71-43.53 23.66-3.9 8.8-7.82 13.58-14.7 18.5-1.78 1.27-18.36 11.7-25.48 16.75-10.34 7.32-16.3 17.87-21.19 34.23-.58 1.96-1.15 4-1.82 6.47l-1.69 6.35c-2.98 11.18-5 16.9-8.17 21.81-5.05 7.81-11.37 12.68-15.89 12.98-4.7.31-5.3-4.23-.94-10.53 11.52-16.64 14.82-34.03 7.92-61.6-4.35-17.42-16.16-29.72-34.27-39.22-4-2.1-8.2-4-12.99-5.97-1.84-.75-3.7-1.49-6-2.38l-5.43-2.08c-8.03-3.12-20.02-.58-36.92 6.63-3.52 1.5-7.21 3.19-11.61 5.27l-13 6.22c-4.71 2.22-8.16 3.75-11.11 4.88a115.87 115.87 0 0 0-12.21 5.63c-1.58.83-7.86 4.18-9.37 4.96a56.55 56.55 0 0 1-7.9 3.54c-5.3 1.75-8.62.85-10.17-3.43-1.46-4.02.66-8.5 5.8-13.74 2.75-2.82 6.16-5.66 11.15-9.48.79-.6 6.43-4.85 7.94-6.02a66.96 66.96 0 0 0 6.23-5.28c6.74-6.74 6.1-6.16 7.61-6.51.87-.2 6.69.36 9.74.36 5.22 0 7.03-.32 8.74-1.35l1.31-.84c.62-.4 1.18-.72 1.84-1.07a41.07 41.07 0 0 1 6.96-2.72c6.64-2.04 8.22-4.84 9.28-13.47.93-7.53 1.5-10.47 3.4-15.24 1.99-4.95 5.04-7.26 9.34-7.51 3.17-.2 5.5.35 12.97 2.6a63.54 63.54 0 0 0 9.02 2.26c1.45.22 2.83.34 4.14.34 20.71 0 39.7-1.82 48.8-5.96 4.32-1.96 10.29-3.1 21.93-4.53l9.4-1.12c3.92-.48 7.11-.88 10.27-1.32 8.16-1.14 15.4-2.43 22.49-4.06 12.42-2.86 24.33-9.26 35.55-18.2a137.4 137.4 0 0 0 19.18-18.64 60.02 60.02 0 0 0 11.15-20.32c3.76-11.57 4.32-24.36.75-38.23A284.86 284.86 0 0 1 392.95 0zM506.7 0h1.26c-.5.66-.9 1.18-1.17 1.51-3.95 4.96-6.9 7.92-9.82 9.57A10.02 10.02 0 0 1 492 12.5c-2.38 0-4.24.67-6.71 2.21l-2.65 1.71c-4.38 2.8-8.01 4.08-13.64 4.08-5.6 0-9.99-1.26-16.08-4.05a202.63 202.63 0 0 1-2.3-1.06l-2.18-.98c-1.6-.7-2.92-1.17-4.17-1.48a13.42 13.42 0 0 0-3.27-.43c-2.3 0-4.3-.68-11-3.37l-1.56-.62c-5-1.97-8.1-2.82-10.52-2.66-2.93.2-4.42 2.03-4.42 6.15 0 20.76-5.21 50.42-12.15 57.35-7.58 7.59-26.55 23.7-34.06 29.06-13.16 9.4-31.17 20.2-44.11 25.06a106.87 106.87 0 0 1-13.32 4.03c-3.28.78-6.6 1.43-11.25 2.24-.53.1-8.8 1.5-11.5 1.99-4.86.87-9.3 1.74-14 2.76-20.62 4.48-25.07 5.01-38.11 5.01-2.49 0-2.9-.07-14.05-2-2.42-.42-4.31-.73-6.15-1-8.11-1.19-13.83-1.36-17.64-.2-4.54 1.4-5.93 4.65-3.7 10.52 2.02 5.28 4.84 8.61 8.84 10.74 3.26 1.74 6.75 2.6 13.82 3.71 9.42 1.48 10.94 1.75 15.5 2.92a78.2 78.2 0 0 1 18.62 7.37c8.3 4.58 14.58 11.5 19.98 20.89 2.73 4.73 9.46 19.33 10.54 21.19 3.4 5.85 6.26 6.63 10.89 2 4.95-4.94 10.35-8.37 21.13-14.06.47-.25 2.06-1.1 2.12-1.12 7.98-4.21 11.92-6.51 15.87-9.54 5.11-3.9 8.66-8.1 10.77-13.11 8.52-20.24 20.75-33.31 32.46-33.31l5.5.03c10.53.08 17.35.02 24.9-.31 13.66-.62 23.78-2.09 29.39-4.67 5.85-2.7 13.42-5.49 24.18-9.02 3.46-1.14 6.29-2.05 12.7-4.1 7.7-2.45 11.08-3.54 15.17-4.9a1059.43 1059.43 0 0 1 11.33-3.72c3.67-1.2 5.96-2 8.03-2.78a59.88 59.88 0 0 0 6.66-2.94c1.87-.98 3.76-2.1 5.86-3.5 3.48-2.33 6.15-3.13 12.04-4.13l1.15-.2c5.71-1.01 9-2.3 12.76-5.63 7.82-6.96 8.58-23.18 3.84-44.52-1.7-7.67-2.1-19.28-1.57-35.47A837.22 837.22 0 0 1 546.76 0h1l-.15 3.06c-.32 6.42-.53 11.02-.68 15.62-.51 16.1-.12 27.65 1.56 35.21 4.82 21.68 4.04 38.2-4.16 45.48-3.91 3.48-7.37 4.84-13.24 5.87l-1.16.2c-5.76.99-8.32 1.75-11.65 3.98a63.73 63.73 0 0 1-5.96 3.56 60.86 60.86 0 0 1-6.77 2.99c-2.09.79-4.39 1.58-8.07 2.79a5398.31 5398.31 0 0 1-11.32 3.71c-4.1 1.37-7.48 2.46-15.18 4.92-6.42 2.04-9.24 2.95-12.7 4.08-10.73 3.53-18.27 6.3-24.07 8.98-5.76 2.66-15.97 4.14-29.77 4.77-7.56.33-14.4.39-24.95.31l-5.49-.03c-11.19 0-23.16 12.79-31.54 32.7-2.19 5.19-5.84 9.52-11.08 13.52-4.02 3.07-7.99 5.39-16.01 9.62l-2.12 1.12c-10.7 5.65-16.04 9.04-20.9 13.9-5.14 5.14-8.75 4.15-12.45-2.22-1.12-1.92-7.85-16.5-10.54-21.2-5.33-9.24-11.48-16.02-19.6-20.5a77.2 77.2 0 0 0-18.4-7.28c-4.5-1.17-6.02-1.43-15.4-2.9-7.17-1.12-10.74-2-14.13-3.81-4.22-2.25-7.2-5.77-9.3-11.27-2.43-6.39-.78-10.26 4.34-11.83 4-1.22 9.82-1.05 18.08.17 1.84.27 3.74.58 6.17 1 11.02 1.9 11.48 1.98 13.88 1.98 12.96 0 17.35-.52 37.9-4.99 4.71-1.02 9.16-1.9 14.03-2.77 2.71-.48 10.98-1.9 11.5-1.98 4.64-.81 7.95-1.46 11.2-2.23 4.55-1.07 8.76-2.34 13.2-4 12.83-4.81 30.79-15.59 43.88-24.94 7.47-5.33 26.4-21.4 33.94-28.94C407.3 61.98 412.5 32.49 412.5 12c0-4.61 1.86-6.9 5.35-7.15 2.63-.18 5.8.7 10.96 2.73l1.56.62c6.53 2.62 8.53 3.3 10.63 3.3 1.14 0 2.3.16 3.5.46 1.32.33 2.68.82 4.34 1.53a90.97 90.97 0 0 1 3.34 1.52l1.15.54c5.98 2.73 10.23 3.95 15.67 3.95 5.41 0 8.87-1.21 13.1-3.92.2-.13 2.1-1.38 2.66-1.72 2.62-1.63 4.64-2.36 7.24-2.36 1.47 0 2.94-.43 4.47-1.3 2.78-1.56 5.67-4.45 9.54-9.31l.7-.89zM324.54 600h-2.03c.49-2.96.91-6.2 1.28-9.66.44-4.1.76-8.25.98-12.21.08-1.39.14-2.65-.35-7.29-.47-1.94-.93-4.14-1.36-6.54-2.01-11.26-2.66-22.9-1.14-33.78a60.76 60.76 0 0 1 5.18-17.95 70.78 70.78 0 0 1 12.6-18.22c3.38-3.6 5.53-5.5 11.83-10.79 4.5-3.78 6.35-5.56 7.52-7.5.64-1.07.95-2.06.95-3.06 0-1.75 0-1.74-.75-9.23-.36-3.7-.57-6.3-.68-8.96-.5-12.1 1.62-19.6 8.11-21.76 15.9-5.3 25.89-12.1 33.45-25.54C409.6 390.65 425.85 376 436 376c12.36 0 20-1.96 29.41-8.8 6.76-4.92 9.5-6.6 12.47-7.46 2.22-.64 3.8-.74 9.12-.74 1.86 0 3.53-.83 5.57-2.62 1.08-.96 5.11-5.12 5.6-5.6 6.04-5.85 11.98-8.78 20.83-8.78 2.45 0 4.54.04 7.32.12 7.51.23 8.87.17 11.27-.7 3.03-1.1 5.53-3.03 14.75-11.17 8-7.06 10.72-8.92 22.87-16.47 1.44-.9 2.59-1.63 3.69-2.37a69.45 69.45 0 0 0 9.46-7.5c4.12-3.88 8.02-7.85 11.64-11.9v2.98a201.58 201.58 0 0 1-10.27 10.38c-3.18 3-6.2 5.35-9.72 7.7-1.12.76-2.28 1.5-3.75 2.4-12.05 7.5-14.71 9.32-22.6 16.28-9.46 8.35-12.01 10.32-15.39 11.55-2.74 1-4.19 1.06-12.01.82-2.76-.08-4.83-.12-7.26-.12-8.27 0-13.75 2.7-19.43 8.22-.44.43-4.52 4.64-5.68 5.66-2.37 2.09-4.46 3.12-6.89 3.12-5.1 0-6.6.1-8.56.66-2.67.78-5.29 2.37-11.85 7.15-9.8 7.13-17.85 9.19-30.59 9.19-9.22 0-24.96 14.2-34.13 30.49-7.84 13.94-18.24 21.02-34.55 26.46-5.31 1.77-7.21 8.51-6.75 19.78.1 2.6.31 5.19.68 8.84.75 7.62.75 7.58.75 9.43 0 1.38-.42 2.73-1.24 4.09-1.33 2.2-3.26 4.07-7.94 8-6.25 5.24-8.36 7.12-11.67 10.63a68.8 68.8 0 0 0-12.25 17.71 58.8 58.8 0 0 0-5 17.36c-1.49 10.66-.85 22.09 1.13 33.15.43 2.37.88 4.53 1.33 6.44.16.66.3 1.25.6 4.06a249.3 249.3 0 0 1-1.17 16.12c-.37 3.37-.78 6.53-1.25 9.44zm-13.4 0h-1.05l.12-.28c3.07-7.16 4.29-11.83 4.29-18.72 0-3.57-.07-4.93-.76-15.65-.77-12.04-1-19.64-.55-28.3.58-11.5 2.4-22.1 5.81-32.16 1.3-3.8 2.8-7.5 4.55-11.1 3.46-7.14 6.83-12.39 10.42-16.6a59.02 59.02 0 0 1 4.35-4.56c.43-.4 3-2.8 3.67-3.45 5.72-5.6 7.51-11.52 7.51-29.18 0-18.84 2.9-23.77 15.82-28.24 1.09-.37 1.92-.67 2.77-.98a51.3 51.3 0 0 0 6.1-2.7c4.95-2.6 9.64-6.22 14.44-11.42 25.5-27.63 37.15-35.16 56.37-35.16 8.28 0 14.54-1.95 22-6.3 1.78-1.03 13.82-8.82 18.16-11.27 2.83-1.59 5.66-3.03 8.63-4.39 7.92-3.6 13.97-4.45 26.6-4.8 7.53-.2 10.7-.49 14.26-1.58 4.55-1.4 8.06-4 10.93-8.43 2.2-3.41 6.85-7.08 14.66-12.06 1.61-1.03 3.27-2.05 5.65-3.5 9.53-5.85 11.56-7.13 14.81-9.57 5.34-4 9.3-8.37 13.68-14.77a204.2 204.2 0 0 0 5.62-8.75v1.9c-1.97 3.17-3.4 5.38-4.8 7.42-4.42 6.48-8.46 10.92-13.9 15-3.29 2.46-5.32 3.75-14.89 9.61a375.06 375.06 0 0 0-5.63 3.5c-7.7 4.9-12.26 8.52-14.36 11.76-3 4.63-6.7 7.39-11.48 8.85-3.68 1.12-6.9 1.42-14.53 1.63-12.5.34-18.44 1.18-26.2 4.7a111.08 111.08 0 0 0-8.56 4.35c-4.3 2.43-16.34 10.22-18.15 11.27-7.6 4.43-14.03 6.43-22.5 6.43-18.87 0-30.3 7.4-55.63 34.84-4.88 5.28-9.67 8.97-14.7 11.62-2 1.05-4 1.92-6.23 2.75-.86.32-1.7.62-5.37 1.87-5.08 1.76-7.44 3.25-9.28 6.37-2.23 3.78-3.29 9.94-3.29 20.05 0 17.9-1.87 24.07-7.8 29.89-.69.67-3.27 3.06-3.69 3.46a58.04 58.04 0 0 0-4.28 4.49c-3.53 4.14-6.86 9.32-10.28 16.38a95.19 95.19 0 0 0-4.5 10.99c-3.38 9.97-5.18 20.48-5.76 31.9-.44 8.6-.22 16.17.55 28.17.69 10.76.76 12.12.76 15.72 0 6.35-1.02 10.87-4.35 19zm25.08 0h-1c-.04-4.73.06-9.39.28-15.02.26-6.41-.4-11.79-2.53-24.37l-.31-1.86c-2.12-12.55-2.76-19.35-1.97-26.47 1.03-9.25 4.75-16.68 12-22.67 22.04-18.2 29.81-30.18 29.81-44.61 0-2.6-.3-4.81-.98-8.17-.97-4.79-1.1-5.68-.97-7.57.2-2.56 1.27-4.7 3.56-6.72 2.67-2.35 7.05-4.6 13.72-7.01 9.72-3.5 15.52-9.18 24.3-21.57l1.78-2.5c4.48-6.33 7.1-9.63 10.43-12.78 4.31-4.07 8.98-6.77 14.54-8.17 13.3-3.32 20.37-5.47 25.34-7.64a49.5 49.5 0 0 0 5.28-2.7c1.1-.65 1.75-1.04 4.24-2.6 2.7-1.68 5.22-2.08 11.38-2.28 5.44-.18 7.9-.43 10.97-1.41a21.47 21.47 0 0 0 9.54-6.22c4.87-5.3 10.03-7.61 17.79-8.9 1.07-.18 1.88-.3 3.86-.58 6.9-.97 9.94-1.69 13.48-3.62 4.5-2.45 6.79-4.44 23.46-19.68l3.14-2.85c9.65-8.71 16.12-13.83 21.42-16.48 4.25-2.12 7.6-4.69 11.22-8.6v1.45c-3.42 3.57-6.69 6-10.78 8.05-5.18 2.59-11.61 7.67-21.2 16.32l-3.12 2.85c-16.8 15.35-19.05 17.3-23.66 19.82-3.68 2-6.8 2.75-13.82 3.73-1.97.28-2.78.4-3.84.57-7.56 1.26-12.52 3.48-17.21 8.6a22.47 22.47 0 0 1-9.97 6.5c-3.2 1-5.72 1.27-11.25 1.45-5.98.2-8.39.57-10.89 2.13a144 144 0 0 1-4.25 2.61 50.48 50.48 0 0 1-5.39 2.75c-5.04 2.2-12.15 4.37-25.5 7.7-9.74 2.44-15.26 7.65-24.4 20.56l-1.77 2.5c-8.9 12.54-14.82 18.34-24.78 21.93-6.57 2.36-10.85 4.57-13.4 6.82-2.1 1.86-3.05 3.74-3.22 6.04-.13 1.76 0 2.63.95 7.3.7 3.42 1 5.7 1 8.37 0 14.79-7.93 27-30.18 45.39-7.03 5.8-10.64 13-11.64 22-.78 7-.14 13.73 1.96 26.2l.32 1.85c2.15 12.65 2.8 18.07 2.54 24.58-.22 5.57-.32 10.2-.28 14.98zM95.9 600h-2.04c.68-3.82 1.14-8.8 1.61-15.98.2-3.11.27-4.06.39-5.6 1.3-17.54 4.04-27.14 11.5-33.2 4.65-3.77 7.22-8.92 8.67-16 .51-2.52.7-3.87 1.33-9.17.66-5.5 1.16-8.06 2.24-10.36 1.45-3.09 3.82-4.69 7.39-4.69 14.28 0 38.48 9.12 53.6 20.2 8.66 6.35 21.26 13.32 31.74 17.11 13.03 4.71 21.89 4.41 24.75-1.73 1.7-3.64 1.92-4.11 2.65-5.77 2.93-6.67 4.69-12.2 5.25-17.5.23-2.17.24-4.23.02-6.2-.32-2.75-1.42-4.55-4.08-7.35l-1.32-1.37a30.59 30.59 0 0 1-2.41-2.79 30.37 30.37 0 0 1-2.5-4.07l-1.13-2.14c-1.62-3.1-2.68-4.6-4.12-5.56-5.26-3.5-14.8-5.5-28.55-6.83a272.42 272.42 0 0 0-9.04-.71l-2.18-.17c-9.57-.73-15.12-1.56-19.06-3.2C156.57 471.07 136 450.5 136 440c0-5.34 1.74-9.53 5.47-14.13 1.98-2.44 11.12-11.71 12.79-13.54 4.52-4.97 10.16-9.54 17.68-14.66 2.8-1.9 14.78-9.6 17.49-11.49a50.54 50.54 0 0 0 6.34-5.43c1.53-1.5 6.96-7.13 7.12-7.3 7.18-7.3 12.7-11.56 19.74-14.38 3.36-1.34 8.13-2.79 17.45-5.38a9577.18 9577.18 0 0 1 11.78-3.28 602.6 602.6 0 0 0 12.67-3.7c20.4-6.24 34-12.08 40.79-18.44 8.74-8.2 11.78-13.84 15.73-26.02 2.02-6.22 3.09-9.04 5.07-12.72 9.54-17.71 28.71-39.37 43.5-45.45C383.77 238.25 389 232.34 389 226c0-2.89 2.73-8.4 6.83-13.73 4.76-6.2 10.65-11.36 16.75-14.18 12.5-5.77 33.5-10.09 47.42-10.09 5.32 0 9.83-1.5 16.42-4.89 9.2-4.71 10.1-5.11 13.58-5.11 10.42 0 32.06-2.55 45.76-5.97l3.88-.98 3.47-.89c2.6-.66 4.33-1.08 5.93-1.43 3.9-.86 6.76-1.23 9.58-1.17 2.74.06 5.47.52 8.67 1.48 4.56 1.37 13.71-.9 22.87-5.68a68.07 68.07 0 0 0 9.84-6.2v2.4c-11.09 8.14-25.76 13.66-33.29 11.4a29.72 29.72 0 0 0-8.13-1.4c-2.63-.05-5.36.3-9.11 1.12a238 238 0 0 0-9.33 2.3l-3.9.99C522.38 177.43 500.58 180 490 180c-2.99 0-3.91.4-12.67 4.89-6.85 3.51-11.61 5.11-17.33 5.11-13.65 0-34.35 4.26-46.58 9.9-5.78 2.67-11.42 7.62-16 13.58-3.85 5.02-6.42 10.2-6.42 12.52 0 7.27-5.8 13.82-20.62 19.92-14.27 5.88-33.16 27.21-42.5 44.55-1.9 3.55-2.95 6.28-4.93 12.4-4.05 12.47-7.23 18.39-16.27 26.86-7.08 6.64-20.87 12.57-41.57 18.89a604.52 604.52 0 0 1-12.7 3.71 1495.1 1495.1 0 0 1-11.8 3.28c-9.24 2.58-13.97 4.01-17.24 5.32-6.73 2.69-12.05 6.8-19.05 13.92-.15.15-5.6 5.8-7.15 7.32a52.4 52.4 0 0 1-6.6 5.65c-2.74 1.92-14.75 9.63-17.5 11.5-7.4 5.04-12.94 9.52-17.33 14.35-1.72 1.9-10.8 11.11-12.71 13.46-3.47 4.26-5.03 8.03-5.03 12.87 0 9.5 20 29.5 33.38 35.08 3.67 1.53 9.1 2.34 18.45 3.05a586.23 586.23 0 0 0 4.34.32c3.24.23 5.07.37 6.93.55 14.08 1.37 23.82 3.4 29.45 7.17 1.82 1.2 3.02 2.91 4.8 6.29l1.11 2.13a28.55 28.55 0 0 0 2.34 3.81c.62.83 1.3 1.6 2.26 2.61.23.24 1.1 1.16 1.32 1.37 2.93 3.09 4.24 5.23 4.61 8.5.24 2.12.23 4.33-.01 6.64-.59 5.55-2.4 11.25-5.41 18.1-.74 1.67-.96 2.15-2.66 5.8-3.49 7.47-13.33 7.8-27.25 2.77-10.67-3.86-23.43-10.92-32.25-17.38C164.62 515.96 140.82 507 127 507c-5 0-6.4 3.02-7.64 13.29a99.03 99.03 0 0 1-1.36 9.33c-1.53 7.5-4.3 13.04-9.37 17.16-6.87 5.58-9.5 14.78-10.77 31.8-.11 1.52-.18 2.47-.38 5.57-.46 7.01-.91 11.99-1.57 15.85zm8.05 0h-1.02c.29-1.41.58-2.94.9-4.59l1.05-5.62c2.5-13.3 4.2-19.92 6.68-24.05 1.7-2.84 3.68-5.5 8.05-11.03 8.21-10.36 10.88-14.55 10.88-18.71l-.02-1.69c-.02-1.78-.02-2.7.02-3.77.21-5.05 1.47-8.2 4.64-9.4 3.92-1.5 10.39.44 20.12 6.43 9.56 5.88 17.53 10.7 25.91 15.66 1.31.78 14.27 8.41 17.67 10.45a714.21 714.21 0 0 1 6.42 3.9c13.82 8.5 38.94 5.05 46.3-7.83 3.6-6.28 4.54-8.52 7.78-17.32a82.3 82.3 0 0 1 1.18-3.07 42.27 42.27 0 0 1 4.06-7.64c9.33-13.98 14.92-26.1 14.92-36.72 0-3.66.75-6.62 3.36-14.85.52-1.64.83-2.66 1.15-3.73 3.64-12.23 3.04-19.12-4.29-24a23.1 23.1 0 0 0-9.98-3.78c-7.2-.93-14.49 1.17-23.91 5.88-1.55.78-6.64 3.44-7.6 3.93a62.6 62.6 0 0 0-4.14 2.3l-4.4 2.66c-11.62 6.92-20.4 9.18-32.81 6.08-3.32-.84-6.24-1.4-13.1-2.64-13.25-2.39-18.7-3.75-23.33-6.46-6.23-3.67-7.46-9.02-2.88-16.65A93.1 93.1 0 0 1 172 415.42a157 157 0 0 1 8.32-7.66c-.07.05 6.16-5.3 7.82-6.77a85.12 85.12 0 0 0 6.5-6.33c7.7-8.46 12.78-13.36 20.08-18.57 9.94-7.1 21.4-12.36 35.18-15.58 37.03-8.64 51-12.7 58.83-17.93 8.6-5.73 21.3-24.77 36.84-54.81 5.22-10.1 12.27-18.4 21.13-25.71 5.13-4.24 9.56-7.25 17.55-12.23 7.42-4.62 9.62-6.14 11.38-8.16a21.15 21.15 0 0 0 2.95-4.87c.61-1.3 2.87-6.47 3-6.77 1.36-3 2.56-5.4 3.95-7.73 6.53-10.97 16.03-18 31.4-20.8 12.73-2.3 19.85-2.7 29.68-2.3 3.25.13 4.13.16 5.6.14 5.15-.07 9.71-1.04 16.61-3.8 20.74-8.3 38.75-12.04 59.19-12.04 3.05 0 6.03.15 10.48.48l2.09.16c12.45.96 18.08.96 25.34-.63a49.65 49.65 0 0 0 14.09-5.45v1.15a50.52 50.52 0 0 1-13.88 5.28c-7.38 1.61-13.08 1.61-25.63.65l-2.08-.16c-4.43-.33-7.39-.48-10.41-.48-20.3 0-38.2 3.72-58.81 11.96-7.01 2.8-11.7 3.8-16.97 3.88-1.5.02-2.39-.01-5.66-.14-9.76-.4-16.8-.01-29.47 2.3-15.06 2.73-24.32 9.58-30.71 20.31a72.8 72.8 0 0 0-3.9 7.63c-.12.28-2.39 5.47-3.01 6.79a22 22 0 0 1-3.1 5.1c-1.86 2.13-4.07 3.66-11.6 8.35-7.95 4.96-12.35 7.95-17.44 12.15-8.76 7.23-15.73 15.43-20.89 25.4-15.61 30.2-28.36 49.32-37.16 55.19-7.98 5.32-21.97 9.39-59.17 18.07-13.65 3.18-24.98 8.39-34.82 15.42-7.22 5.16-12.27 10.01-19.92 18.43a86.07 86.07 0 0 1-6.57 6.4c-1.67 1.48-7.91 6.83-7.84 6.77-3.27 2.84-5.8 5.16-8.26 7.62a92.1 92.1 0 0 0-14.27 18.13c-4.3 7.16-3.22 11.89 2.53 15.26 4.47 2.63 9.88 3.99 23.24 6.39a185.7 185.7 0 0 1 12.92 2.6c12.11 3.03 20.64.84 32.06-5.96l4.4-2.65c1.66-1 2.96-1.73 4.2-2.35.95-.48 6.04-3.14 7.6-3.92 9.59-4.8 17.04-6.94 24.49-5.98a24.1 24.1 0 0 1 10.4 3.93c7.82 5.21 8.45 12.52 4.7 25.13-.32 1.07-.64 2.1-1.16 3.74-2.57 8.12-3.31 11.04-3.31 14.55 0 10.88-5.66 23.14-15.08 37.28a41.28 41.28 0 0 0-3.97 7.46c-.37.9-.73 1.82-1.18 3.04-3.25 8.85-4.21 11.13-7.84 17.47-7.67 13.42-33.43 16.95-47.7 8.18a578.4 578.4 0 0 0-6.4-3.89c-3.4-2.04-16.36-9.67-17.67-10.45-8.38-4.97-16.36-9.78-25.92-15.66-9.5-5.85-15.7-7.7-19.24-6.36-2.68 1.02-3.8 3.82-4 8.51a61.12 61.12 0 0 0-.02 3.72l.02 1.7c0 4.5-2.69 8.73-11.52 19.87-3.92 4.95-5.87 7.59-7.55 10.39-2.39 3.97-4.08 10.56-6.56 23.72l-1.05 5.62-.86 4.4zm10.5 0h-1c.03-.34.04-.68.04-1 0-12.39 8.48-33.57 19.16-43.37a26.18 26.18 0 0 0 3.67-4.17 35.8 35.8 0 0 0 2.88-4.9c.36-.72 1.75-3.66 2.1-4.36 3.22-6.29 6.84-6.54 16.97.39 1.34.9 6.07 4.16 6.4 4.38 2.62 1.8 4.67 3.2 6.7 4.56 5.03 3.39 9.37 6.2 13.51 8.7 14.33 8.67 25.49 13.27 34.11 13.27 16.86 0 32.71-5.95 39.6-14.8 1.59-2.04 3.2-5.17 5.06-9.63.8-1.92 1.64-4.06 2.67-6.8l2.74-7.33c4.66-12.44 7.76-19.06 11.56-23.27 7.9-8.79 14.87-36 14.87-52.67 0-1.9.17-3.11 1.02-8.27.37-2.2.58-3.6.74-5.07.63-5.51.21-9.46-1.68-12.39-4.6-7.1-19.7-9.23-38.46-4.78a100.57 100.57 0 0 0-18.94 6.3c-5.17 2.37-17.11 9.74-16.5 9.4-6.72 3.64-12.97 4.15-24.8 1.3-29.55-7.14-30.43-8.62-15.26-26.81 17.44-20.93 47.12-46.18 56.38-46.18 9.92 0 53.84-11.98 65.78-17.95 9.46-4.73 24.32-21.18 36.82-37.85.71-.95 13.5-21.6 19.2-29.6 9.35-13.13 18.22-22.55 26.95-27.53 7.29-4.17 13.16-10.28 18.8-18.73 1.93-2.9 10.52-17.65 12.73-20.41 1.54-1.93 3-3.21 4.52-3.89 14.07-6.25 24.22-9.04 39.2-9.04h29c4.05 0 7.36-.4 22.93-2.5l4.3-.57c9.92-1.3 16.57-1.93 21.77-1.93 1.66 0 2.95.01 6.03.04 18.61.19 28.55-.48 44.86-4.03 3.1-.67 6.13-1.78 9.11-3.31v1.12a37.96 37.96 0 0 1-8.9 3.17c-16.4 3.56-26.4 4.24-45.08 4.05-3.08-.03-4.36-.04-6.02-.04-5.15 0-11.76.63-21.64 1.92l-4.3.58c-15.64 2.11-18.94 2.5-23.06 2.5h-29c-14.81 0-24.84 2.75-38.8 8.96-1.34.6-2.69 1.78-4.14 3.6-2.16 2.68-10.72 17.39-12.68 20.33-5.72 8.57-11.7 14.8-19.13 19.04-8.57 4.9-17.36 14.23-26.63 27.24-5.68 7.97-18.47 28.64-19.22 29.63-12.6 16.8-27.52 33.32-37.18 38.15-12.06 6.03-56.14 18.05-66.22 18.05-8.82 0-38.39 25.15-55.62 45.82-14.6 17.52-14.19 18.21 14.74 25.2 11.6 2.8 17.6 2.3 24.09-1.2-.67.35 11.31-7.03 16.56-9.44 5.41-2.48 11.6-4.59 19.11-6.37 19.13-4.53 34.65-2.35 39.54 5.22 2.05 3.17 2.48 7.32 1.84 13.04a96.34 96.34 0 0 1-.75 5.13c-.84 5.08-1.01 6.29-1.01 8.1 0 16.9-7.03 44.33-15.13 53.33-3.68 4.09-6.76 10.65-11.37 22.96-.35.93-2.2 5.94-2.73 7.33-1.04 2.76-1.88 4.9-2.68 6.84-1.9 4.53-3.55 7.73-5.2 9.85-7.1 9.13-23.25 15.19-40.39 15.19-8.86 0-20.15-4.65-34.63-13.42-4.15-2.51-8.5-5.32-13.55-8.72a861.54 861.54 0 0 1-6.71-4.56l-6.4-4.39c-9.68-6.63-12.61-6.42-15.5-.75-.35.68-1.74 3.62-2.1 4.35a36.77 36.77 0 0 1-2.96 5.03c-1.12 1.57-2.37 3-3.81 4.33-10.47 9.6-18.84 30.51-18.84 42.63l-.03 1zm-29.65 0h-1.1c1.17-2.52 1.79-5.2 1.79-8 0-20 4.83-42.04 12.15-49.35 5.17-5.18 7.77-8.38 9.9-12.74 2.64-5.41 3.95-12 3.95-20.91 0-6.82 1.14-11.59 3.37-15.07 1.74-2.7 3.6-4.21 8.91-7.52a31.64 31.64 0 0 0 3.9-2.79c4.61-3.96 6.58-6.2 7.72-9.41 1.43-4.02.93-9.04-1.86-16.02a68.98 68.98 0 0 0-3.99-8.07l-.93-1.7a75.47 75.47 0 0 1-2.64-5c-5.16-10.71-3.77-18.9 7.68-29.78a204 204 0 0 1 26.81-21.55c3.96-2.69 16.8-10.8 19.24-12.5 1.99-1.4 4.33-3.3 7.77-6.3-.02 0 7.23-6.39 9.47-8.3 4.97-4.26 9.09-7.5 13.05-10.15 4.72-3.15 8.97-5.28 12.87-6.32 12.78-3.41 15.6-4.18 21.77-5.97 12.55-3.64 21.96-6.9 28.14-10a45.47 45.47 0 0 1 7.47-2.79c8.66-2.66 12.02-4.1 16.97-8.1 6.78-5.46 13.07-14.25 19.33-27.87 15.97-34.77 19.08-39.39 32.15-49.19 3.14-2.36 6.37-4.1 11.43-6.4l2.33-1.04c11.93-5.35 16.87-8.93 21.1-17.38 1.88-3.77 2.48-6.29 3.37-12.27.78-5.19 1.48-7.56 3.53-10.25 2.57-3.4 7.03-6.27 14.36-9.01 3.37-1.26 7.36-2.5 12.05-3.73 16.33-4.3 25.28-5.36 39.6-5.81 6.9-.22 9.5-.56 12.66-2 1.19-.54 2.36-1.23 3.58-2.11 3.7-2.7 8.14-4.54 13.24-5.67 5.71-1.27 10.69-1.54 18.7-1.45l2.35.02c2.82 0 6.8-1 19.7-4.69 10.83-3.08 15.95-4.31 19.3-4.31.82 0 1.9.13 3.55.41l5.01.9c9.82 1.68 17.44 1.89 25.15-.21 7.98-2.18 14.8-6.77 20.29-14.24V147c-5.47 7.04-12.21 11.42-20.03 13.55-7.88 2.15-15.63 1.94-25.58.23l-5-.9c-1.6-.26-2.64-.39-3.39-.39-3.2 0-8.32 1.22-19.74 4.48-12.35 3.53-16.3 4.52-19.26 4.52l-2.36-.02c-7.94-.1-12.85.17-18.47 1.42-4.97 1.11-9.3 2.9-12.88 5.5a21.4 21.4 0 0 1-3.75 2.22c-3.32 1.5-6 1.87-13.04 2.09-14.25.44-23.13 1.5-39.37 5.77a125.56 125.56 0 0 0-11.95 3.7c-7.17 2.7-11.49 5.46-13.93 8.68-1.9 2.52-2.58 4.76-3.33 9.8-.9 6.08-1.53 8.68-3.47 12.56a30.6 30.6 0 0 1-9.66 11.45c-3.12 2.26-5.95 3.73-11.93 6.4l-2.31 1.04c-5.01 2.27-8.18 3.99-11.25 6.29-12.9 9.68-15.93 14.17-31.85 48.8-6.31 13.76-12.7 22.68-19.6 28.25-5.08 4.1-8.53 5.57-17.3 8.27a44.64 44.64 0 0 0-7.33 2.73c-6.24 3.12-15.7 6.4-28.3 10.06a867.4 867.4 0 0 1-21.8 5.97c-3.77 1.01-7.93 3.1-12.56 6.19a137.35 137.35 0 0 0-12.95 10.07c-2.24 1.92-9.48 8.3-9.48 8.3a98.2 98.2 0 0 1-7.84 6.37c-2.46 1.72-15.32 9.83-19.26 12.5a203 203 0 0 0-26.69 21.45c-11.13 10.58-12.43 18.3-7.47 28.63a74.52 74.52 0 0 0 2.62 4.95l.94 1.7a69.84 69.84 0 0 1 4.03 8.17c2.88 7.2 3.4 12.46 1.89 16.73-1.22 3.43-3.28 5.77-8.02 9.84-1.14.97-2.32 1.8-5.3 3.67-3.92 2.45-5.69 3.89-7.31 6.42-2.13 3.3-3.22 7.89-3.22 14.53 0 9.05-1.34 15.79-4.05 21.34-2.19 4.49-4.85 7.77-10.1 13.01-7.07 7.07-11.85 28.9-11.85 48.65 0 2.8-.58 5.48-1.7 8zm282.54 0h-1.01l-1.1-5.8c-3.08-16.26-4.05-26.2-2.74-37.26.7-5.8.77-9.68.55-15.3-.18-4.45-.17-5.68.19-7.63.78-4.3 3.44-8.53 10.39-16.34 9.07-10.2 12.26-15.41 19.8-30.15 1.35-2.64 2.33-4.47 3.38-6.3.9-1.58 1.82-3.06 2.77-4.5 3.14-4.7 7.03-8.42 16.84-16.81 11.22-9.6 15.5-13.86 18.13-19.13.7-1.4 1.3-2.8 1.93-4.4a206 206 0 0 0 1.49-4.05c3.63-9.94 8.01-13.93 22.9-17.81 4.99-1.3 20.55-5.13 21.38-5.34 16.19-4.1 25.33-7.36 33.48-12.6 5.86-3.77 5.84-3.76 27.66-16.53l2.6-1.52c10.23-6 17.1-10.2 22.73-13.95a149.3 149.3 0 0 0 8.8-6.3 723.7 723.7 0 0 0 6.37-5.08A87.74 87.74 0 0 1 600 342.95v1.12a85.76 85.76 0 0 0-15.49 9.9c.18-.14-4.76 3.84-6.38 5.1a150.3 150.3 0 0 1-8.85 6.35c-5.65 3.76-12.53 7.96-22.78 13.97l-2.6 1.53c-21.8 12.75-21.78 12.74-27.63 16.5-8.27 5.32-17.49 8.61-33.78 12.73-.83.21-16.39 4.04-21.36 5.33-8.03 2.1-13.15 4.5-16.45 7.5-2.66 2.42-4 4.86-5.77 9.7l-1.5 4.07a51.12 51.12 0 0 1-1.96 4.47c-2.72 5.45-7.04 9.75-18.38 19.45-9.73 8.32-13.6 12.02-16.65 16.6a77.18 77.18 0 0 0-2.74 4.45c-1.05 1.81-2.01 3.63-3.35 6.25-7.58 14.81-10.82 20.08-19.96 30.36-6.83 7.7-9.4 11.78-10.15 15.86-.34 1.85-.34 3.04-.17 7.4.22 5.68.14 9.6-.55 15.47-1.3 10.92-.34 20.79 2.73 36.95l1.12 5.99zm-76.59 0h-2.1l1.39-4.3c1.04-3.3 1.93-6.78 2.68-10.4 2.65-12.73 3.27-23.63 3.27-41.3 0-5.71-1.86-9.75-4.13-9.75-2.94 0-6.96 5.61-10.93 17.08C271.14 579.68 258.3 593 238 593c-22.42 0-29.26-1.35-48.42-10.09a87.69 87.69 0 0 1-9.42-5.04c-2.95-1.8-12.78-8.57-14.84-9.72-4.2-2.36-7-2.71-9.72-.99-.63.4-1.26.91-1.9 1.55a57.69 57.69 0 0 1-4.31 3.86 147.88 147.88 0 0 1-3.06 2.44l-1 .8C137.01 582.43 134 587.18 134 597c0 1.02-.02 2.01-.07 3h-2c.05-.99.07-1.98.07-3 0-10.52 3.33-15.78 12.09-22.76a265.61 265.61 0 0 1 2-1.6c.83-.64 1.43-1.13 2.03-1.61a55.76 55.76 0 0 0 4.17-3.74c.74-.73 1.48-1.34 2.24-1.82 3.47-2.2 7-1.75 11.77.93 2.15 1.21 12.03 8 14.9 9.76a85.7 85.7 0 0 0 9.22 4.93C209.29 589.7 215.85 591 238 591c19.25 0 31.49-12.7 41.06-40.33 4.24-12.25 8.66-18.42 12.81-18.42 3.8 0 6.13 5.06 6.13 11.75 0 17.8-.63 28.8-3.3 41.7-.77 3.7-1.68 7.23-2.75 10.6-.4 1.3-.8 2.53-1.19 3.7zm-149.25 0l.5-.94a160.1 160.1 0 0 0 6.53-13.26c2.73-6.29 5.78-9.64 9.24-10.52 3.74-.95 7.15.74 12.56 5.13 5.43 4.4 6.07 4.86 7.73 5.1 1.6.22 4.28 1.14 8.86 2.95 1.3.5 10.78 4.35 13.85 5.55 3.07 1.2 5.85 2.25 8.49 3.18 3.1 1.1 5.98 2.04 8.65 2.81h-3.45c-1.76-.56-3.6-1.18-5.54-1.87a281.2 281.2 0 0 1-8.51-3.19c-3.08-1.2-12.57-5.04-13.86-5.55-4.5-1.78-7.15-2.68-8.63-2.9-1.94-.27-2.53-.7-8.22-5.3-5.17-4.2-8.36-5.78-11.69-4.94-3.1.78-5.94 3.92-8.56 9.95a161 161 0 0 1-6.82 13.8h-1.13zm112.89 0a30.34 30.34 0 0 0 11.27-6.27c1.55-1.36 3.32-3.46 5.34-6.29 1.05-1.46 2.15-3.1 3.41-5.04a349.73 349.73 0 0 0 2.5-3.9l.47-.75.93-1.47a89.17 89.17 0 0 1 3.25-4.86c1.05-1.43 1.82-2.23 2.44-2.46 1.02-.37 1.49.48 1.49 2.04l.01 2.11c.05 6.91-.08 11.32-.7 16.33a48.4 48.4 0 0 1-2.38 10.56h-1.07a46.47 46.47 0 0 0 2.45-10.68c.62-4.96.75-9.33.7-16.2l-.01-2.12c0-.97-.08-1.12-.15-1.1-.36.14-1.05.85-1.97 2.1a88.44 88.44 0 0 0-3.22 4.82l-.92 1.46-.48.75a1268.1 1268.1 0 0 1-2.5 3.92c-1.26 1.95-2.38 3.6-3.44 5.08-2.06 2.88-3.87 5.04-5.5 6.45a30.87 30.87 0 0 1-8.94 5.52h-2.98zm-183.72 0H69.3c3.37-3.43 5.19-8.33 5.19-15 0-18.6-.04-17.35 1.02-20.77.6-1.93 1.5-3.74 3.27-6.63.42-.7 4.92-7.8 6.78-10.86 3.04-4.97 11.04-16.5 12.21-18.56 3.48-6.08 4.72-12.06 4.72-24.18 0-7.85 2.5-14.2 8.1-23.44l2.84-4.63a72.67 72.67 0 0 0 2.49-4.4c1.62-3.15 2.48-5.78 2.62-8.28.2-3.78-1.3-7.29-4.9-10.9-5.13-5.12-8.6-5.43-11.2-1.85-2.12 2.92-3.48 7.74-5.06 16.47-.2 1.03-.82 4.6-.82 4.57-.83 4.67-1.4 7.33-2.1 9.6-1.35 4.42-3.7 7.61-8.36 12.26l-3.26 3.2c-6.38 6.39-9.68 11.51-11.36 19.5l-1.16 5.52c-.87 4.1-1.56 7.04-2.33 9.94-3.67 13.74-9.65 25.97-22.59 44.72-7.68 11.14-11.05 18.87-10.92 23.72h-1c-.12-5.16 3.35-13.05 11.1-24.28 12.87-18.67 18.8-30.8 22.44-44.42.77-2.88 1.45-5.8 2.32-9.89l1.16-5.51c1.73-8.22 5.13-13.5 11.64-20 .63-.64 2.84-2.8 3.25-3.21 4.57-4.54 6.82-7.62 8.12-11.84a81.58 81.58 0 0 0 2.07-9.48l.81-4.57c1.62-8.9 3-13.8 5.24-16.89 3-4.15 7.2-3.78 12.71 1.74 3.8 3.8 5.42 7.58 5.2 11.66-.15 2.66-1.05 5.41-2.73 8.68a73.6 73.6 0 0 1-2.52 4.46l-2.84 4.63c-5.52 9.1-7.96 15.3-7.96 22.92 0 12.28-1.28 18.43-4.85 24.68-1.2 2.1-9.21 13.65-12.22 18.58-1.87 3.06-6.37 10.18-6.78 10.86-1.73 2.82-2.6 4.57-3.17 6.4-1.02 3.28-.98 2.1-.98 20.48 0 6.52-1.7 11.44-4.82 15zM310.09 0h1.06c-.37.9-.77 1.83-1.2 2.82-3.9 9.06-5.45 15.15-5.45 25.18 0 7.64-2.1 11.6-6.64 13.05-3.46 1.1-5.72.98-17.57-.43-11.55-1.36-19.17-1.58-28.16-.14-6.24 2.49-25.91 7.02-32.13 7.02-11.15 0-36.76-2.88-54.12-7.01a22.08 22.08 0 0 0-16.95 2.48c-4.05 2.33-7.09 5.03-13.9 11.97-6.28 6.39-9.53 9.23-13.8 11.5-7.09 3.79-11.22 7.65-13.4 12.27-1.82 3.85-2.33 7.84-2.33 15.29 0 4.4-2.65 6.69-9.45 9.74.1-.05-2.97 1.31-3.84 1.71-8.78 4.06-12.71 8.29-12.71 16.55 0 12.52-4.86 19.22-17.34 27.96l-4.56 3.14c-1.9 1.3-3.3 2.3-4.67 3.3-.92.68-1.79 1.34-2.62 2-7.16 5.62-11 14.54-15.56 33.28-.63 2.57-3.3 14-4.07 17.14a350.44 350.44 0 0 1-5.2 19.33c-1.37 4.5-4.5 15.07-4.96 16.53-1.05 3.4-1.64 4.94-2.46 6.32-.82 1.4-6.85 9.08-12.64 18.27L0 277.98v-1.9l4.58-7.35a270.8 270.8 0 0 1 12.61-18.23c-.3.5 1.35-2.8 2.38-6.12.45-1.44 3.58-12.01 4.95-16.53 1.83-6.03 3.44-12.09 5.19-19.27.76-3.13 3.44-14.56 4.06-17.14 4.62-18.95 8.52-28.02 15.92-33.83.84-.67 1.72-1.33 2.65-2.01 1.38-1.02 2.8-2.01 4.7-3.32l4.54-3.14C73.83 140.57 78.5 134.13 78.5 122c0-8.74 4.2-13.26 13.29-17.45.88-.41 3.96-1.77 3.85-1.73 6.46-2.9 8.86-4.97 8.86-8.82 0-7.6.53-11.7 2.42-15.71 2.29-4.84 6.57-8.85 13.84-12.73 4.15-2.21 7.35-5 14.15-11.93 6.28-6.4 9.36-9.13 13.52-11.53a23.07 23.07 0 0 1 17.69-2.59c17.27 4.12 42.8 6.99 53.88 6.99 6.1 0 25.73-4.53 31.92-7 9.12-1.46 16.83-1.25 28.49.13 11.63 1.38 13.9 1.5 17.15.47 4.06-1.3 5.94-4.85 5.94-12.1 0-10.1 1.56-16.3 6.6-28zm25.12 0h1c.05 5.62.26 11.48.65 19.4.47 9.7.64 14.57.64 21.6 0 9.81-4.68 17.46-13.1 23.16-6.53 4.43-14.94 7.46-24.33 9.33-3.74.54-9.42.56-22.68.23-6.74-.17-9.35-.22-12.39-.22-2.77 0-4.97.43-7.63 1.36-.88.3-4.55 1.74-5.58 2.11-6.55 2.35-13.59 3.53-24.79 3.53-8.1 0-13.58-1.38-22.46-4.9l-3.18-1.25c-12.55-4.87-21.27-5.15-37.18 1.12-11.15 4.39-18.13 9.2-22.28 14.81-3.15 4.26-4.33 7.8-5.94 15.8-1.22 6.09-1.93 8.74-3.5 12.13-1.65 3.53-3.97 5.81-7.07 7.22-2.33 1.07-4.35 1.5-9.32 2.19-9.04 1.27-12.77 3.09-15.61 9.58-3.71 8.48-7.72 13.87-14.22 19.76-2.4 2.18-13.14 11.02-15.91 13.42-8.2 7.1-13.85 17.37-18.7 31.97a258.81 258.81 0 0 0-3.27 10.7c-.01.05-2.26 7.97-2.88 10.1-8.49 28.85-17.88 52.95-26.13 61.2-2.8 2.8-5.06 5.64-10.4 12.96-3.4 4.68-6.23 8.25-8.95 11.1v-1.55c2.74-2.98 5.73-6.82 9.48-11.97 4.03-5.52 6.32-8.4 9.17-11.24 8.07-8.08 17.44-32.14 25.87-60.8.62-2.1 2.86-10.03 2.88-10.08 1.21-4.24 2.21-7.53 3.28-10.74 4.9-14.75 10.63-25.16 19-32.4 2.78-2.42 13.5-11.25 15.89-13.4 6.4-5.8 10.32-11.09 13.97-19.43 1.68-3.83 4.05-6.31 7.2-7.86 2.4-1.17 4.64-1.67 9.53-2.36 4.54-.63 6.5-1.05 8.7-2.06 2.89-1.31 5.03-3.42 6.58-6.73 1.53-3.3 2.23-5.9 3.43-11.9 1.64-8.14 2.85-11.79 6.11-16.2 4.28-5.79 11.41-10.7 22.73-15.16 16.15-6.36 25.13-6.07 37.9-1.11l3.19 1.26c8.77 3.47 14.13 4.82 22.09 4.82 11.09 0 18.02-1.16 24.46-3.47 1-.36 4.68-1.8 5.58-2.11A22.5 22.5 0 0 1 265 72.5c3.05 0 5.67.05 14.07.26 11.53.29 17.2.27 20.83-.25 9.25-1.85 17.54-4.83 23.94-9.17C332 57.8 336.5 50.46 336.5 41c0-7-.17-11.86-.7-22.7-.35-7.26-.55-12.83-.59-18.3zM93.87 0h2.04c-.7 4-1.61 6.82-3.03 9.47-2.33 4.38-2.85 5.75-5.26 13.03a40.46 40.46 0 0 1-1.94 5.03c-2.24 4.66-5.92 8.8-13.07 14.26-8.01 6.13-14.27 16.55-20.03 31.55-2.4 6.23-8.75 25.63-9.64 28.01-2.69 7.16-6.56 12.7-15.63 23.68l-2.68 3.24c-6.02 7.34-9.35 12.07-11.72 17.15-2.3 4.94-7.12 9.9-12.91 14.15v-2.4c5.14-3.94 9.1-8.3 11.1-12.6 2.46-5.27 5.87-10.1 11.98-17.56l2.68-3.26c8.94-10.8 12.72-16.22 15.3-23.1.88-2.33 7.24-21.74 9.65-28.03 5.89-15.31 12.3-26 20.68-32.41 6.92-5.3 10.4-9.2 12.48-13.55.65-1.35 1.16-2.7 1.85-4.79 2.45-7.4 3-8.83 5.4-13.34A27.68 27.68 0 0 0 93.87 0zm9.07 0h1.02c-1.66 8.3-2.91 12.67-4.54 15.26a59.14 59.14 0 0 0-4.1 8.21c-1.27 3-2.44 6.2-3.5 9.4-.38 1.12-.7 2.16-2.41 5.39a251.48 251.48 0 0 0-12.81 13.3c-3.48 3.96-5.95 7.27-7.15 9.66-.95 1.9-2.06 5.99-3.61 12.97-.64 2.9-3.65 17.15-4.51 21.07-3.63 16.45-6.63 26.69-9.9 32-7.66 12.45-10.64 15.71-37.08 41.1A69.78 69.78 0 0 1 0 179.21v-1.15a69.39 69.39 0 0 0 13.65-10.42c26.4-25.33 29.32-28.55 36.92-40.9 3.2-5.18 6.18-15.37 9.78-31.7.86-3.91 3.87-18.16 4.51-21.06 1.57-7.09 2.7-11.2 3.7-13.2 1.24-2.5 3.76-5.86 7.29-9.89.9-1.03 1.86-2.1 2.86-3.18 2.4-2.6 4.96-5.22 7.53-7.76.9-.88 1.73-1.7 3.37-3.4a129.02 129.02 0 0 1 4.78-13.46 60.07 60.07 0 0 1 4.19-8.35c1.52-2.44 2.74-6.71 4.36-14.74zM83.71 0h1.1c-2.09 4.74-6.03 8.92-11.42 12.3-7.2 4.52-16.5 7.2-24.39 7.2-8.9 0-11.8 7-11.74 21.52 0 1.7.04 3.17.12 5.99.1 3.3.12 4.45.12 5.99 0 5.73-.76 11.3-2.01 16.5a66.67 66.67 0 0 1-2.15 6.97 2597.76 2597.76 0 0 1-7 15.86A4270.8 4270.8 0 0 1 6.44 136.2 54.64 54.64 0 0 1 0 147v-1.65a54.87 54.87 0 0 0 5.55-9.57A4269.82 4269.82 0 0 0 30.7 79.97c.53-1.2.99-2.23 2.44-5.9A69.23 69.23 0 0 0 36.5 53c0-1.52-.03-2.66-.12-5.95-.08-2.83-.12-4.31-.12-6.01-.03-6.79.53-11.62 2.07-15.34 1.94-4.68 5.39-7.19 10.67-7.19 7.7 0 16.81-2.63 23.86-7.05C77.93 8.27 81.66 4.38 83.7 0zm282.63 0h1.01c1.86 10.02 2.18 12.67 2.32 18.3a123.43 123.43 0 0 1 .37 27.83c-.96 8.78-3.1 16.01-6.63 21.15-11.34 16.5-39.8 29.22-66.41 29.22-5.09 0-10.47.28-16.31.83a413.8 413.8 0 0 0-24.37 3.16c-21.56 3.26-27.66 4.01-36.32 4.01-6.92 0-12.2-1.05-21.69-3.9l-2.78-.83c-1.39-.41-2.54-.74-3.65-1.02-8-2.05-14.22-2.04-21.7.72a16.32 16.32 0 0 0-9.17 8.18c-1.6 3.05-2.5 6.06-4.02 12.83-1.5 6.64-2.34 9.52-3.99 12.64a16.16 16.16 0 0 1-9.85 8.36 104.8 104.8 0 0 0-9.5 3.42c-6.55 2.8-10.1 5.57-13.8 10.47-1.33 1.75-1.03 1.3-5.43 7.9-1.98 2.97-4.66 5.8-8.48 9.14-2.01 1.76-10.71 8.83-12.88 10.7-7.37 6.35-12.58 12.14-16.63 19.14-4.22 7.3-7.8 18.3-11.28 33.26-.87 3.73-1.72 7.64-2.64 12.14l-1.18 5.8-1.09 5.45c-1.8 8.96-2.77 13.28-3.77 16.26-6.8 20.44-17.26 42.16-27.13 51.2-5.11 4.7-8.1 7.07-11.1 8.86-.9.54-1.84 1.04-2.92 1.57-.44.22-9.6 4.4-14.1 6.66l-1.22.62v-1.13l.78-.39c4.52-2.26 13.67-6.44 14.1-6.65a41.19 41.19 0 0 0 2.84-1.54c2.94-1.75 5.88-4.09 10.94-8.73 9.71-8.9 20.1-30.51 26.87-50.79.97-2.92 1.94-7.22 3.73-16.13l1.1-5.46a490.5 490.5 0 0 1 3.82-17.96c3.5-15.06 7.1-26.14 11.39-33.54 4.11-7.11 9.4-12.98 16.83-19.4 2.19-1.88 10.88-8.95 12.88-10.7 3.77-3.28 6.39-6.05 8.3-8.93 4.43-6.64 4.12-6.18 5.47-7.96 3.8-5.03 7.5-7.91 14.21-10.78 2.61-1.12 5.74-2.24 9.59-3.46a15.17 15.17 0 0 0 9.27-7.86c1.59-3.02 2.42-5.85 4.03-12.99 1.41-6.27 2.32-9.33 3.98-12.48a17.31 17.31 0 0 1 9.7-8.66c7.7-2.83 14.1-2.84 22.3-.75 1.12.29 2.28.61 3.68 1.03l3.73 1.11c8.47 2.54 13.66 3.58 20.46 3.58 8.59 0 14.67-.75 36.18-4a414.64 414.64 0 0 1 24.41-3.17c5.88-.54 11.29-.83 16.41-.83 26.3 0 54.45-12.58 65.59-28.78 3.42-4.98 5.5-12.06 6.46-20.7.84-7.74.73-16.02.02-23.9a136.2 136.2 0 0 0-.57-5.12c0-4.47-.3-6.94-2.16-17zM18.88 0h1.03C18 7.57 17.15 10.18 14.46 16.2c-1.95 4.37-2.67 9.19-2.42 14.89.2 4.33.71 7.7 2.28 16.13 1.09 5.88 1.57 8.77 1.94 12.2.96 8.9.24 16.08-2.8 22.79A463.4 463.4 0 0 1 0 109.43v-2.12a465 465 0 0 0 12.54-25.52c2.97-6.52 3.67-13.53 2.72-22.27-.36-3.4-.84-6.26-1.93-12.12-1.57-8.47-2.1-11.88-2.29-16.27-.26-5.84.48-10.81 2.5-15.33 2.64-5.9 3.48-8.47 5.34-15.8zm280.47 0a70.78 70.78 0 0 1-4.91 11.24c-2.56 4.7-4.01 8.45-4.86 11.98l-.4 1.8-.28 1.45a5.28 5.28 0 0 1-.74 2.07c-.74 1.03-1.93 1.28-5.13 1.25.92 0-9.85-.29-15.03-.29-10.2 0-18.45.82-29.46 2.56-16.87 2.66-17.73 2.77-23.66 2.52a42.57 42.57 0 0 1-8-1.09c-17.7-4.16-46.18-5.86-54.72-3.01-2.72.9-5.88 2.8-9.52 5.59a112.37 112.37 0 0 0-6.54 5.48c-1.4 1.25-9.17 8.5-10.78 9.84-1.45 1.2-8.18 7.42-8.85 8.02a114.65 114.65 0 0 1-4.55 3.9c-4.99 4.03-8.9 6.2-11.92 6.2-3.52.05-4.32 0-5.14-.4-1.13-.56-1.5-1.72-1.13-3.57.74-3.63 4.47-10.84 12.84-24.8 5.69-9.48 9.42-18 11.78-26.2 1.45-5.04 1.94-7.4 2.97-14.54h1.01c-1.05 7.3-1.54 9.7-3.01 14.82-2.39 8.28-6.16 16.89-11.9 26.44-8.3 13.84-12 21.01-12.7 24.48-.3 1.45-.08 2.14.59 2.47.6.3 1.35.35 3.48.3 3.92 0 7.69-2.1 12.5-5.98 1.4-1.13 2.87-2.39 4.51-3.86.66-.59 7.41-6.83 8.88-8.05 1.59-1.33 9.34-8.55 10.75-9.82 2.4-2.15 4.55-3.96 6.6-5.53 3.72-2.85 6.97-4.8 9.81-5.74 8.76-2.92 37.41-1.22 55.27 2.99 2.57.6 5.14.95 7.81 1.06 5.84.25 6.7.14 23.47-2.51 11.05-1.75 19.36-2.57 29.6-2.57 5.2 0 15.99.3 15.05.29 2.87.03 3.84-.17 4.3-.83.23-.32.4-.8.58-1.7l.28-1.43.4-1.85c.88-3.6 2.36-7.44 4.96-12.22 1.87-3.43 3.44-7 4.73-10.76h1.06zm-8.59 0c-5.91 17.94-9.55 22-19.76 22-4.5 0-10.22.32-28.69 1.5l-1.53.1c-15.6.99-23.47 1.4-28.78 1.4-5.35 0-13.24-.96-28.86-3.28l-1.54-.23C163.18 18.75 157.47 18 153 18c-4.45 0-7.3 1.01-10.96 3.34-.1.06-1.8 1.17-2.3 1.47-2.43 1.5-4.32 2.19-6.74 2.19-2.8 0-4.11-1.46-4.11-4.22 0-1.04.16-2.29.5-4.1.16-.82.9-4.4 1.07-5.32.8-4.11 1.3-7.68 1.47-11.36h2c-.17 3.82-.68 7.5-1.5 11.75-.19.94-.92 4.5-1.07 5.31a21.04 21.04 0 0 0-.47 3.72c0 1.7.46 2.22 2.11 2.22 1.99 0 3.55-.57 5.7-1.9.47-.28 2.15-1.37 2.26-1.44C144.92 17.14 148.12 16 153 16c4.62 0 10.3.74 28.9 3.51l1.53.23C198.93 22.04 206.8 23 212 23c5.25 0 13.11-.41 28.65-1.4l1.54-.1C260.73 20.32 266.43 20 271 20c8.95 0 12.15-3.4 17.66-20h2.1zM141.51 0h1.13c-2.06 3.86-2.63 5.1-2.77 6.19-.15 1.12.42 1.64 2.32 1.96 1.8.3 3.85.35 10.81.35 6.02 0 13 .56 21.35 1.62 3.95.5 8.03 1.1 13.13 1.89 24 3.7 22.5 3.49 26.83 3.49 24.02 0 51.83-2.24 60.45-6.94 2.88-1.57 5.05-4.49 6.6-8.56h1.07c-1.64 4.47-3.98 7.69-7.2 9.44-8.83 4.82-36.67 7.06-60.92 7.06-4.41 0-2.84.22-26.98-3.5-5.1-.8-9.17-1.38-13.1-1.88-8.31-1.06-15.26-1.62-21.23-1.62-7.04 0-9.1-.05-10.97-.37-2.38-.4-3.38-1.32-3.15-3.07.16-1.22.69-2.41 2.63-6.06zm76.4 0c5.69 1.64 10.37 2.5 14.09 2.5 9.59 0 16.7-.71 22.4-2.5h2.98C251.12 2.53 243.2 3.5 232 3.5c-4.5 0-10.32-1.21-17.53-3.5h3.45zM70.69 0c-2.87 3.27-6.95 5.39-12.02 6.53-3.98.89-7.5 1.08-12.92 1A97.24 97.24 0 0 0 44 7.5c-5.37 0-8.86-1.24-10.1-4.97A8.6 8.6 0 0 1 33.5 0h.99c.02.82.14 1.56.36 2.22C35.91 5.39 39.02 6.5 44 6.5l1.76.02c5.35.09 8.8-.1 12.69-.97C62.95 4.54 66.63 2.74 69.3 0h1.37zM0 207.87c7.31-.16 11.5 3.33 11.5 11.13 0 11.41-5.05 28.35-11.5 41.5v-2.3c5.93-12.72 10.5-28.47 10.5-39.2 0-7.18-3.7-10.3-10.5-10.13v-1zm0 7.05c1.23.14 2.18.58 2.87 1.31 1.4 1.48 1.6 3.72 1.16 7.58l-.16 1.3A28.93 28.93 0 0 0 3.5 229c0 3.2-1.48 9.52-3.5 15.9v-3.45c1.49-5.13 2.5-9.87 2.5-12.45 0-.98.08-1.75.37-4.02l.16-1.29c.42-3.56.24-5.59-.88-6.77-.5-.53-1.21-.87-2.15-1v-1zM0 410.9v-1.47a21.67 21.67 0 0 0 2.97-4.7c1.32-2.7 2.68-6.28 4.56-11.89 7.85-23.55 7.83-26.6.25-30.4-2.25-1.12-4.8-1.43-7.78-.91v-1.02a13.1 13.1 0 0 1 8.22 1.04c8.24 4.12 8.26 7.6.25 31.6-1.88 5.66-3.25 9.27-4.6 12.02A20.82 20.82 0 0 1 0 410.9zM33.64 452c1.68 0 3.04-.23 8.34-1.31l2.38-.47c8.26-1.57 12.72-1.3 14.53 2.33 1.38 2.75-.47 5.86-4.75 9.68a75.6 75.6 0 0 1-5.08 4.07c-.94.7-4.89 3.59-5.79 4.27-1.86 1.4-2.97 2.37-3.47 3.03a19.08 19.08 0 0 0-2.89 5.5c.07-.2-4.02 13.65-6.96 22.22-2.7 7.85-5.56 10.72-8.82 8.59-2.11-1.4-3.66-4.24-6.6-11.03-1.98-4.62-2.5-5.76-3.4-7.4-4.55-8.18-3.9-23.9-.05-32.87a9.6 9.6 0 0 1 6.98-5.96c2.59-.66 4.86-.75 11.78-.67l3.8.02zm0 2c-1.13 0-2.09 0-3.82-.02-12.07-.13-14.83.57-16.9 5.41-3.63 8.47-4.26 23.55-.05 31.12.96 1.73 1.48 2.88 3.5 7.58 2.72 6.3 4.24 9.08 5.86 10.14 1.64 1.08 3.5-.8 5.82-7.55a682.9 682.9 0 0 0 6.97-22.24 21.03 21.03 0 0 1 3.18-6.04c.65-.87 1.85-1.9 3.86-3.43.92-.7 4.87-3.57 5.8-4.27 2.02-1.5 3.6-2.77 4.95-3.97 3.63-3.23 5.09-5.7 4.3-7.28-1.21-2.42-5.07-2.65-12.38-1.27l-2.35.47c-5.49 1.11-6.86 1.35-8.74 1.35zm345.63 146c-3.45-12.26-3.77-14.13-3.77-19 0-3.33-.13-6.27-.43-11.34-.63-10.33-.65-13.5.26-17.07 1.21-4.74 4.21-7.1 9.67-7.1h26c4.08 0 5.19 1.85 5.93 7.11.1.79.13.97.19 1.32.84 5.35 2.8 7.58 8.88 7.58 3.64 0 5.54.4 6.43 1.37.76.83.76 1.44.36 3.93-.85 5.26.5 8.85 7.5 13.8 6.32 4.45 11.63 5.36 16.55 3.37 3.8-1.54 6.73-4.16 11.92-10l1.1-1.23 1.09-1.23a75.6 75.6 0 0 1 2.7-2.86 35.81 35.81 0 0 1 9.57-6.73c1.52-.76 1.72-.86 5.66-2.63 6.1-2.73 9.01-4.5 11.74-7.62 2.63-3 4.67-4.85 6.7-6.04 3.18-1.85 5.46-2.13 13.68-2.13 5.98 0 10.56-4.32 18-14.99l2.82-4.03c1.06-1.5 1.94-2.7 2.79-3.79 7.87-10.12 19.38-10.4 30.74.96 5.54 5.53 10.17 19.43 13.64 38.51 2.5 13.75 4.18 29.46 4.47 39.84h-1c-.3-10.32-1.96-25.97-4.45-39.66-3.43-18.87-8.02-32.65-13.36-37.99-10.95-10.95-21.76-10.68-29.26-1.04-.83 1.07-1.7 2.26-2.75 3.75l-2.81 4.02c-7.65 10.95-12.38 15.42-18.83 15.42-8.04 0-10.21.26-13.17 2-1.92 1.12-3.9 2.9-6.45 5.83-2.86 3.26-5.87 5.09-12.09 7.88a103.35 103.35 0 0 0-5.62 2.6 34.84 34.84 0 0 0-9.32 6.54 74.67 74.67 0 0 0-3.75 4.05l-1.1 1.24c-5.28 5.95-8.29 8.64-12.28 10.25-5.26 2.13-10.92 1.17-17.5-3.48-7.33-5.17-8.82-9.15-7.92-14.77.34-2.12.34-2.6-.1-3.1-.64-.69-2.34-1.04-5.7-1.04-6.63 0-8.96-2.63-9.87-8.42l-.2-1.34c-.67-4.82-1.53-6.24-4.93-6.24h-26c-5 0-7.6 2.04-8.7 6.34-.88 3.43-.85 6.57-.23 16.76a177 177 0 0 1 .43 11.4c0 4.78.32 6.63 3.81 19h-1.04zm13.68 0c-1.31-6.58-1.61-10.71-1.36-14.84.04-.7.1-1.44.18-2.38l.23-2.56c.34-3.81.5-6.97.5-11.22 0-4.94 1.46-7.76 4.21-8.42 2.38-.58 5.56.54 9.2 3 6.64 4.52 13.99 13.07 16.55 19.23 4.77 11.44 14.12 15.69 33.54 15.69 8.6 0 14.32-2.35 20.67-7.88 1.45-1.26 15.06-15 21-20 7.21-6.07 11.77-7.59 20.62-8.32 5.52-.45 7.98-.9 11.44-2.36 4.58-1.95 9.36-5.48 14.9-11.29 7.43-7.76 13.25-8.92 17.47-4.3 3.32 3.63 5.46 10.58 6.82 20.24.73 5.17.94 7.74 1.58 17.38.25 3.75.17 5.32-.92 18.03h-1c1.09-12.7 1.17-14.28.92-17.97-.64-9.6-.85-12.16-1.57-17.3-1.33-9.47-3.43-16.27-6.56-19.7-3.76-4.11-8.93-3.08-16 4.32-5.65 5.9-10.54 9.5-15.25 11.5-3.58 1.53-6.13 1.99-11.6 2.44-8.8.72-13.17 2.18-20.2 8.1-5.9 4.96-19.5 18.7-21 19.99-6.52 5.68-12.47 8.12-21.32 8.12-19.78 0-29.5-4.42-34.46-16.3-2.49-5.97-9.71-14.38-16.2-18.79-3.42-2.32-6.36-3.35-8.4-2.86-2.2.53-3.44 2.92-3.44 7.45 0 4.28-.16 7.47-.5 11.31l-.23 2.56c-.09.93-.14 1.65-.19 2.35-.24 4.08.06 8.18 1.39 14.78h-1.02zm113.75 0c2.52-3.26 8.93-11.79 10.9-14.3 5.48-6.98 13.05-12.38 19.4-13.94 7.01-1.71 11.5 1.45 11.5 9.24 0 4.02-.04 5.16-.74 19h-1c.7-13.85.74-15 .74-19 0-7.12-3.86-9.83-10.26-8.26-6.11 1.5-13.5 6.77-18.85 13.57-1.86 2.36-7.65 10.07-10.43 13.69h-1.26zm-9.86-338.96c3.44 2.71 7 5.1 11.44 7.75 1.06.64 8.42 4.9 10.35 6.1 11.27 7 15 13.35 12.35 25.33-1.45 6.52-4.53 11.1-9.39 14.44-3.83 2.63-8.07 4.26-16.08 6.56-11.97 3.45-13.68 3.99-18.82 6.28a60.18 60.18 0 0 0-7.81 4.18c-11.11 7.07-19.1 7.7-27.96 3.28-3.56-1.77-17.2-11-17.2-11.01a101.77 101.77 0 0 0-5.2-3.07c-16.04-8.83-34.27-24.16-34.52-31.85-.11-3.46 1.99-6.57 6.28-10.26 1.03-.9 2.18-1.81 3.68-2.95.72-.55 3.38-2.56 3.94-3 4.47-3.4 7.18-5.79 9.32-8.45 11.12-13.82 26.55-28.68 34.36-32.28 12.06-5.54 19.84-5.77 27.37.12 3.25 2.54 5.65 6.54 8.58 13.35.29.65 2.3 5.45 2.88 6.74 1.62 3.65 2.9 5.8 4.24 6.94.72.6 1.45 1.2 2.2 1.8zm-3.49-.28c-1.63-1.39-3.03-3.74-4.77-7.65-.58-1.3-2.6-6.12-2.88-6.76-2.81-6.5-5.08-10.3-7.98-12.56-6.83-5.35-13.85-5.15-25.3.12-7.45 3.42-22.7 18.12-33.64 31.72-2.27 2.82-5.08 5.3-9.67 8.79l-3.94 2.98a79.98 79.98 0 0 0-3.59 2.88c-3.87 3.33-5.67 6-5.58 8.69.21 6.64 18.14 21.72 33.48 30.15 1.76.97 3.5 2 5.3 3.13.12.08 13.61 9.22 17.03 10.92 8.22 4.1 15.46 3.52 26-3.18a62.17 62.17 0 0 1 8.07-4.31c5.25-2.35 7-2.9 19.08-6.38 7.8-2.24 11.9-3.82 15.5-6.3 4.44-3.04 7.23-7.18 8.56-13.22 2.44-11.02-.83-16.6-11.45-23.2-1.9-1.18-9.23-5.42-10.32-6.08-4.5-2.69-8.13-5.12-11.64-7.9-.77-.6-1.52-1.21-2.26-1.84zM87.72 241.6c4.3-2.98 7.88-5 12.14-6.95.84-.4 1.73-.78 2.78-1.24l4.37-1.88a164.3 164.3 0 0 0 17.74-8.96 320.67 320.67 0 0 1 27.87-14.5c4.22-1.95 21.89-9.84 21.17-9.52 19.17-8.62 28.1-6.93 49.5 8.05 7.91 5.54 13.24 13.25 16.45 22.66 3.02 8.83 3.76 16.51 3.76 27.75 0 8.32-.66 12.95-3.68 18.97-4.18 8.36-12.3 16.14-25.58 23.47-24.45 13.49-38.83 27.55-52.83 47.84-8.83 12.8-47.76 44.21-65.16 54.15C75.04 413.55 48.89 423.5 31 423.5c-10.05 0-14.67-4.78-14.76-13.37-.07-6.32 2.06-13.73 6.3-24.32 2.95-7.37 2.02-12.9-2.16-22.29-3.19-7.17-3.88-9.14-3.88-12.52 0-3.35 1.87-6.9 5.52-11.07 2.61-3 3.5-3.83 11.9-11.5 5.09-4.66 8.08-7.6 10.7-10.75 9.46-11.36 12.62-19.47 17.9-44.78 3.12-15.05 6.63-20.28 15.12-25.25.8-.47 3.95-2.25 4.7-2.68a76.66 76.66 0 0 0 5.38-3.38zm.56.82a77.63 77.63 0 0 1-5.44 3.43l-4.7 2.67c-8.23 4.82-11.57 9.81-14.65 24.6-5.3 25.45-8.51 33.7-18.1 45.21-2.66 3.19-5.68 6.16-10.8 10.84-8.36 7.64-9.24 8.48-11.82 11.42-3.5 4.01-5.27 7.36-5.27 10.42 0 3.18.68 5.1 3.8 12.12 4.27 9.6 5.24 15.37 2.16 23.07-4.18 10.47-6.29 17.78-6.22 23.93.08 8.06 4.26 12.38 13.76 12.38 17.67 0 43.68-9.9 64.75-21.93 17.28-9.88 56.1-41.2 64.84-53.85 14.08-20.42 28.57-34.59 53.17-48.16 13.12-7.23 21.09-14.87 25.17-23.03 2.92-5.86 3.57-10.35 3.57-18.53 0-11.13-.74-18.73-3.7-27.43-3.15-9.22-8.36-16.75-16.09-22.16-21.13-14.8-29.7-16.42-48.5-7.95.7-.32-16.96 7.56-21.17 9.5-1.7.8-3.3 1.55-4.86 2.3a319.68 319.68 0 0 0-22.93 12.17 165.3 165.3 0 0 1-17.85 9.01l-4.37 1.88c-1.04.45-1.92.84-2.76 1.23a74.56 74.56 0 0 0-11.99 6.86zm-7.6 12.2c7.7-6.25 12.3-8.17 23.68-11.27 6.12-1.67 9.12-2.95 12.31-5.72 3.8-3.3 7.47-4.52 15.86-6.1 2.75-.52 3.67-.7 5.06-1.02 5.48-1.24 9.48-2.93 13.1-5.89 10.42-8.53 25.4-14.11 36.31-14.11 5.33 0 16.77 7.58 25.74 17.16 10.73 11.46 15.96 23.27 12.73 32.5-3.18 9.1-11.39 18.57-23.03 27.86-8.44 6.73-18.36 13-25.22 16.43-3.72 1.86-6.59 4.88-9.77 9.99-.69 1.1-11.1 20.25-16.03 27.83-5.62 8.65-15.4 17.36-30.23 27.96a552.58 552.58 0 0 1-9.2 6.42c-.13.09-6.81 4.65-8.6 5.89-6.47 4.46-10.35 7.35-13.05 9.83-11.64 10.67-37.14 15.54-43.7 8.98-1.96-1.96-2.2-4.06-1.95-10.52.37-9.42-.5-14.5-4.95-20.51a34.09 34.09 0 0 0-7.04-6.92c-3.93-2.95-6.07-6.11-6.56-9.49-.97-6.61 3.87-13.06 14.17-21.69 1.58-1.32 6.67-5.44 7.09-5.78a48.03 48.03 0 0 0 5.23-4.77c4.1-4.63 5.85-9.55 7.8-20.07a501.52 501.52 0 0 0 .8-4.37c.33-1.87.6-3.3.88-4.73.74-3.78 1.5-7.18 2.4-10.63 1-3.78 1.38-5.5 2.36-10.37.6-3.02.93-4.21 1.56-5.47 1.22-2.45 1.27-2.5 12.25-11.42zm.64.78c-10.77 8.74-10.88 8.84-12 11.08-.58 1.16-.88 2.3-1.47 5.22-.98 4.89-1.36 6.63-2.37 10.44-.9 3.43-1.65 6.8-2.39 10.56a339.79 339.79 0 0 0-1.29 6.95l-.39 2.15c-1.98 10.68-3.77 15.74-8.04 20.54a48.77 48.77 0 0 1-5.34 4.88c-.42.34-5.5 4.47-7.07 5.78-10.04 8.4-14.72 14.65-13.83 20.78.45 3.1 2.44 6.03 6.17 8.83 3 2.25 5.39 4.62 7.24 7.12 4.63 6.24 5.52 11.52 5.15 21.15-.25 6.14-.01 8.1 1.66 9.78 6.1 6.1 31.02 1.33 42.31-9.02 2.75-2.52 6.66-5.43 13.16-9.92l8.6-5.89c3.63-2.48 6.45-4.44 9.19-6.4 14.73-10.54 24.44-19.18 29.97-27.7 4.9-7.54 15.31-26.68 16.02-27.8 3.27-5.26 6.26-8.41 10.18-10.37 6.79-3.4 16.65-9.63 25.03-16.32 11.52-9.18 19.61-18.53 22.72-27.4 3.07-8.78-2.02-20.27-12.52-31.49-8.8-9.4-20.04-16.84-25.01-16.84-10.67 0-25.43 5.5-35.68 13.89-3.76 3.07-7.9 4.81-13.5 6.09-1.41.32-2.35.5-5.11 1.02-8.21 1.55-11.76 2.73-15.38 5.88-3.34 2.9-6.45 4.22-12.7 5.92-11.26 3.07-15.75 4.94-23.31 11.09zM212 251.85c0 7.56-.6 10.92-2.6 14.3-1.1 1.84-7.66 10.05-8.6 11.3-5.96 7.94-9.33 10.28-17.26 13.76-1.34.58-2.2 1-3.03 1.5-.55.33-1.2.66-2 1.02-.71.33-4.46 1.9-5.52 2.39-6.05 2.78-8.99 5.8-8.99 10.73 0 10.97-18.95 36.12-34.51 44.87-8.18 4.6-21.3 9.36-32.78 11.86-13.33 2.9-22.49 2.48-24.62-2.32-1.32-2.97-4.4-4.26-11.98-5.81l-.6-.12c-4.84-.99-6.94-1.55-9.03-2.64-2.92-1.5-4.48-3.7-4.48-6.84 0-2.74 1.08-5.77 3.25-9.67.85-1.53 1.82-3.13 3.23-5.35-.16.25 2.83-4.4 3.67-5.76 6.69-10.7 9.85-18.5 9.85-27.22 0-18.41 11.22-33.37 27.5-42.86 5.22-3.05 9.23-3.31 15.2-2.12 5.04 1 6.05.9 7.43-1.52 4.5-7.85 7.04-9.5 15.87-9.5 3.93 0 6.97-.98 10.47-3.16 1.56-.97 8.67-6.17 10.99-7.68 9.2-5.98 11.34-7 25.2-11.95 6.95-2.48 15.18 1.28 22.33 9.12 6.55 7.19 11.01 16.61 11.01 23.67zm-2 0c0-6.5-4.25-15.48-10.49-22.32-6.67-7.32-14.16-10.74-20.17-8.59-13.73 4.9-15.73 5.85-24.8 11.75-2.24 1.46-9.37 6.68-11.01 7.7-3.8 2.36-7.2 3.46-11.53 3.46-8.08 0-9.98 1.23-14.13 8.5-1.1 1.91-2.51 2.88-4.35 3.09-1.3.14-1.9.05-5.22-.61-5.53-1.1-9.07-.88-13.8 1.88-15.72 9.17-26.5 23.55-26.5 41.14 0 9.2-3.28 17.29-10.15 28.28l-3.68 5.77c-1.39 2.19-2.35 3.77-3.17 5.25-2.02 3.63-3 6.38-3 8.7 0 4.19 2.87 5.67 11.9 7.52l.61.12c8.27 1.7 11.7 3.13 13.4 6.95 3.17 7.14 36 0 54.6-10.46 14.98-8.43 33.49-32.99 33.49-43.13 0-5.9 3.47-9.48 10.16-12.55 1.1-.5 4.85-2.08 5.52-2.38.74-.34 1.32-.64 1.8-.93.92-.55 1.85-1 3.25-1.62 7.65-3.35 10.75-5.5 16.47-13.12 1.02-1.36 7.47-9.42 8.47-11.11 1.79-3.01 2.33-6.06 2.33-13.3zm-37.18-22.4c.15-.1 2.4-1.51 2.95-1.84.96-.57 1.7-.94 2.43-1.17 2.57-.83 5.06-.1 11.04 3.12 14.86 8 19.43 22.87 9.18 38.71-4.04 6.24-9.37 9-18.72 11.11-.85.2-1.2.27-3.13.68-6.04 1.29-8.78 2.08-11.6 3.65-3.63 2.02-6.09 4.98-7.5 9.44-7.87 24.93-19.72 43.34-36.28 50.31-16.45 6.93-21.13 8.53-27.98 8.89-4.94.25-9.8-.65-15.4-2.89a44.45 44.45 0 0 1-5.64-2.6c-4.02-2.33-5.14-4.74-4.5-9.31.3-2.13 3.77-15.53 4.84-20.65.63-3.05 1.19-6.14 1.75-9.69a464.04 464.04 0 0 0 1.35-8.9c1.42-9.41 2.5-14.27 4.49-18.65 2.46-5.43 6.13-9.03 11.72-11.13 6.59-2.47 10.54-3.1 18.03-3.53 4.75-.27 6.68-.64 9-2.05.61-.37 1.22-.81 1.82-1.33a30.61 30.61 0 0 0 3.37-3.4c.59-.69 2.38-2.9 2.63-3.19 3.36-4 6.3-5.53 12.33-5.53 3.94 0 5.9-.92 8.18-3.36-.17.18 2.75-3.14 3.85-4.22a30.95 30.95 0 0 1 6.79-5c1.5-.83 3.15-1.62 4.99-2.38a64.92 64.92 0 0 0 10.01-5.1zm-14.52 8.34a29.95 29.95 0 0 0-6.57 4.84 116.68 116.68 0 0 0-3.82 4.2c-2.46 2.63-4.68 3.67-8.91 3.67-5.72 0-8.39 1.39-11.57 5.17-.23.28-2.03 2.5-2.63 3.2a31.6 31.6 0 0 1-3.47 3.51c-.65.55-1.3 1.03-1.96 1.43-2.5 1.51-4.55 1.9-9.47 2.19-7.39.42-11.25 1.04-17.72 3.47-5.34 2-8.82 5.4-11.17 10.6-1.93 4.27-3 9.07-4.41 18.39l-.65 4.34-.7 4.57c-.57 3.56-1.12 6.67-1.76 9.73-1.08 5.18-4.54 18.53-4.83 20.59-.59 4.17.35 6.18 4.01 8.3 1.35.77 3.1 1.58 5.52 2.55 5.46 2.18 10.18 3.05 14.97 2.8 6.69-.34 11.32-1.93 27.65-8.8 16.21-6.83 27.92-25.01 35.71-49.7 1.49-4.7 4.12-7.86 7.97-10 2.93-1.63 5.74-2.45 11.87-3.76 1.92-.4 2.28-.49 3.12-.68 9.12-2.06 14.24-4.7 18.1-10.67 9.92-15.34 5.55-29.55-8.82-37.29-5.75-3.1-8.03-3.76-10.25-3.05-.65.2-1.33.54-2.23 1.08-.55.32-2.77 1.72-2.93 1.82a65.91 65.91 0 0 1-10.16 5.17c-1.8.75-3.42 1.52-4.89 2.33zm-42.39 32.72c16.15-2.87 26.36-.97 32.47 6.16 5.08 5.93 1.13 21.42-5.93 35.55-4.79 9.58-10.6 16.21-23.16 25.19-14.15 10.1-35.5 12.2-40.71 3.85-1.86-2.97-2.1-8.14-1.06-15.73.78-5.68 1.86-10.71 4.73-22.98l.12-.51c1.59-6.8 2.37-10.31 3.14-14.14 1.45-7.25 3.74-11.47 7.26-13.74 2.81-1.8 5.53-2.28 12.33-2.62 5.33-.27 7.56-.46 10.81-1.03zm.18.98c-3.3.59-5.56.78-10.94 1.05-6.62.33-9.23.78-11.84 2.46-3.25 2.1-5.42 6.09-6.82 13.1-.77 3.84-1.56 7.35-3.15 14.17l-.12.5c-2.86 12.24-3.93 17.26-4.7 22.9-1.03 7.36-.79 12.36.9 15.07 4.82 7.7 25.54 5.67 39.29-4.15 12.43-8.88 18.13-15.39 22.84-24.81 6.86-13.72 10.75-29 6.07-34.45-5.84-6.81-15.7-8.65-31.53-5.84zM132 276.5c7.12 0 10.66 3.08 11.25 8.7.42 4.02-.43 8.14-2.77 15.94-2.56 8.52-18.36 25.38-27.2 31.28-7.01 4.67-20.02 5.67-26.57.99-3.99-2.85-3.53-12.08.02-26.46.68-2.75 1.47-5.65 2.37-8.76a412.6 412.6 0 0 1 3.05-10.14l.37-1.2c1.48-4.8 5.1-7.75 10.73-9.27 4.4-1.2 9.54-1.5 17.48-1.33l3.89.1c3.87.11 5.42.15 7.38.15zm0 1c-1.97 0-3.53-.04-7.41-.15l-3.88-.1c-7.85-.17-12.92.13-17.2 1.3-5.32 1.43-8.67 4.16-10.03 8.6a1277.83 1277.83 0 0 1-1.6 5.21c-.68 2.2-1.27 4.17-1.82 6.1-.9 3.1-1.68 5.99-2.36 8.73-3.43 13.88-3.87 22.93-.4 25.4 6.17 4.42 18.73 3.45 25.42-1 8.66-5.78 24.33-22.49 26.8-30.73 2.3-7.67 3.14-11.71 2.73-15.56-.53-5.1-3.64-7.8-10.25-7.8zm-17.79 7a31.3 31.3 0 0 1 8.57 1.4c5.42 1.78 8.72 5.03 8.72 10.1 0 9.59-9.51 17.2-22.34 21.47-9.82 3.28-13.62-1.79-11.66-16.54.84-6.28 3.82-10.67 8.24-13.46a20.38 20.38 0 0 1 8.47-2.97zm-.6 1.08a19.39 19.39 0 0 0-7.34 2.73c-4.18 2.64-6.98 6.78-7.77 12.76-1.89 14.11 1.36 18.45 10.34 15.46C121.3 312.37 130.5 305 130.5 296c0-4.56-2.98-7.5-8.03-9.15a28.05 28.05 0 0 0-8.2-1.35c-.13 0-.35.03-.66.08zm80.87-23.45c-2.72 9.8-14.93 9.86-26.72 3.3-10.17-5.64-13.8-17.98-5-22.87a66.53 66.53 0 0 0 4.48-2.7l2.03-1.3a50.15 50.15 0 0 1 3.92-2.3c4.73-2.43 8.82-2.8 14-.72 9.16 3.66 10.98 13.33 7.3 26.6zm-20.83-24.98a49.26 49.26 0 0 0-3.84 2.25l-2.03 1.3c-.84.53-1.5.95-2.16 1.35-.82.5-1.6.96-2.38 1.39-7.94 4.4-4.59 15.8 5 21.12 11.31 6.29 22.8 6.23 25.28-2.7 3.57-12.83 1.85-21.97-6.7-25.4-4.9-1.95-8.69-1.62-13.17.7zm17.85 12.15c0 5.7-2.44 9-6.64 9.96-3.3.76-7.56-.05-11.08-1.81l-1.89-.94c-.67-.34-1.18-.62-1.63-.88-4.07-2.38-4.13-4.97.34-10.93 6.8-9.06 20.9-7.16 20.9 4.6zm-1 0c0-5.3-2.87-8.55-7.32-9.16-4.23-.57-8.99 1.44-11.78 5.16-4.15 5.54-4.1 7.44-.64 9.47.44.25.93.51 1.59.85l1.87.93c3.34 1.67 7.36 2.44 10.42 1.74 3.73-.86 5.86-3.74 5.86-9zM387 530.3c0-12.8 2.44-16.74 18.48-29.77a56.8 56.8 0 0 1 7.61-5.2c2.6-1.5 5.33-2.82 8.5-4.18 1.24-.53 2.48-1.05 4.1-1.7l3.92-1.57c9.4-3.83 13.74-6.7 16.62-12.05 1.2-2.22 2.21-4.4 3.23-6.83a148.57 148.57 0 0 0 1.54-3.84l.3-.74.56-1.44c3.2-8.02 6.05-12.08 12.7-16.5a35.26 35.26 0 0 0 4.96-4 46.36 46.36 0 0 0 3.88-4.29c.27-.34 2.55-3.2 3.2-3.98 3.48-4.15 6.51-5.9 11.51-5.9 3.08 0 5.62-.63 9.57-2.1 5.42-2.02 6.53-2.34 8.96-2.2 2.53.13 4.85 1.26 7.18 3.59 1.3 1.3 5.55 5.83 6.52 6.78 5.06 5 9.44 6.92 17.77 6.92a197.5 197.5 0 0 1 12.08.45c15.93.87 21.94.57 25.28-2.21 6.91-5.77 11.64-2.73 11.64 7.76 0 10.73-8.6 20-19 20-4.8 0-8.32 1.43-9.34 3.67-1.12 2.48.68 6.15 5.98 10.57 13.6 11.33 11.24 20.76-7.64 20.76a21.91 21.91 0 0 0-14.6 5.24c-3.28 2.71-5.8 5.86-9.85 11.82l-1.52 2.25c-3.1 4.57-5.01 7.1-7.32 9.4-6.21 6.21-9.3 7.64-13.05 6.89l-1-.23a10.82 10.82 0 0 0-2.66-.37c-1.6 0-2.41.67-8.18 6.22-4.85 4.67-8.07 6.78-11.82 6.78-1.33 0-3.46 1.15-6.45 3.45-1.27.98-2.68 2.14-4.5 3.7l-4.92 4.29a181.11 181.11 0 0 1-4.54 3.82c-9.33 7.56-15.63 10.2-20.21 6.52-2.7-2.15-4.14-4.51-4.63-7.26-.37-2.04-.26-3.63.29-7.3.87-5.85.65-8.42-1.83-11.6-2.32-2.98-2.96-3.22-3.77-2.39-.25.26-1.35 1.63-1.61 1.94-2.21 2.5-4.85 3.57-9 2.82-4.6-.84-5.57-4.11-4.72-10.09l.24-1.56c.6-3.66.68-4.93.25-5.8-.44-.86-1.9-.94-5.23.4l-.74.29c-13.78 5.54-15.26 6.09-19.43 6.67-6.03.84-9.31-1.6-9.31-7.9zm2 0c0 5 2.14 6.6 7.04 5.92 3.91-.55 5.43-1.1 18.95-6.55l.75-.3c4.17-1.66 6.7-1.54 7.76.58.71 1.43.62 2.76-.06 7l-.24 1.53c-.72 5.04-.06 7.27 3.09 7.84 3.43.62 5.38-.17 7.15-2.18.2-.23 1.34-1.66 1.68-2 1.9-1.96 3.82-1.25 6.78 2.55 2.9 3.74 3.17 6.77 2.22 13.12-1 6.75-.52 9.4 3.62 12.71 3.49 2.8 9.1.45 17.7-6.51 1.35-1.1 2.75-2.28 4.49-3.78l4.93-4.3c1.84-1.58 3.27-2.76 4.58-3.77 3.34-2.56 5.74-3.86 7.67-3.86 3.04 0 5.95-1.9 10.43-6.22l2.46-2.39c.94-.89 1.67-1.56 2.37-2.13 1.81-1.49 3.3-2.26 4.74-2.26 1.03 0 1.81.13 3.1.42.7.16.71.17.96.21 2.96.6 5.45-.55 11.23-6.33 2.2-2.2 4.06-4.65 7.09-9.11l1.52-2.25c4.15-6.11 6.76-9.37 10.22-12.24a23.9 23.9 0 0 1 15.88-5.7c16.87 0 18.62-7.01 6.36-17.23-5.9-4.92-8.12-9.41-6.52-12.93 1.42-3.12 5.67-4.84 11.16-4.84 9.25 0 17-8.34 17-18 0-8.94-2.88-10.79-8.36-6.23-3.94 3.28-9.98 3.59-26.67 2.68l-1.02-.06c-5.09-.27-7.99-.39-10.95-.39-8.88 0-13.76-2.14-19.18-7.5-1-.98-5.26-5.53-6.53-6.79-1.99-1.99-3.86-2.9-5.87-3-2.03-.12-3.06.18-8.15 2.07-4.15 1.55-6.9 2.22-10.27 2.22-4.33 0-6.84 1.46-9.98 5.2-.63.74-2.89 3.6-3.18 3.95a48.29 48.29 0 0 1-4.04 4.46 37.26 37.26 0 0 1-5.24 4.23c-6.26 4.17-8.9 7.91-11.95 15.58l-.57 1.43-.28.74a531.5 531.5 0 0 1-1.56 3.88 77.49 77.49 0 0 1-3.32 7c-3.16 5.88-7.82 8.97-17.63 12.96l-3.92 1.58c-1.6.64-2.84 1.15-4.05 1.67a79.2 79.2 0 0 0-8.3 4.08 54.8 54.8 0 0 0-7.35 5.02C391.12 514.78 389 518.21 389 530.31zm133.22-79.76c3.06 1.53 6.54 2.02 10.68 1.7 2.53-.2 4.91-.62 8.8-1.49 5.36-1.19 6.33-1.38 8.33-1.54 2.78-.23 4.82.17 6.29 1.4 1.58 1.31 1.96 2.72 1.26 4.22-.66 1.38-1.05 1.74-5.05 5.07-3.53 2.93-5.03 4.83-5.03 7.09 0 7.3 1.29 10.02 7.83 15.62 3.86 3.3 5.93 6.84 5.28 9.62-.75 3.25-4.96 5.02-12.61 5.02-7.18 0-12.7 4.61-20.03 14.68-.5.7-3.96 5.57-4.94 6.87a38.89 38.89 0 0 1-4.72 5.5c-1.06.98-2.09 1.7-3.1 2.15-2.85 1.26-5.05 1.57-9.83 1.74-7.66.27-10.87 1.45-14.98 7.1-1.58 2.17-3.11 4-4.68 5.6a42.87 42.87 0 0 1-8.65 6.69c-.15.08-10.69 6.19-14.8 8.83-3.76 2.42-6.45 2.04-8.22-.77-1.28-2.03-1.9-4.54-2.87-10.35-.84-5.08-1.27-7.08-2.06-8.93-.97-2.3-2.21-3.24-4.02-2.88-6.2 1.24-8.95 1.39-10.98.2-2.37-1.4-3.13-4.62-2.62-10.73.16-1.96-1.04-2.87-3.76-3.04-2.24-.13-4.9.2-9.94 1.12l-.69.12c-7.97 1.45-10.72 1.72-12.72.73-2.91-1.43-1.6-5.27 4.23-12.21 5.48-6.53 10.6-10.81 15.76-13.53 3.74-1.97 5.94-2.65 12.16-4.1 7.29-1.72 10.4-3.51 14.04-9.31 2.96-4.75 10.74-18.62 12.14-20.84 3.59-5.67 6.8-9.1 11.05-11.34 2.6-1.38 4.72-2.82 9.17-6.07l1.38-1.01c7.85-5.72 12.3-7.98 17.68-7.98 4.22 0 6.49 1.36 9.13 4.77.34.43 1.67 2.22 2 2.67.85 1.09 1.6 1.98 2.45 2.83a24.29 24.29 0 0 0 6.64 4.78zm-.44.9c-2.8-1.4-5-3.03-6.92-4.97-.87-.9-1.65-1.81-2.51-2.93-.35-.46-1.68-2.25-2.01-2.67-2.47-3.18-4.46-4.38-8.34-4.38-5.09 0-9.4 2.2-17.09 7.78l-1.38 1.01c-4.49 3.29-6.63 4.74-9.3 6.15-4.06 2.15-7.16 5.45-10.66 11-1.39 2.19-9.16 16.05-12.15 20.82-3.79 6.07-7.13 7.98-14.66 9.75-6.13 1.45-8.27 2.1-11.92 4.02-5.04 2.66-10.05 6.86-15.46 13.3-5.43 6.46-6.53 9.69-4.55 10.66 1.7.84 4.48.57 12.1-.81l.7-.13c5.12-.93 7.82-1.27 10.17-1.12 3.21.2 4.92 1.48 4.7 4.11-.48 5.76.2 8.64 2.13 9.78 1.73 1.02 4.34.88 10.27-.31 2.35-.47 4 .78 5.14 3.47.83 1.95 1.27 4 2.07 8.8l.06.36c.94 5.65 1.55 8.11 2.72 9.98 1.46 2.3 3.52 2.6 6.84.46 4.14-2.66 14.69-8.77 14.81-8.85a41.9 41.9 0 0 0 8.46-6.54 47.89 47.89 0 0 0 4.6-5.48c4.32-5.95 7.81-7.23 15.74-7.5 4.66-.17 6.76-.47 9.46-1.67.9-.4 1.85-1.06 2.84-1.96a38.03 38.03 0 0 0 4.6-5.36c.96-1.3 4.4-6.16 4.93-6.87 7.5-10.31 13.22-15.09 20.83-15.09 7.24 0 11.02-1.6 11.64-4.24.54-2.32-1.36-5.55-4.97-8.64-6.75-5.79-8.17-8.79-8.17-16.38 0-2.67 1.64-4.74 5.39-7.86 3.8-3.17 4.23-3.56 4.78-4.73.5-1.06.25-1.99-.99-3.03-2.23-1.85-4.72-1.65-13.76.36-3.93.87-6.35 1.3-8.94 1.5-4.3.34-7.97-.18-11.2-1.8zm-28-3.9c5.65-2.82 8.96-2.2 12.9 1.37.56.5 2.6 2.47 3.02 2.87 4.2 3.89 8.07 5.71 14.3 5.71 11.37 0 14 1.41 16.1 8.09.26.83 1.35 4.6 1.66 5.62.8 2.63 1.64 5.03 2.7 7.6 2.13 5.17 2.64 8.32 1.72 10.24-.77 1.61-2.1 2.18-5.37 2.79-2.32.43-2.8.53-3.85.85-1.85.58-3.35 1.4-4.6 2.66-1 1-2.02 2.13-3.31 3.66-.6.71-2.91 3.5-3.46 4.14-7.2 8.54-12.43 12.35-19.59 12.35-3.76 0-6.95 1.28-10.59 4-1.84 1.37-11.62 10.31-15.22 13.06a73.09 73.09 0 0 1-8.95 5.88c-4.58 2.54-7.35 3.22-8.98 2.23-1.32-.8-1.65-2.07-1.94-5.5a52.53 52.53 0 0 0-.16-1.81c-.54-4.73-2.24-6.86-7.16-6.86-7.11 0-8.85-1.23-9.73-5.41-.96-4.61-2.1-6.7-6.55-9.67-3.97-2.65-4.31-5.42-1.52-8.22 2-2 4.63-3.5 11.35-6.87 6.61-3.3 9.2-4.8 11.1-6.68a39.09 39.09 0 0 0 5.3-6.48c.98-1.5 1.83-3.04 2.88-5.13l2.12-4.3c.91-1.83 1.72-3.37 2.61-4.98 5.74-10.32 10.37-14.78 23.22-21.2zm-22.34 21.7c-.89 1.59-1.69 3.12-2.6 4.94l-2.11 4.3a52.9 52.9 0 0 1-2.94 5.23 40.08 40.08 0 0 1-5.44 6.63c-2 2-4.62 3.51-11.35 6.87-6.6 3.3-9.2 4.8-11.1 6.69-2.33 2.34-2.08 4.37 1.38 6.67 4.7 3.14 5.96 5.46 6.97 10.3.78 3.7 2.09 4.62 8.75 4.62 5.5 0 7.57 2.57 8.15 7.75.06.5.09.82.17 1.84.25 3.06.55 4.17 1.46 4.72 1.2.74 3.69.13 7.98-2.25a72.09 72.09 0 0 0 8.82-5.8c3.55-2.7 13.34-11.65 15.24-13.07 3.79-2.83 7.18-4.19 11.18-4.19 6.77 0 11.8-3.67 18.83-12l3.45-4.13a60.07 60.07 0 0 1 3.37-3.72 11.72 11.72 0 0 1 5.01-2.91c1.1-.34 1.6-.45 3.97-.89 2.95-.55 4.07-1.02 4.65-2.23.76-1.59.28-4.5-1.74-9.43a84.46 84.46 0 0 1-2.74-7.69c-.31-1.03-1.4-4.8-1.66-5.61-1.95-6.2-4.16-7.39-15.14-7.39-6.5 0-10.61-1.93-14.98-5.98-.44-.4-2.46-2.37-3.01-2.86-3.65-3.3-6.52-3.85-11.79-1.21-12.67 6.33-17.15 10.65-22.78 20.8zm55.86 11.93c-2.98 6.45-16.78 15.26-26.74 15.26-5.33 0-7.56-2.98-7.11-7.86.32-3.48 2.1-7.91 3.93-10.61l1.52-2.32a44.95 44.95 0 0 1 1.88-2.7c3.66-4.8 7.85-7.45 13.62-7.45 9.06 0 15.75 9.52 12.9 15.68zm-.9-.42c2.52-5.47-3.65-14.26-12-14.26-5.4 0-9.33 2.48-12.82 7.06-.6.8-1.17 1.6-1.85 2.64 0 0-1.2 1.87-1.52 2.33-1.74 2.57-3.46 6.85-3.77 10.14-.4 4.33 1.43 6.77 6.12 6.77 9.57 0 23.02-8.58 25.83-14.68zm-69.67 20.74c2.08.18 4.44.81 5.88 1.8 2.12 1.47 2.2 3.6-.26 6.05-5.14 5.15-12.85 4.34-12.85-1.35 0-4.66 3.14-6.84 7.23-6.5zm-.09 1c-3.56-.3-6.14 1.5-6.14 5.5 0 4.58 6.53 5.26 11.15.65 2.03-2.04 1.98-3.43.4-4.52-1.27-.88-3.48-1.47-5.4-1.63zm29.59-225.95c4.64 2.35 17.27 8.24 19.39 9.43a24.14 24.14 0 0 1 7.05 5.64 45.03 45.03 0 0 1 3.75 5.2c2.4 3.78.04 7.66-6.2 11.63-4.97 3.16-12.18 6.3-21.95 9.82-4.84 1.74-19.63 6.68-21.1 7.2-6.59 2.33-14.85.1-25.14-5.86-3.93-2.27-8-5-12.94-8.54-2.23-1.61-9.5-6.99-10.7-7.85a81.21 81.21 0 0 0-8.63-5.7c-4.82-2.6-4.45-6.64.17-12.13 3.27-3.88 4.17-4.67 18.1-16.33a230.2 230.2 0 0 0 8.89-7.74 95.2 95.2 0 0 0 4.72-4.66c5.08-5.43 9.8-6.49 14.97-3.92 2.24 1.1 4.53 2.85 7.43 5.52 1.48 1.37 6.94 6.72 7.98 7.7 5.2 4.91 9.46 8.2 14.2 10.6zm-.46.9c-4.85-2.45-9.18-5.79-14.44-10.76-1.05-1-6.5-6.34-7.97-7.69-2.83-2.61-5.06-4.3-7.2-5.37-4.75-2.36-9-1.4-13.8 3.71a96.18 96.18 0 0 1-4.76 4.71c-2.48 2.3-5.16 4.62-8.92 7.77-13.86 11.6-14.77 12.4-17.98 16.21-4.28 5.08-4.58 8.4-.46 10.61 2.23 1.2 4.9 2.99 8.74 5.77 1.2.87 8.47 6.24 10.7 7.85a154.8 154.8 0 0 0 12.85 8.49c10.06 5.82 18.07 7.98 24.3 5.78 1.48-.52 16.27-5.47 21.1-7.2 9.7-3.5 16.86-6.61 21.75-9.72 5.84-3.71 7.9-7.1 5.9-10.26a44.09 44.09 0 0 0-3.67-5.08 23.16 23.16 0 0 0-6.78-5.42c-2.08-1.16-14.68-7.05-19.36-9.4zm-38.83 8.05c3.11-.37 5.7-.13 8.4.7 2.15.66 2.74.93 8.64 3.77 4.75 2.29 8.39 3.86 13.19 5.56 8.38 2.97 11.32 6.23 8.83 9.76-2.08 2.94-8.04 5.92-17.84 9.18-8.45 2.82-15.48 2.35-21.43-.9-4.65-2.55-8.33-6.5-12.15-12.3-2.9-4.41-2.73-8.2.16-11.06 2.48-2.45 6.87-4.07 12.2-4.7zm.12 1c-5.13.6-9.33 2.16-11.62 4.42-2.53 2.5-2.68 5.77-.02 9.8 3.73 5.68 7.3 9.51 11.8 11.97 5.7 3.11 12.43 3.57 20.62.84 9.59-3.2 15.44-6.12 17.34-8.82 1.94-2.75-.5-5.45-8.35-8.24-4.84-1.72-8.5-3.3-13.28-5.6-5.84-2.81-6.42-3.07-8.5-3.71a18.42 18.42 0 0 0-8-.66zM202.5 500.38c0 4.78-1.45 7.56-4.43 8.93-2.29 1.05-4.55 1.23-10.79 1.2l-1.78-.01c-9.19 0-17-7.65-17-15.5 0-7.59 10.6-10.51 19.74-5.44 2.78 1.55 4.21 1.94 8.57 2.75 4.44.83 5.69 2.27 5.69 8.07zm-1 0c0-5.3-.9-6.34-4.88-7.08-4.45-.83-5.96-1.25-8.86-2.86-8.57-4.76-18.26-2.1-18.26 4.56 0 7.3 7.36 14.5 16 14.5h1.79c6.06.04 8.26-.14 10.36-1.1 2.6-1.2 3.85-3.6 3.85-8.02zm33.33-117.85c3.71-1.31 8.7-2.7 16.1-4.55 2.58-.65 16.53-4.04 20.56-5.05 19.59-4.93 31.55-8.9 38.23-13.35 14.93-9.95 36.87-33.88 43.83-47.8 2.25-4.5 4.65-6.38 7.68-6.25 1.26.06 2.61.45 4.32 1.2a50.81 50.81 0 0 1 3.54 1.7l1.26.63c4.78 2.34 8.38 3.44 12.65 3.44 7.2 0 10.01 3.07 8.35 7.91-1.4 4.06-5.92 8.91-11.1 12.02-8.3 4.98-11.75 17.3-11.75 33.57 0 3.59-1.37 6.28-3.98 8.36-1.98 1.58-4.2 2.6-8.47 4.16l-1.02.37c-4.85 1.75-6.98 2.77-8.68 4.46-5.09 5.1-12.54 7.15-20.35 7.15-1.38 0-2.47.92-3.99 3.1-.29.41-1.32 1.95-1.47 2.18-2.68 3.92-4.93 5.72-8.54 5.72-7.84 0-10.74.93-21.76 6.94-5.18 2.82-8.8 3.58-14.66 3.68-.26 0-.47 0-.92.02-4.82.06-7.12.3-10.51 1.34a73.43 73.43 0 0 0-8.89 3.56c-2.17 1-10.53 5.01-10.23 4.87-7.79 3.7-13.32 5.98-18.9 7.57-12.41 3.55-18.58 2.24-27.42-4.07-2.58-1.85-2.72-4.43-.83-7.62 1.45-2.45 3.9-5.09 8.08-8.97l1.78-1.64c3.92-3.6 4.48-4.11 5.9-5.53 2.32-2.32 3.12-3.5 5.48-7.63 1.93-3.36 3.37-5.11 6.27-7.06 2.3-1.54 5.34-2.98 9.44-4.43zm.34.94c-4.03 1.42-7 2.83-9.22 4.32-2.75 1.85-4.1 3.49-5.96 6.73-2.4 4.2-3.24 5.44-5.64 7.83-1.43 1.44-2 1.96-5.94 5.57l-1.77 1.63c-4.1 3.82-6.52 6.41-7.9 8.75-1.65 2.79-1.54 4.8.55 6.3 8.6 6.14 14.46 7.38 26.57 3.92 5.5-1.57 11-3.84 18.74-7.51-.3.14 8.06-3.88 10.24-4.88a74.3 74.3 0 0 1 9.01-3.6c3.51-1.09 5.89-1.33 10.8-1.4h.91c5.72-.1 9.18-.83 14.2-3.57 11.16-6.08 14.2-7.06 22.24-7.06 3.19 0 5.2-1.6 7.71-5.28l1.48-2.2c1.7-2.43 3-3.52 4.81-3.52 7.57 0 14.78-2 19.65-6.85 1.83-1.84 4.04-2.9 9.04-4.7l1.02-.37c8.6-3.13 11.79-5.67 11.79-11.58 0-16.6 3.53-29.2 12.24-34.43 5-3 9.35-7.67 10.66-11.48 1.42-4.13-.83-6.59-7.4-6.59-4.45 0-8.19-1.14-13.09-3.54-7.52-3.67-6.78-3.34-8.72-3.43-2.58-.1-4.65 1.52-6.74 5.7-7.04 14.07-29.1 38.14-44.17 48.19-6.81 4.54-18.84 8.52-38.55 13.48-4.03 1.02-17.98 4.4-20.56 5.05-7.37 1.84-12.33 3.23-16 4.52zM252 387.5c2.08 0 4-.2 7.25-.69 5.22-.77 6.64-.9 8.46-.5 2.52.56 3.79 2.35 3.79 5.69 0 4.05-2.27 7.29-6.62 10.11-3.24 2.1-6.53 3.53-14.15 6.4l-.27.1-2.28.86c-3.04 1.16-5.27 2.52-9.33 5.43l-.8.57c-8.19 5.88-13.35 8.03-23.05 8.03-4.98 0-6.88-2.03-5.75-5.62.87-2.81 3.58-6.56 7.8-11.13 1.26-1.37 2.64-2.8 4.15-4.3 3.17-3.14 11.25-10.61 11.45-10.8.46-.47.93-.89 1.4-1.26 3.38-2.71 5.77-3.08 14.18-2.93 1.65.03 2.63.04 3.77.04zm0 1c-1.15 0-2.13-.01-3.79-.04-8.18-.14-10.4.2-13.54 2.71-.44.35-.88.74-1.32 1.18-.2.21-8.3 7.69-11.45 10.82a134.6 134.6 0 0 0-4.12 4.26c-4.12 4.47-6.76 8.12-7.58 10.75-.9 2.88.45 4.32 4.8 4.32 9.46 0 14.44-2.07 22.46-7.84l.8-.57c4.13-2.96 6.42-4.36 9.56-5.56l2.3-.86.25-.1c7.55-2.84 10.8-4.25 13.97-6.3 4.08-2.65 6.16-5.6 6.16-9.27 0-2.89-.97-4.26-3-4.7-1.65-.37-3.05-.25-8.1.5-3.3.5-5.26.7-7.4.7zm112.47-45.34c-1.88 5.44-1.98 6.76-.98 12.76 1.18 7.06-1.38 16.58-5.49 16.58a16.89 16.89 0 0 0-1.51.07l-.64.04c-2.86.18-4.83.17-6.94-.17-6.55-1.06-10.41-5.14-10.41-13.44 0-13.9 2.14-19.69 8.13-26.33a21.9 21.9 0 0 0 2.52-3.75c.59-1.03 2.78-5.13 2.72-5.01 4.44-8.14 7.71-11.53 12.25-10.4 1.17.3 2.2.77 3.58 1.59l1.39.84a20 20 0 0 0 3.1 1.6c.7.27 1.8.32 4.75.26l.72-.01c3.16-.05 4.78.08 5.83.66 1.61.89 1.2 2.56-1.14 4.9a215.9 215.9 0 0 1-3.86 3.76c-10.6 10.1-12.75 12.4-14.02 16.05zm-.94-.32c1.34-3.9 3.46-6.17 14.27-16.46 1.55-1.47 2.73-2.62 3.85-3.73 1.94-1.95 2.17-2.88 1.35-3.33-.82-.45-2.37-.58-5.32-.53l-.72.01c-3.14.06-4.26.02-5.14-.34-1.06-.41-1.97-.9-3.25-1.67l-1.38-.83a12.1 12.1 0 0 0-3.31-1.47c-3.88-.97-6.92 2.17-11.13 9.9.07-.13-2.14 3.98-2.73 5.02a22.71 22.71 0 0 1-2.65 3.92c-5.81 6.47-7.87 12-7.87 25.67 0 7.79 3.48 11.47 9.57 12.45 2.01.33 3.92.34 6.71.16a371.33 371.33 0 0 0 1.23-.07c.42-.03.73-.04.99-.04 3.2 0 5.6-8.9 4.5-15.42-1.02-6.16-.91-7.64 1.03-13.24zm-9.26 12.42c.58.52 2.5 1.9 2.55 1.93 1.96 1.57 2.04 3.31.01 6.36-3.74 5.64-8.83 3.09-8.83-4.55 0-3.81.51-5.67 2.07-6.02 1.18-.26 2 .3 4.2 2.28zm-1.34 1.48c-1.5-1.35-2.23-1.85-2.43-1.8-.17.03-.5 1.23-.5 4.06 0 5.87 2.67 7.21 5.17 3.45 1.5-2.26 1.47-2.84.4-3.7.03.03-1.95-1.4-2.64-2zm222.9-130.19c2.2-1.1 3.67-1.66 5.88-2.36l.28-.09a48.92 48.92 0 0 0 8.79-3.55c4.17-2.08 6.35-1.88 6.96.84.44 2 .2 4.01-1.25 12.7-2.27 13.62-9.16 26.14-21.17 36.3-4.3 3.63-7.41 4.39-9.75 2.44-1.88-1.57-3.1-4.57-4.61-10.48-.3-1.15-1.43-5.83-1.72-6.96a114.18 114.18 0 0 0-2.71-9.22c-2.4-6.82-3.03-10.78-2.1-12.94.77-1.83 2.08-2.24 5.6-2.45 1.49-.09 2.09-.14 2.97-.28l1.95-.33c.72-.12 1.22-.2 1.68-.29 1.1-.2 1.92-.38 2.71-.6 1.7-.49 3.42-1.2 6.49-2.73zm.44.9c-3.11 1.54-4.88 2.29-6.65 2.79-.84.23-1.69.42-2.81.63a108.77 108.77 0 0 1-3.81.63c-.77.13-1.39.19-2.92.28-3.13.18-4.17.51-4.74 1.85-.78 1.84-.2 5.62 2.13 12.2a115.12 115.12 0 0 1 2.74 9.31l1.72 6.96c1.46 5.7 2.62 8.58 4.28 9.96 1.87 1.56 4.49.93 8.47-2.44 11.82-10 18.6-22.3 20.83-35.7 1.4-8.45 1.65-10.51 1.25-12.31-.41-1.87-1.86-2-5.54-.16a49.87 49.87 0 0 1-8.93 3.6l-.28.1a35.4 35.4 0 0 0-5.74 2.3zm-4.5 6.58c1.37-.32 2.5-.75 3.9-1.42.35-.18 2.57-1.31 3.32-1.67 1.5-.71 2.97-1.31 4.7-1.89 2.7-.9 4.64-.77 5.88.4.98.94 1.34 2.26 1.41 4.18.02.4.02.7.02 1.37 0 5.63-4.63 16.88-11.34 22.75-4.34 3.8-7.31 4.67-9.92 2.52-2.06-1.7-3.5-4.65-6.67-12.91-1.86-4.83-2.05-8.1-.68-10.2 1.12-1.7 2.9-2.36 5.83-2.7l1.26-.12c1.19-.12 1.75-.19 2.3-.31zm-2.1 2.3l-1.22.12c-2.4.27-3.7.76-4.39 1.81-.93 1.43-.78 4.1.87 8.38 3.02 7.84 4.41 10.71 6.08 12.09 1.63 1.34 3.64.75 7.33-2.48C584.6 250.77 589 240.08 589 235c0-.64 0-.93-.02-1.29-.05-1.44-.3-2.33-.79-2.8-.6-.57-1.8-.65-3.87.04a37.95 37.95 0 0 0-4.47 1.8c-.72.34-2.93 1.47-3.32 1.66a19.54 19.54 0 0 1-4.3 1.56c-.66.16-1.28.24-2.56.36zm-227.73-88.98c-1.59 4.3-3.54 7.25-7.14 11.4l-2.6 2.97a67.02 67.02 0 0 0-2.63 3.23 46.4 46.4 0 0 0-4.68 7.5c-2.85 5.7-7.14 10.18-12.85 13.89-4.25 2.76-8.25 4.62-15.67 7.59-11.01 4.4-16.43 1.26-27.22-16.4-2.86-4.69-8.8-8.63-17.98-12.66-3-1.33-12.88-5.24-14.43-5.92-4.96-2.18-7.04-3.72-6.42-5.85.67-2.32 5.3-4.05 15.48-6.08 16.63-3.32 26.93-3.82 39.93-3.02 7.9.49 9.67.5 12.74-.26 1.99-.48 3.92-1.3 6-2.6l2.79-1.71c9.86-6.14 12.94-7.96 17.3-9.9 6.03-2.71 10.57-3.32 13.94-1.4 7.2 4.12 7.68 7.7 3.44 19.22zm-1.88-.7c3.95-10.7 3.6-13.26-2.56-16.78-2.66-1.52-6.62-.99-12.12 1.48-4.24 1.9-7.3 3.7-17.07 9.77l-2.79 1.73a22.6 22.6 0 0 1-6.57 2.84c-3.36.81-5.22.8-13.34.3-12.84-.78-22.97-.29-39.41 3-4.9.97-8.45 1.88-10.79 2.75-2.03.76-3.04 1.45-3.17 1.91-.16.57 1.48 1.79 5.3 3.46 1.5.67 11.39 4.58 14.44 5.93 9.52 4.19 15.74 8.3 18.87 13.44 10.35 16.93 14.87 19.56 24.78 15.6 7.3-2.93 11.21-4.75 15.33-7.42 5.42-3.53 9.47-7.75 12.15-13.1 1.44-2.9 3.02-5.4 4.86-7.82a68.95 68.95 0 0 1 2.72-3.33l2.6-2.97c3.46-3.99 5.28-6.75 6.77-10.79zm-6.64-.39c-7.94 12.8-18.53 21.75-33.3 25.23-7.82 1.83-12.47-.79-13.12-5.93-.55-4.45 2.29-9.06 6-9.06 3.02 0 5.6-1.68 15.38-9.16 1.47-1.12 2.57-1.96 3.66-2.74 4.4-3.2 7.77-5.17 10.82-6.08 5.57-1.67 9.33-2.15 11.35-1.22 2.5 1.14 2.22 4.13-.79 8.96zm-.84-.52c2.72-4.4 2.94-6.74 1.21-7.53-1.71-.79-5.32-.33-10.65 1.27-2.9.87-6.2 2.79-10.51 5.92-1.08.79-2.18 1.62-3.65 2.74-10.08 7.72-12.62 9.36-15.98 9.36-3.02 0-5.5 4.02-5 7.94.56 4.5 4.62 6.78 11.89 5.07 14.48-3.4 24.86-12.18 32.69-24.77zM461.17 33.53c13.88 4.96 20.75 4.96 31.62.01 3.02-1.37 5.47-2.94 11-6.82 5.57-3.92 8.05-5.51 11.14-6.92 4.14-1.88 7.78-2.38 11.22-1.28 3.92 1.26 6.2 12.3 6.78 28.45.5 14.2-.52 28.93-2.46 34.2-1.82 4.93-5.86 8.17-11.51 10.02A41.7 41.7 0 0 1 506 93.01c-5.79 0-9 2.4-12.2 7.64-.37.59-1.55 2.6-1.71 2.87-1.75 2.9-3.05 4.33-4.93 4.95-.94.32-2.07.83-3.87 1.74l-2.43 1.23c-1.03.53-1.87.94-2.7 1.34-6.43 3.1-11.73 4.72-17.16 4.72-5.71 0-10.04 2.09-14.02 5.92-1.16 1.11-4.2 4.53-4.63 4.94-2.54 2.44-5.93 4.24-10.85 6.1-1.4.52-5.98 2.13-6.25 2.22l-2.06.78c-.89.36-1.78.63-2.7.81-5.55 1.14-11.14-.54-17.98-4.42-1.27-.73-5.13-3.06-5.76-3.42-2.05-1.16-4.12-1.53-9.09-1.9l-1.73-.15c-4.78-.4-7.68-1.14-10.22-2.97-5-3.61-6.77-7.76-5.65-12.33 1.33-5.42 6.5-11.02 14.85-17.28a169.2 169.2 0 0 1 6.5-4.61c-.33.23 4.33-2.92 5.3-3.6 2.73-1.91 4.8-3.9 12.75-12.04l1.09-1.1c3.49-3.56 5.89-5.89 8.12-7.83 2.9-2.5 4.72-5.95 7.5-13.05l.63-1.61c2.7-6.92 4.28-10 6.87-12.33 1.42-1.28 6.68-6.54 7.93-7.5 3.98-3 8.01-2.73 19.57 1.4zm-.34.94c-11.26-4.02-15-4.28-18.62-1.53-1.19.9-6.4 6.11-7.88 7.43-2.42 2.18-3.96 5.19-6.6 11.95l-.63 1.61c-2.83 7.26-4.72 10.8-7.77 13.45a141.85 141.85 0 0 0-9.16 8.87c-8.02 8.2-10.08 10.2-12.88 12.16-.99.69-5.65 3.84-5.31 3.6-2.5 1.71-4.52 3.13-6.47 4.59-8.17 6.13-13.23 11.6-14.48 16.72-1.02 4.15.58 7.9 5.26 11.27 2.36 1.7 5.11 2.4 9.72 2.8l1.73.13c5.12.4 7.28.78 9.5 2.05.65.36 4.5 2.7 5.76 3.4 6.66 3.78 12.04 5.4 17.29 4.32.86-.17 1.7-.42 2.52-.75a67 67 0 0 1 2.1-.8c.28-.1 4.86-1.7 6.24-2.22 4.8-1.8 8.08-3.56 10.5-5.88.4-.38 3.44-3.8 4.63-4.94 4.16-4 8.72-6.2 14.72-6.2 5.25 0 10.42-1.59 16.73-4.62.82-.4 1.65-.8 2.68-1.33.12-.06 1.93-.99 2.43-1.23 1.84-.93 3-1.46 4-1.8 1.6-.52 2.76-1.82 4.39-4.52l1.7-2.88c3.39-5.5 6.87-8.11 13.07-8.11 4.45 0 8.73-.49 12.64-1.77 5.4-1.76 9.2-4.8 10.9-9.41 1.87-5.11 2.9-19.75 2.39-33.83-.56-15.53-2.81-26.48-6.08-27.52-3.18-1.02-6.57-.55-10.5 1.23-3.02 1.37-5.47 2.94-11 6.83-5.57 3.92-8.05 5.5-11.14 6.92-11.13 5.05-18.26 5.05-32.38.01zM475 55c5.38 0 7.55-.21 9.72-.96 1.26-.43 9.95-4.8 14.88-6.96 1.9-.82 3.56-2.44 6.6-6.04 2.56-3.04 3.19-3.75 4.4-4.84 3.7-3.35 7.07-3.28 10.22 1.23 6.23 8.9 5.61 15.94.07 27.02a71.26 71.26 0 0 0-2.5 5.48c-.32.8-1 2.7-1.09 2.9-.17.45-.34.81-.54 1.17-.63 1.14-1.56 2.21-4.05 4.7-2.4 2.4-5.16 3.27-11.68 4.33-1.81.3-2.2.36-3 .51-6.02 1.1-9.6 2.69-12.24 6.07-3.57 4.59-7.9 7.48-14.98 10.74-.55.24-1.1.5-1.8.8l-1.78.8a60.08 60.08 0 0 0-7.7 3.9c-2.57 1.6-4.79 2.35-9.42 3.46-8.58 2.06-12.28 3.76-17.37 9.36-5.12 5.64-10.17 7.64-16.63 6.7-5.36-.79-10.63-3.01-23.56-9.48-6.3-3.15-6.43-7.78-1.5-13.56 3.38-3.94 3.52-4.06 19.4-16.44 8.12-6.33 12.97-10.57 16.63-14.88 2.53-2.98 4.2-5.73 4.96-8.3 5.5-18.3 12.5-21.98 22.78-15.56 1.95 1.22 6.61 4.55 7.18 4.9 3.36 2.15 6.52 2.95 13 2.95zm0 2c-6.84 0-10.37-.89-14.08-3.26-.63-.4-5.27-3.71-7.16-4.9-9.05-5.65-14.66-2.7-19.8 14.45-.86 2.87-2.67 5.85-5.35 9.01-3.78 4.45-8.7 8.75-16.94 15.17-15.66 12.21-15.86 12.38-19.1 16.16-4.17 4.9-4.09 8 .88 10.48 12.71 6.35 17.89 8.54 22.94 9.28 5.78.84 10.18-.9 14.87-6.06 5.42-5.96 9.45-7.82 18.38-9.96 4.43-1.07 6.5-1.76 8.83-3.22a61.7 61.7 0 0 1 7.94-4.02l1.78-.8 1.78-.8c6.82-3.13 10.91-5.87 14.24-10.14 3-3.87 7-5.64 13.46-6.82.83-.15 1.21-.21 3.04-.51 6.1-1 8.6-1.78 10.58-3.77 2.36-2.36 3.21-3.34 3.72-4.26.15-.27.29-.56.44-.94.06-.15.75-2.06 1.09-2.9.64-1.6 1.45-3.4 2.57-5.64 5.24-10.49 5.8-16.8.07-24.98-2.4-3.44-4.37-3.48-7.24-.89-1.11 1-1.73 1.7-4.22 4.65-3.24 3.85-5.04 5.59-7.32 6.59-4.82 2.1-13.62 6.53-15.03 7.01-2.44.84-4.79 1.07-10.37 1.07zm-12.7 8.6c5.47 3.9 10.34 3.72 18.23.88 5.39-1.94 5.92-2.1 7.7-2.1 2.5-.01 4.21 1.36 5.24 4.46 1.66 4.98-2.32 8.52-12.3 12.68-2.7 1.13-16.25 6.18-20 7.73-7.86 3.24-13.93 6.42-18.87 10.15-13.02 9.84-18.36 11.93-23.71 9.68a24.67 24.67 0 0 1-3.62-1.98l-1.99-1.28a90.4 90.4 0 0 0-2.24-1.4c-3.33-2-2.82-4.28.85-7.34 1.35-1.13 10.66-7.61 13.53-9.91 7.1-5.69 11.91-11.47 14.41-18.34 3.07-8.45 4.89-12.1 6.8-13.39 1.73-1.16 3.36-.53 6.18 1.9.63.56 3.4 3.08 4.11 3.7 1.93 1.7 3.71 3.15 5.67 4.55zm-.6.8c-1.98-1.42-3.79-2.88-5.74-4.6-.73-.64-3.48-3.16-4.1-3.7-2.5-2.16-3.75-2.65-4.97-1.83-1.66 1.11-3.44 4.7-6.42 12.9-2.57 7.07-7.5 12.99-14.72 18.78-2.91 2.33-12.21 8.8-13.52 9.9-3.22 2.68-3.56 4.17-.97 5.72l2.26 1.4 1.99 1.28c1.47.93 2.48 1.5 3.47 1.91 4.9 2.07 9.96.07 22.72-9.56 5.02-3.79 11.15-7 19.1-10.28 3.76-1.55 17.3-6.6 20-7.72 9.5-3.97 13.14-7.2 11.73-11.44-.9-2.71-2.25-3.8-4.3-3.79-1.6 0-2.15.17-7.36 2.05-8.17 2.94-13.34 3.14-19.16-1.01z'%3E%3C/path%3E%3C/svg%3E"); + position: relative; +}.shinobi-bg-shade { + position: absolute; + height: 100%; + width: 100%; + background: rgba(0,0,0,0.5); +} +.container>div>.form-group-group{ + box-shadow: 0 0 10px #31708f +} +nav{margin-top:20px} +#sub_accounts{ + margin-bottom: 0 +} +.navbar-forestgreen { + background-color: #1e4046 !important; + border-color: #1e4046 !important; +} +.navbar-forestgreen .navbar-brand,.navbar-forestgreen .dropdown-toggle{ + color: #fff !important; +} diff --git a/web/libs/css/main.dash2.css b/web/libs/css/main.dash2.css index b0daee1..a62515c 100644 --- a/web/libs/css/main.dash2.css +++ b/web/libs/css/main.dash2.css @@ -6,7 +6,7 @@ position: relative; min-height: 1px; padding-right: 15px; - padding-left: 15px; + padding-left: 15px; } .col-5ths { @@ -24,7 +24,7 @@ -ms-flex: 0 0 20%; flex: 0 0 20%; max-width: 20%; - } + } } @media (min-width: 768px) { @@ -34,7 +34,7 @@ -ms-flex: 0 0 20%; flex: 0 0 20%; max-width: 20%; - } + } } ::-webkit-scrollbar-thumb:hover { background-color:#bd9565; @@ -90,6 +90,9 @@ img{max-width:100%} } .monitor_item .stream-hud{opacity:0;position:absolute;top:0;left:0;width:100%;height:100%;z-index:2} +.monitor_item .stream-hud .camera_cpu_usage{position:absolute;top:0;left:0;width: 100%;} +.monitor_item .stream-hud .camera_cpu_usage .progress{width: 100%;} +.monitor_item .stream-hud .camera_cpu_usage:hover .progress{height:20px;transition:0.2s} .monitor_item .stream-hud .controls{position:absolute;top:10px;left:10px;} .monitor_item .stream-hud:hover{opacity:1} .monitor_item .stream-hud .bottom-text{position:absolute;bottom:0;left:0;width:100%;padding:5px;text-shadow: 0 0 10px #333;} @@ -114,8 +117,7 @@ img{max-width:100%} .monitor_item.fullscreen img.stream-element{height:100%;width:auto} .monitor_item.fullscreen canvas.stream-element{height:auto;width:auto;background-color:black;} .monitor_item .stream-element{border: 0;object-fit: fill;height: 100%;width:100%} -.monitor_item{position:relative;padding:0;} -.monitor_item{transition:none;} +.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.selected .stream-element{height:600px} @@ -124,7 +126,7 @@ img{max-width:100%} .monitor_item.detector_triggered .detector-fade{opacity:1} .monitor_item .detector-fade{opacity:0} .monitor_item .indifference{position:absolute;width:100%;left:0;top:0;transition:0.2s;} -.monitor_item .indifference .progress{width:100%;background:#333;box-shadow:0;} +.monitor_item .progress{width:100%;background:#333;box-shadow:0;} .monitor_item .indifference:hover .progress{height:20px;transition:0.2s} .hide_indifference .indifference{display:none!important} .hide_indifference [class_toggle="hide_indifference"]{color:#d9534f!important} @@ -330,9 +332,16 @@ form.modal-body{margin:0} .timelapse_hud .controlBar{position: absolute;background:rgba(22,22,22,0.8);width:100%;left:0;bottom:0;} .timelapse_hud .hover-hide{opacity:0} .timelapse_hud:hover .hover-hide{opacity:1;z-index:5} - +.video_grid{overflow: auto;height: 100%;display: block;} +.video_grid .col-md-2{padding-left:5px;padding-right:5px;padding-bottom:10px} +.video_grid .thumb{width:100%;height:150px;display:inline-block;background-size:cover;position:relative;overflow:hidden;border-radius:4px;border:1px solid #000;box-shadow:0 0 10px #151515} +.video_grid .thumb .title-strip, .video_grid .thumb .button-strip{width:100%;position:absolute;left:0;background:rgba(0,0,0,0.7);color:#fff;padding:4px} +.video_grid .thumb .title-strip{top:0;opacity:0.5} +.video_grid .thumb .button-strip{bottom:0;opacity:0} +.video_grid .thumb:hover .title-strip, .video_grid .thumb:hover .button-strip{opacity:1} .table-striped>tbody>tr>td{vertical-align:middle} +.table-striped .thumbnail{width:100px;height:80px;border-radius:5px;margin:0;display:inline-block;} #motion_list{height:155px;overflow:auto;border-radius:5px;border:1px solid #444;position:relative;background: #222;margin:0} .dark .list-group-item{border-color: #444;background:#222} .dark .list-group-item.active{background:#c49a68;border-color:#a7865f} @@ -456,7 +465,7 @@ ul.msg_list li .message { .nav>li>a:focus, .nav>li>a:hover,.nav .open>a, .nav .open>a:focus, .nav .open>a:hover{background:#867560} .mdl-js-layout.hide-side:not(.is-small-screen){ - + } @media screen and (min-width: 1025px){ .mdl-js-layout.hide-side:not(.is-small-screen)>.mdl-layout__drawer { @@ -639,7 +648,7 @@ ul.msg_list li .message { } /*animations*/ -@keyframes blink { +@keyframes blink { 0% { opacity:1 } 50% { opacity:0 } 100% { opacity:1 } @@ -730,4 +739,4 @@ ul.msg_list li .message { .bg-hexagon { background-color: #054e9f; background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='49' viewBox='0 0 28 49'%3E%3Cg fill-rule='evenodd'%3E%3Cg id='hexagons' fill='%23fdfdfd' fill-opacity='0.4' fill-rule='nonzero'%3E%3Cpath d='M13.99 9.25l13 7.5v15l-13 7.5L1 31.75v-15l12.99-7.5zM3 17.9v12.7l10.99 6.34 11-6.35V17.9l-11-6.34L3 17.9zM0 15l12.98-7.5V0h-2v6.35L0 12.69v2.3zm0 18.5L12.98 41v8h-2v-6.85L0 35.81v-2.3zM15 0v7.5L27.99 15H28v-2.31h-.01L17 6.35V0h-2zm0 49v-8l12.99-7.5H28v2.31h-.01L17 42.15V49h-2z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); -} \ No newline at end of file +} diff --git a/web/libs/css/super-page.css b/web/libs/css/super-page.css new file mode 100644 index 0000000..62c608c --- /dev/null +++ b/web/libs/css/super-page.css @@ -0,0 +1,6 @@ +.navbar.bg-primary,.nav.bg-primary { + background-color: #903619 !important; +} +.navbar.bg-success { + background-color: #1f791b !important; +} diff --git a/web/libs/js/basic.js b/web/libs/js/basic.js new file mode 100644 index 0000000..5c98c92 --- /dev/null +++ b/web/libs/js/basic.js @@ -0,0 +1,36 @@ +var tool = {} +tool.getVideoImage = function (path, secs, callback) { + var me = this, video = document.createElement('video'); + var backCalled = false + var finish = function(err,data){ + if(!backCalled){ + backCalled = true + callback(err,data) + clearTimeout(timeout) + } + } + var timeout = setTimeout(function(){ + finish(new Error('Failed Getting Snap from Video')) + },5000) + video.onloadedmetadata = function() { + video.play() + this.currentTime = Math.min(Math.max(0, (secs < 0 ? this.duration : 0) + secs), this.duration) + video.pause() + }; + video.onseeked = function(e) { + var canvas = document.createElement('canvas') + canvas.height = video.videoHeight + canvas.width = video.videoWidth + var ctx = canvas.getContext('2d') + ctx.drawImage(video, 0, 0, canvas.width, canvas.height) + var base64 = canvas.toDataURL() + finish(null, base64) + delete(ctx) + delete(video) + delete(canvas) + }; + video.onerror = function(e) { + finish(e) + }; + video.src = path; +} diff --git a/web/libs/js/extra.js b/web/libs/js/extra.js new file mode 100644 index 0000000..8c2c5ec --- /dev/null +++ b/web/libs/js/extra.js @@ -0,0 +1,11 @@ +var browserCheck = {} +browserCheck.forLastPass = function(){ + var lastpass = navigator.plugins['LastPass']; + if (lastpass === undefined) { + return false; + } + return true; +} +if(browserCheck.forLastPass){ + alert('It appears you are using LastPass. Be aware that LastPass is known to cause issues with the form fields.') +} diff --git a/web/libs/js/main.dash2.js b/web/libs/js/main.dash2.js index 485875f..a5f7f9a 100644 --- a/web/libs/js/main.dash2.js +++ b/web/libs/js/main.dash2.js @@ -123,6 +123,32 @@ switch($user.details.lang){ return base64 } + $.ccio.destroyStream = function(d,user,killElement){ + if(d.mid && !d.id)d.id = d.mid + console.log(d.ke+d.id+user.auth_token) + console.log($.ccio.mon[d.ke+d.id+user.auth_token]) + if($.ccio.mon[d.ke+d.id+user.auth_token]){ + console.log('destroy') + $.ccio.init('closeVideo',{mid:d.id,ke:d.ke},user); + $.ccio.init('jpegModeStop',{mid:d.id,ke:d.ke},user); + $.ccio.init('clearTimers',d,user) + clearInterval($.ccio.mon[d.ke+d.id+user.auth_token].signal);delete($.ccio.mon[d.ke+d.id+user.auth_token].signal); + $.ccio.mon[d.ke+d.id+user.auth_token].watch = 0; + $.ccio.mon[d.ke+d.id+user.auth_token].PoseidonErrorCount = 0 + if($.ccio.mon[d.ke+d.id+user.auth_token].hls){$.ccio.mon[d.ke+d.id+user.auth_token].hls.destroy()} + if($.ccio.mon[d.ke+d.id+user.auth_token].Poseidon){$.ccio.mon[d.ke+d.id+user.auth_token].Poseidon.stop()} + if($.ccio.mon[d.ke+d.id+user.auth_token].Base64){$.ccio.mon[d.ke+d.id+user.auth_token].Base64.disconnect()} + if($.ccio.mon[d.ke+d.id+user.auth_token].h265Socket){$.ccio.mon[d.ke+d.id+user.auth_token].h265Socket.disconnect()} + if($.ccio.mon[d.ke+d.id+user.auth_token].h265Player){$.ccio.mon[d.ke+d.id+user.auth_token].h265Player.stop()} + if($.ccio.mon[d.ke+d.id+user.auth_token].dash){$.ccio.mon[d.ke+d.id+user.auth_token].dash.reset()} + if($.ccio.mon[d.ke+d.id+user.auth_token].h265HttpStream && $.ccio.mon[d.ke+d.id+user.auth_token].abort){ + $.ccio.mon[d.ke+d.id+user.auth_token].h265HttpStream.abort() + } + if(killElement){ + $.grid.data().removeWidget($('#monitor_live_'+d.id+user.auth_token)) + } + } + } $.ccio.init=function(x,d,user,k){ if(!k){k={}};k.tmp=''; if(d&&d.user){ @@ -401,16 +427,16 @@ switch($user.details.lang){ case'humanReadMode': switch(d){ case'idle': - k.mode='<%-cleanLang(lang['Idle'])%>' + k.mode=lang['Idle'] break; case'stop': - k.mode='<%-cleanLang(lang['Disabled'])%>' + k.mode=lang['Disabled'] break; case'record': - k.mode='<%-cleanLang(lang['Record'])%>' + k.mode=lang['Record'] break; case'start': - k.mode='<%-cleanLang(lang['Watch Only'])%>' + k.mode=lang['Watch Only'] break; } return k.mode @@ -488,7 +514,7 @@ switch($user.details.lang){ $.ccio.mon[d.ke+d.mid+user.auth_token].Base64.disconnect() } if($.ccio.mon[d.ke+d.mid+user.auth_token].Poseidon){ - $.ccio.mon[d.ke+d.mid+user.auth_token].Poseidon.destroy() + $.ccio.mon[d.ke+d.mid+user.auth_token].Poseidon.stop() } } break; @@ -590,7 +616,7 @@ switch($user.details.lang){ return $.ccio.init('tf',d.time,user)+'.'+d.ext break; case'filters': - k.tmp=''; + k.tmp=''; $.each(user.details.filters,function(n,v){ k.tmp+='' }); @@ -621,8 +647,14 @@ switch($user.details.lang){ } break; case'url': - if(d.port==80){d.porty=''}else{d.porty=':'+d.port} - d.url=d.protocol+'://'+d.host+d.porty;return d.url; + var porty + if(d.port && d.port !== ''){ + porty = ':' + d.port + }else{ + porty = '' + } + d.url = d.protocol + '://' + d.host + porty + return d.url break; case'data-video': if(!d){ @@ -664,7 +696,7 @@ switch($user.details.lang){ case'hls':case'flv':case'mp4': if(d.p.find('video')[0].paused){ if(d.d.signal_check_log==1){ - d.log={type:'Stream Check',msg:'<%-cleanLang(lang.clientStreamFailedattemptingReconnect)%>'} + d.log={type:'Stream Check',msg:lang.clientStreamFailedattemptingReconnect} $.ccio.tm(4,d,'#logs,.monitor_item[mid="'+d.id+'"][ke="'+d.ke+'"][auth="'+user.auth_token+'"] .logs') } $.ccio.cx({f:'monitor',ff:'watch_on',id:d.id},user); @@ -908,9 +940,9 @@ switch($user.details.lang){ d.hr=parseInt(d.startMoment.format('HH')), d.per=parseInt(d.hr/24*100); d.circle='
'+d.hr+'
' - tmp+='
  • '+d.circle+'
    <%-cleanLang(lang.Start)%> : '+d.startMoment.format('h:mm:ss , MMMM Do YYYY')+'
    <%-cleanLang(lang.End)%> : '+d.endMoment.format('h:mm:ss , MMMM Do YYYY')+'
    '+(parseInt(d.size)/1000000).toFixed(2)+'mb
    ' + tmp+='
  • '+d.circle+'
    '+lang.Start+' : '+d.startMoment.format('h:mm:ss , MMMM Do YYYY')+'
    '+lang.End+' : '+d.endMoment.format('h:mm:ss , MMMM Do YYYY')+'
    '+(parseInt(d.size)/1000000).toFixed(2)+'mb
    ' <% if(config.DropboxAppKey){ %> tmp+='' <% } %> - tmp+='
  • '; + tmp+=''; $(z).each(function(n,v){ v=$(v); if(v.find('.video-item').length>10){v.find('.video-item:last').remove()} @@ -918,7 +950,7 @@ switch($user.details.lang){ break; case 1://monitor icon d.src=placeholder.getData(placeholder.plcimg({bgcolor:'#b57d00',text:'...'})); - tmp+='
    '+d.name+'
    '+d.mid+'
    <%-cleanLang(lang['Save as'])%> : '+d.ext+'
    Status : '+d.status+'
    ' + tmp+='
    '+d.name+'
    '+d.mid+'
    '+lang['Save as']+' : '+d.ext+'
    Status : '+d.status+'
    ' tmp+='
    ' var buttons = { "Pop": { @@ -967,7 +999,9 @@ switch($user.details.lang){ tmp+='
    '; tmp+='
    '; tmp+='
    '; - tmp+='
    ' + tmp+='
    ' + tmp+='
    '; + tmp+='
    ' $.each([ {label:'Currently Detected',tag:'stream-detected-count'} ],function(n,v){ @@ -977,7 +1011,7 @@ switch($user.details.lang){ tmp+='
    '; tmp+='
    '; tmp+='
    '; - tmp+='
    '+d.name+', <%-cleanLang(lang['Recording FPS'])%> : '+d.fps+'
    '; + tmp+='
    '+d.name+', '+lang['Recording FPS']+' : '+d.fps+'
    '; tmp+='
    '; tmp+='
    '//start of btn list var buttons = { @@ -1029,6 +1063,12 @@ switch($user.details.lang){ "class": "default", "icon": "angle-double-right" }, + "Video Grid": { + "label": "Video Grid", + "attr": "monitor=\"video_grid\"", + "class": "default", + "icon": "th" + }, "Videos List": { "label": "Videos List", "attr": "monitor=\"videos_table\"", @@ -1169,17 +1209,18 @@ switch($user.details.lang){ tmp+='
    ' tmp+=' ' @@ -1187,29 +1228,29 @@ switch($user.details.lang){ tmp+='
    ' tmp+=' ' tmp+='
    ' tmp+='
    ' tmp+=' ' tmp+='
    ' tmp+='
    ' tmp+=' ' tmp+='
    ' @@ -1225,35 +1266,35 @@ switch($user.details.lang){ tmp+='
    '; tmp+=' '; tmp+='
    '; tmp+='
    '; tmp+=' '; tmp+='
    '; tmp+='
    '; tmp+=' '; tmp+='
    '; tmp+='
    '; @@ -1265,10 +1306,10 @@ switch($user.details.lang){ if(!d.secure){d.secure="0"} tmp+='
    ' tmp+='

    '+d.host+' '+d.ke+' 

    ' - tmp+='
    ' - tmp+='
    ' - tmp+='
    ' - tmp+='
    ' + tmp+='
    ' + tmp+='
    ' + tmp+='
    ' + tmp+='
    ' tmp+='
    '; break; case 'form-group'://Input Map Selector @@ -1311,7 +1352,7 @@ switch($user.details.lang){ case 'input-map-selector'://Input Map Selector if(!d.map){d.map=''} tmp+='
    ' - tmp+='
    @@ -246,4 +258,4 @@
    -
    \ No newline at end of file +
    diff --git a/web/pages/blocks/header-favicon.ejs b/web/pages/blocks/header-favicon.ejs index d451cbe..6fd79ac 100644 --- a/web/pages/blocks/header-favicon.ejs +++ b/web/pages/blocks/header-favicon.ejs @@ -1,3 +1,5 @@ + + @@ -9,4 +11,4 @@ - \ No newline at end of file + diff --git a/web/pages/blocks/header.ejs b/web/pages/blocks/header.ejs index 69845d3..feec05f 100644 --- a/web/pages/blocks/header.ejs +++ b/web/pages/blocks/header.ejs @@ -8,8 +8,6 @@ - - <% include header-favicon.ejs %> @@ -18,4 +16,4 @@ <% cleanLang = function(string){ if(!string){string=''} return string.replace(/'/g,"\\'") -}%> \ No newline at end of file +}%> diff --git a/web/pages/blocks/mainpermissions.ejs b/web/pages/blocks/mainpermissions.ejs index 8d9f9ce..82d17c8 100644 --- a/web/pages/blocks/mainpermissions.ejs +++ b/web/pages/blocks/mainpermissions.ejs @@ -34,7 +34,7 @@
    @@ -66,8 +66,8 @@
    @@ -75,7 +75,7 @@
    @@ -83,7 +83,7 @@
    @@ -91,7 +91,7 @@
    @@ -99,7 +99,7 @@
    @@ -107,7 +107,7 @@
    @@ -115,7 +115,7 @@
    @@ -123,7 +123,7 @@
    @@ -131,7 +131,7 @@
    @@ -139,15 +139,15 @@
    \ No newline at end of file +
    diff --git a/web/pages/blocks/settings.ejs b/web/pages/blocks/settings.ejs index f3dce57..b758fc7 100644 --- a/web/pages/blocks/settings.ejs +++ b/web/pages/blocks/settings.ejs @@ -30,14 +30,14 @@
    @@ -46,7 +46,7 @@
    @@ -144,7 +144,7 @@
    @@ -194,11 +194,34 @@
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    <% } %> + <% if(details.use_bb_b2!=='0'){ %> +
    +

    <%-lang['Backblaze B2']%>

    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + +
    +
    +
    + +
    +
    + <% } %> <% if(details.use_discordbot!=='0'){ %>

    <%-lang['Discord Bot']%>

    @@ -304,8 +400,8 @@
    @@ -349,7 +445,7 @@
    @@ -362,15 +458,15 @@
    @@ -398,4 +494,4 @@
    -
    \ No newline at end of file + diff --git a/web/pages/blocks/videoview.ejs b/web/pages/blocks/videoview.ejs index b950bda..6baa5f1 100644 --- a/web/pages/blocks/videoview.ejs +++ b/web/pages/blocks/videoview.ejs @@ -1,5 +1,5 @@ -