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