diff --git a/INSTALL/nodejs-ubuntu.sh b/INSTALL/nodejs-ubuntu.sh index b3cac4de..e5a27105 100644 --- a/INSTALL/nodejs-ubuntu.sh +++ b/INSTALL/nodejs-ubuntu.sh @@ -1,8 +1,27 @@ +#!/bin/sh + +# Get the Ubuntu version +UBUNTU_VERSION=$(lsb_release -rs) +NODE_MAJOR=18 + +# Check if Ubuntu version is 18.04 +if [ "$UBUNTU_VERSION" = "18.04" ]; then + NODE_MAJOR=16 +fi + +echo "Installing Node version: $NODE_MAJOR" + +# Update and install necessary packages sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg + +# Setup NodeSource keyring and sources list sudo mkdir -p /etc/apt/keyrings curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | sudo gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg -NODE_MAJOR=18 + +# Add NodeSource repository echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | sudo tee /etc/apt/sources.list.d/nodesource.list + +# Update package list and install Node.js sudo apt-get update -sudo apt-get install nodejs -y +sudo apt-get install -y nodejs diff --git a/INSTALL/ubuntu-touchless.sh b/INSTALL/ubuntu-touchless.sh index d60a9fbc..0310ba4c 100644 --- a/INSTALL/ubuntu-touchless.sh +++ b/INSTALL/ubuntu-touchless.sh @@ -56,7 +56,7 @@ if ! [ -x "$(command -v ifconfig)" ]; then fi echo "=============" echo "Shinobi - Installing Node.js" -sh $DIR/nodejs-ubuntu.sh +bash $DIR/nodejs-ubuntu.sh if ! [ -x "$(command -v npm)" ]; then sudo apt install npm -y fi diff --git a/definitions/base.js b/definitions/base.js index 8e2f1e41..ac661d6e 100644 --- a/definitions/base.js +++ b/definitions/base.js @@ -3151,12 +3151,30 @@ module.exports = function(s,config,lang){ } ] }, + { + hidden: true, + "id": "detectorsSelected", + "name": "detail=detectors_selected", + "field": lang["Detectors Selected"], + "description": lang.fieldTextDetectorsSelected, + "default": "all", + "attribute": "multiple", + "fieldType": "select", + "form-group-class": "h_casc_input h_casc_1", + "possible": [ + { + "name": `${lang.All} (${lang.Default})`, + "value": "all" + } + ] + }, { "name": "detail=detector_object_ignore_not_move", "field": lang["Ignore Non-Moving"], "default": "0", "fieldType": "select", "selector": "h_obj_ignore_move", + "form-group-class": "h_casc_input h_casc_1", "possible": [ { "name": lang.No, @@ -3183,6 +3201,7 @@ module.exports = function(s,config,lang){ "description": lang["fieldTextDetectorSendFramesObject"], "default": "1", "fieldType": "select", + "form-group-class": "h_casc_input h_casc_1", "possible": [ { "name": lang.No, @@ -3222,6 +3241,7 @@ module.exports = function(s,config,lang){ "default": "1", "example": "", "fieldType": "select", + "form-group-class": "h_casc_input h_casc_1", "possible": [ { "name": lang.No, @@ -3242,6 +3262,7 @@ module.exports = function(s,config,lang){ "example": "", "selector": "h_det_mot_fir", "fieldType": "select", + "form-group-class": "h_casc_input h_casc_1", "possible": [ { "name": lang.No, @@ -3290,7 +3311,6 @@ module.exports = function(s,config,lang){ ] }, { - isAdvanced: true, hidden: true, "name": lang['Event-Based Recording'], "input-mapping": "detector_sip_buffer", diff --git a/languages/en_CA.json b/languages/en_CA.json index 39b0af44..7eb947c6 100644 --- a/languages/en_CA.json +++ b/languages/en_CA.json @@ -18,10 +18,17 @@ "accountEditError": "Account Edit Error", "Monitor Map": "Monitor Map", "Geolocation": "Geolocation", + "Tested on": "Tested on", + "Architecture": "Architecture", + "Operating Systems": "Operating Systems", "fieldTextGeolocation": "The map coordinates of this camera in the real world. This will plot a point for your camera on the Monitor Map.", "playUntilVideoEnd": "Play until video end", "Monitor Saved": "Monitor Saved", "Auto Placement": "Auto Placement", + "Downloaded": "Downloaded", + "GPU Required": "GPU Required", + "Experimental": "Experimental", + "Market": "Market", "Unmute": "Unmute", "byUser": "by user", "accountDeleted": "Account Deleted", @@ -468,6 +475,8 @@ "Max Storage Amount": "Max Storage Amount", "Video Share": "Video Share", "FileBin": "FileBin", + "File Saved": "File Saved", + "checkFileBinForNewFile": "Check the FileBin for the newly created file.", "File Download Ready": "File Download Ready", "Timelapse Video Build Complete": "Timelapse Video Build Complete", "yourFileDownloadedShortly": "Please wait. Your file will be downloaded shortly.", @@ -535,6 +544,7 @@ "Time-lapse Tool": "Time-lapse Tool", "total": "total", "MB": "MB", + "All": "All", "Calendar": "Calendar", "Leave blank for random.": "Leave blank for random.", "Currently viewing": "Currently viewing", @@ -570,6 +580,10 @@ "clientStreamFailedattemptingReconnect": "Client side stream check failed, attempting reconnect.", "Export Video": "Export Video", "Merge Video": "Merge Video", + "Merge Videos": "Merge Videos", + "Merge": "Merge", + "MergeAllSelected": "Merge all selected Videos?", + "MergeAllInRange": "Merge all Videos in date range selected?", "Delete Filter": "Delete Filter", "Delete Files": "Delete Files", "confirmDeleteFilter": "Do you want to delete this filter? You cannot recover it.", @@ -684,6 +698,7 @@ "Minutes": "Minutes", "Custom": "Custom", "Detector": "Detector", + "Detectors Selected": "Detectors Selected", "Audio Detector": "Audio Detector", "Audio Detection": "Audio Detection", "Minimum dB": "Minimum dB", @@ -1093,6 +1108,7 @@ "Can't Connect": "Can't Connect", "Video Finished": "Video Finished", "No Monitors Selected": "No Monitors Selected", + "No Monitor Selected": "No Monitor Selected", "Nothing Selected": "Nothing Selected", "makeASelection": "Make a selection and try again.", "monSavedButNotCopied": "Your monitor was saved but not copied to any other monitor.", @@ -1204,6 +1220,7 @@ "Preview": "Preview", "Websocket Connected": "Websocket Connected", "Websocket Disconnected": "Websocket Disconnected", + "Disconnected": "Disconnected", "Videos Merge": "Videos Merge", "Channel ID": "Channel ID", "Recipient ID": "Recipient ID", @@ -1574,6 +1591,7 @@ "MQTT Client": "MQTT Client", "Buffer Time from Event": "Buffer Time from Event", "detected": "detected", + "fieldTextDetectorsSelected": "Select which detectors to send frames to.", "fieldTextEventFilters": "Enable to have all Events honor your Event Filter rules.", "fieldTextBufferTimeFromEvent": "The amount of seconds to record before the trigger happened. If this is consistently inaccurate you will need to look at the optimization guide or force encoding on the server.", "fieldTextMode": "This is the primary task of the monitor.", @@ -1918,6 +1936,8 @@ "HowToConnectDes1": "This feature is available to Mobile License subscribers. To get an API Key please login to your ShinobiShop account and create a key associated to any active Subscription ID. Learn More.", "HowToConnectDes2": "If you would like to get access to a private (dedicated) P2P server please create an account at the ShinobiShop and contact us via the Live Chat widget", "User": "User", + "Save Unknown Faces": "Save Unknown Faces", + "saveUnknownFacesFieldText": "Save Unknown faces to the Face Manager. Manual sorting may still be required.", "Current Version": "Current Version", "Default is Global value": "Default is Global value", "rejectUnauth": "Ignore server certificate" diff --git a/libs/basic/utils.js b/libs/basic/utils.js index ef66d23a..85586399 100644 --- a/libs/basic/utils.js +++ b/libs/basic/utils.js @@ -1,4 +1,5 @@ const fs = require('fs'); +const fsP = require('fs').promises; const path = require('path'); const moment = require('moment'); const fetch = require('node-fetch'); @@ -217,6 +218,13 @@ module.exports = (processCwd,config) => { readStream.pipe(writeStream) }) } + async function moveFile(inputFilePath,outputFilePath) { + try{ + await fsP.rm(outputFilePath) + }catch(err){} + await copyFile(inputFilePath, outputFilePath) + await fsP.rm(inputFilePath) + } function hmsToSeconds(str) { var p = str.split(':'), s = 0, m = 1; @@ -240,6 +248,17 @@ module.exports = (processCwd,config) => { } } } + async function deleteFilesInFolder(folderPath) { + try { + const files = await fsP.readdir(folderPath); + for (const file of files) { + const filePath = path.join(folderPath, file); + await fsP.rm(filePath, { recursive: true }); + } + } catch (error) { + console.error(`Error deleting files: ${error.message}`); + } + } return { parseJSON: parseJSON, stringJSON: stringJSON, @@ -261,5 +280,7 @@ module.exports = (processCwd,config) => { copyFile: copyFile, hmsToSeconds, setDefaultIfUndefined, + deleteFilesInFolder, + moveFile, } } diff --git a/libs/childNode/utils.js b/libs/childNode/utils.js index 14c85044..642ae15d 100644 --- a/libs/childNode/utils.js +++ b/libs/childNode/utils.js @@ -1,5 +1,8 @@ const fs = require('fs'); module.exports = function(s,config,lang,app,io){ + const { + postProcessCompletedMp4Video, + } = require('../video/utils.js')(s,config,lang) const masterDoWorkToo = config.childNodes.masterDoWorkToo; const maxCpuPercent = config.childNodes.maxCpuPercent || 75; const maxRamPercent = config.childNodes.maxRamPercent || 75; @@ -177,17 +180,21 @@ module.exports = function(s,config,lang,app,io){ filename : filename, filesizeMB : parseFloat((data.filesize/1048576).toFixed(2)) } - s.insertDatabaseRow(monitorConfig,insert) - s.insertCompletedVideoExtensions.forEach(function(extender){ - extender(activeMonitor, monitorConfig, insert) + s.insertDatabaseRow(monitorConfig,insert,function(response){ + postProcessCompletedMp4Video(response.insertQuery).then((isGood) => { + if(!isGood)return console.error(`FAILED VIDEO INSERT`); + s.insertCompletedVideoExtensions.forEach(function(extender){ + extender(activeMonitor, monitorConfig, insert) + }) + //purge over max + s.purgeDiskForGroup(data.ke) + //send new diskUsage values + s.setDiskUsedForGroup(data.ke,insert.filesizeMB) + clearTimeout(activeMonitor.recordingChecker) + clearTimeout(activeMonitor.streamChecker) + resolve(response) + }) }) - //purge over max - s.purgeDiskForGroup(data.ke) - //send new diskUsage values - s.setDiskUsedForGroup(data.ke,insert.filesizeMB) - clearTimeout(activeMonitor.recordingChecker) - clearTimeout(activeMonitor.streamChecker) - resolve(response) }) }) } diff --git a/libs/cron.js b/libs/cron.js index 64a4d0b8..6cf41770 100644 --- a/libs/cron.js +++ b/libs/cron.js @@ -13,6 +13,15 @@ module.exports = (s,config,lang) => { workerProcess.on('message',function(data){ if(data.time === 'moment()')data.time = moment(); switch(data.f){ + case'knexQuery': + s.knexQuery(...data.args, function(...args){ + workerProcess.postMessage({ + f: 'callback', + rid: data.rid, + args, + }) + }); + break; case'debugLog': s.debugLog(...data.data) break; diff --git a/libs/cron/worker.js b/libs/cron/worker.js index 4fc8b4e6..fd074fe8 100644 --- a/libs/cron/worker.js +++ b/libs/cron/worker.js @@ -42,6 +42,13 @@ parentPort.on('message',(data) => { setDefaultConfigOptions() beginProcessing() break; + case'callback': + if(pendingCallbacks[data.rid]){ + pendingCallbacks[data.rid](...data.args) + // console.log(data.rid,typeof pendingCallbacks[data.rid]) + delete(pendingCallbacks[data.rid]) + } + break; case'start':case'restart': setIntervalForCron() break; @@ -51,7 +58,7 @@ parentPort.on('message',(data) => { } }) function debugLog(...args){ - if(config.debugLog === true){ + if(config.debugLog === true || config.logCronInfo === true){ console.log(...([`CRON.js DEBUG LOG ${new Date()}`].concat(args))) } } @@ -64,6 +71,7 @@ function errorLog(...args){ const s = { debugLog, } +const pendingCallbacks = {}; function beginProcessing(){ normalLog(`Worker Processing!`) const { @@ -74,8 +82,6 @@ function beginProcessing(){ } = require('../basic/utils.js')(process.cwd()) const { sqlDate, - knexQuery, - knexQueryPromise, initiateDatabaseEngine } = require('../sql/utils.js')(s,config) var theCronInterval = null @@ -111,6 +117,23 @@ function beginProcessing(){ const setDiskUsedForGroup = (groupKey,size,target,videoRow) => { postMessage({f:'s.setDiskUsedForGroup', ke: groupKey, size: size, target: target, videoRow: videoRow}) } + const knexQuery = (...args) => { + const requestId = generateRandomId(); + const callback = args.pop(); + pendingCallbacks[requestId] = callback; + postMessage({ f: 'knexQuery', args: args, rid: requestId }) + } + const knexQueryPromise = (options) => { + return new Promise((resolve,reject) => { + knexQuery(options,(err,rows) => { + resolve({ + ok: !err, + err: err, + rows: rows, + }) + }) + }) + } const getVideoDirectory = function(e){ if(e.mid&&!e.id){e.id=e.mid}; if(e.details&&(e.details instanceof Object)===false){ diff --git a/libs/dropInEvents.js b/libs/dropInEvents.js index 8acbaccb..d4d7b9c5 100644 --- a/libs/dropInEvents.js +++ b/libs/dropInEvents.js @@ -26,6 +26,9 @@ module.exports = function(s,config,lang,app,io){ const { triggerEvent, } = require('./events/utils.js')(s,config,lang) + const { + deleteFilesInFolder, + } = require('./basic/utils.js')(process.cwd(), config) if(config.dropInEventServer === true){ if(config.dropInEventForceSaveEvent === undefined)config.dropInEventForceSaveEvent = true if(config.dropInEventDeleteFileAfterTrigger === undefined)config.dropInEventDeleteFileAfterTrigger = true @@ -142,7 +145,7 @@ module.exports = function(s,config,lang,app,io){ if(config.dropInEventDeleteFileAfterTrigger){ clearTimeout(fileQueue[filePath]) fileQueue[filePath] = setTimeout(function(){ - exec('rm -rf ' + filePath,function(err){ + fs.rm(filePath, { recursive: true },(err) => { delete(fileQueue[filePath]) }) },1000 * 60 * 5) @@ -152,7 +155,7 @@ module.exports = function(s,config,lang,app,io){ if(config.dropInEventDeleteFileAfterTrigger){ clearTimeout(fileQueueForDeletion[deletionKey]) fileQueueForDeletion[deletionKey] = setTimeout(function(){ - exec('rm -rf ' + deletionKey,function(err){ + fs.rm(filePath, { recursive: true },(err) => { delete(fileQueueForDeletion[deletionKey]) }) },1000 * 60 * 5) @@ -194,7 +197,7 @@ module.exports = function(s,config,lang,app,io){ directory = s.dir.dropInEvents + e.ke + '/' + (e.id || e.mid) + '/' fs.mkdir(directory,function(err){ s.handleFolderError(err) - exec('rm -rf "' + directory + '*"',function(){}) + deleteFilesInFolder(directory) callback(err,directory) }) }) diff --git a/libs/events/utils.js b/libs/events/utils.js index 629c5c87..ba86a0a6 100644 --- a/libs/events/utils.js +++ b/libs/events/utils.js @@ -40,7 +40,7 @@ module.exports = (s,config,lang) => { const monitorId = options.mid || options.id const groupKey = options.ke //if(!frameBuffer || imageSaveEventLock[groupKey + monitorId])return; - if(!frameBuffer || frameBuffer.length === 0 || imageSaveEventLock[groupKey + monitorId]) return; + if(!frameBuffer || frameBuffer.length === 0 || imageSaveEventLock[groupKey + monitorId]) return; const eventTime = options.time const objectsFound = options.matrices const monitorConfig = Object.assign({id: monitorId},s.group[groupKey].rawMonitorConfigurations[monitorId]) @@ -678,17 +678,37 @@ module.exports = (s,config,lang) => { const activeMonitor = s.group[groupKey].activeMonitors[monitorId] const theEmitter = activeMonitor.secondaryDetectorOutput if(!activeMonitor.sendingFromSecondaryDetectorOuput){ - s.debugLog('start sending object frames',groupKey,monitorId) - theEmitter.on('data',activeMonitor.secondaryDetectorOuputContentWriter = (data) => { + const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId] + const monitorDetails = monitorConfig.details; + let chosenDetector = monitorDetails.detectors_selected; + if(chosenDetector instanceof Array)chosenDetector = chosenDetector.join(','); + let sendToDetector = (data) => { s.ocvTx({ f : 'frame', - mon : s.group[groupKey].rawMonitorConfigurations[monitorId].details, + mon : monitorDetails, ke : groupKey, id : monitorId, time : s.formattedTime(), frame : data }) - }) + } + if(chosenDetector && !(chosenDetector.includes('all'))){ + const pluginsGettingIt = chosenDetector.split(',').map(item => item.trim()).filter(item => !!item); + sendToDetector = (data) => { + for(pluginName of pluginsGettingIt){ + s.sendToDetector(pluginName, { + f : 'frame', + mon : monitorDetails, + ke : groupKey, + id : monitorId, + time : s.formattedTime(), + frame : data + }) + } + } + } + s.debugLog('start sending object frames',groupKey,monitorId) + theEmitter.on('data', activeMonitor.secondaryDetectorOuputContentWriter = sendToDetector) } clearTimeout(activeMonitor.sendingFromSecondaryDetectorOuput) activeMonitor.sendingFromSecondaryDetectorOuput = setTimeout(() => { diff --git a/libs/extenders.js b/libs/extenders.js index 0f8fc0fd..9359de4d 100644 --- a/libs/extenders.js +++ b/libs/extenders.js @@ -87,6 +87,8 @@ module.exports = function(s,config){ createExtension(`onSubscriptionCheck`) createExtension(`onDataPortMessage`) createExtension(`onHttpRequestUpgrade`,null,true) + createExtension(`onPluginConnected`) + createExtension(`onPluginDisconnected`) /////// CRON //////// createExtension(`onCronGroupProcessed`) createExtension(`onCronGroupProcessedAwaited`) diff --git a/libs/ffmpeg/builders.js b/libs/ffmpeg/builders.js index af245f93..81ff45e0 100644 --- a/libs/ffmpeg/builders.js +++ b/libs/ffmpeg/builders.js @@ -790,6 +790,8 @@ module.exports = (s,config,lang) => { return `` } const getDefaultSubstreamFields = function(monitor){ + const otherInputFlags = [] + const otherOutputFlags = [] const subStreamFields = parseJSON(monitor.details.substream || {input:{},output:{}}) const inputAndConnectionFields = Object.assign({ "type":"h264", @@ -825,7 +827,15 @@ module.exports = (s,config,lang) => { "svf":"", "cust_stream":"" },subStreamFields.output); + const isMp4Input = inputAndConnectionFields.type === 'mp4'; + const inputTypeCanLoop = isMp4Input || inputAndConnectionFields.type === 'local' + if(inputTypeCanLoop){ + otherInputFlags.push('-stream_loop', '-1'); + if(isMp4Input)otherInputFlags.push('-re'); + } return { + otherInputFlags, + otherOutputFlags, inputAndConnectionFields, outputFields, } diff --git a/libs/fileBin.js b/libs/fileBin.js index 82bdaa31..4ff6a94b 100644 --- a/libs/fileBin.js +++ b/libs/fileBin.js @@ -192,6 +192,11 @@ module.exports = function(s,config,lang,app,io){ }) }) } + s.notifyFileBinUploaded = function(fileBinInsertQuery){ + s.tx(Object.assign({ + f: 'fileBin_item_added', + },fileBinInsertQuery),'GRP_'+fileBinInsertQuery.ke); + } s.getFileBinDirectory = getFileBinDirectory s.getFileBinEntry = getFileBinEntry s.getFileBinBuffer = getFileBinBuffer diff --git a/libs/monitor/utils.js b/libs/monitor/utils.js index 6f3ca3c3..8134ecec 100644 --- a/libs/monitor/utils.js +++ b/libs/monitor/utils.js @@ -278,6 +278,15 @@ module.exports = (s,config,lang) => { } }) } + const sendSubstreamEvent = function(groupKey, monitorId, eventName = 'substream_start'){ + const activeMonitor = getActiveMonitor(groupKey,monitorId) + s.tx({ + f: eventName, + mid: monitorId, + ke: groupKey, + channel: activeMonitor.subStreamChannel + },'GRP_'+groupKey); + } const spawnSubstreamProcess = function(e){ // e = monitorConfig try{ @@ -295,9 +304,13 @@ module.exports = (s,config,lang) => { substreamConfig.input.fulladdress = substreamConfig.input.fulladdress || s.buildMonitorUrl(monitorConfig) substreamConfig.input.rtsp_transport = substreamConfig.input.rtsp_transport || monitorConfig.details.rtsp_transport const { + otherInputFlags, + otherOutputFlags, inputAndConnectionFields, outputFields, } = getDefaultSubstreamFields(monitorConfig); + ffmpegCommand.push(...otherInputFlags); + ffmpegCommand.push(...otherOutputFlags); ([ buildSubstreamString(channelNumber + config.pipeAddition,e), ]).forEach(function(commandStringPart){ @@ -375,12 +388,7 @@ module.exports = (s,config,lang) => { } }) activeMonitor.subStreamProcess = subStreamProcess - s.tx({ - f: 'substream_start', - mid: monitorId, - ke: groupKey, - channel: activeMonitor.subStreamChannel - },'GRP_'+groupKey); + sendSubstreamEvent(groupKey, monitorId) return subStreamProcess }catch(err){ s.systemLog(err) @@ -403,11 +411,7 @@ module.exports = (s,config,lang) => { response.hadSubStream = true response.closeResponse = closeResponse delete(activeMonitor.subStreamProcess) - s.tx({ - f: 'substream_end', - mid: activeMonitor.mid, - ke: activeMonitor.ke - },'GRP_'+activeMonitor.ke); + sendSubstreamEvent(activeMonitor.mid, activeMonitor.ke, 'substream_end') activeMonitor.subStreamProcessClosing = false } }catch(err){ @@ -975,6 +979,7 @@ module.exports = (s,config,lang) => { const groupKey = e.ke const monitorId = e.mid || e.id const activeMonitor = getActiveMonitor(groupKey,monitorId) + if(!activeMonitor)return; clearTimeout(activeMonitor.streamChecker) activeMonitor.streamChecker = setTimeout(function(){ if(activeMonitor && activeMonitor.isStarted === true){ @@ -988,7 +993,7 @@ module.exports = (s,config,lang) => { } }) } - },60000*1); + },60000 * 1); } function resetTimelapseFramesCheck(e){ const groupKey = e.ke @@ -1181,7 +1186,6 @@ module.exports = (s,config,lang) => { }) } if(e.details.detector === '1'){ - s.ocvTx({f:'init_monitor',id:monitorId,ke:groupKey}) //frames from motion detect if(e.details.detector_pam === '1'){ // activeMonitor.spawn.stdio[3].pipe(activeMonitor.p2p).pipe(activeMonitor.pamDiff) @@ -1856,5 +1860,6 @@ module.exports = (s,config,lang) => { setTimedActiveViewerForHttp: setTimedActiveViewerForHttp, attachMainProcessHandlers: attachMainProcessHandlers, removeSenstiveInfoFromMonitorConfig, + sendSubstreamEvent, } } diff --git a/libs/plugins.js b/libs/plugins.js index 9cd51d87..fe0cb4b3 100644 --- a/libs/plugins.js +++ b/libs/plugins.js @@ -43,18 +43,7 @@ module.exports = function(s,config,lang,app,io){ s.detectorPluginArray = [] s.isAtleatOneDetectorPluginConnected = false s.addDetectorPlugin = function(name,d){ - if(config.useOldPluginConnectionMethod === true){ - s.ocv = { - started: s.timeObject(), - id: d.id, - plug: d.plug, - notice: d.notice, - isClientPlugin: d.isClientPlugin, - isHostPlugin: d.isHostPlugin, - connectionType: d.connectionType - } - } - s.connectedDetectorPlugins[d.plug] = { + const newDetector = { started: s.timeObject(), id: d.id, plug: d.plug, @@ -62,15 +51,22 @@ module.exports = function(s,config,lang,app,io){ isClientPlugin: d.isClientPlugin, isHostPlugin: d.isHostPlugin, connectionType: d.connectionType + }; + if(config.useOldPluginConnectionMethod === true){ + s.ocv = newDetector } + s.connectedDetectorPlugins[d.plug] = newDetector s.resetDetectorPluginArray() + s.runExtensionsForArray('onPluginConnected', null, [d.plug, newDetector]) } s.removeDetectorPlugin = function(name){ + const theDetector = Object.assign({}, s.connectedDetectorPlugins[name]) if(config.oldPluginConnectionMethod === true && s.ocv && s.ocv.plug === name){ delete(s.ocv) } delete(s.connectedDetectorPlugins[name]) s.resetDetectorPluginArray(name) + s.runExtensionsForArray('onPluginDisconnected', null, [name, theDetector]) } s.resetDetectorPluginArray = function(){ pluginArray = [] @@ -164,6 +160,10 @@ module.exports = function(s,config,lang,app,io){ }) } } + s.sendToDetector = function(pluginName, data){ + const detector = s.connectedPlugins[pluginName]; + if(detector)detector.tx(data); + } s.sendDetectorInfoToClient = function(data,txFunction){ s.detectorPluginArray.forEach(function(name){ var detectorData = Object.assign(data,{ @@ -363,7 +363,33 @@ module.exports = function(s,config,lang,app,io){ } } } + function onMonitorUpdate(monitorConfig){ + // console.log('Sending Monitor Info to Plugin', monitorConfig.mid) + s.sendToAllDetectors({ f: 'monitorUpdate', monitorConfig }); + } + function sendCopyOfAllMonitorConfigs(){ + const groupKeys = Object.keys(s.group); + for(groupKey of groupKeys){ + const monitorConfigs = Object.values(s.group[groupKey].rawMonitorConfigurations); + for(monitorConfig of monitorConfigs){ + onMonitorUpdate(monitorConfig) + } + } + } + /** + * API : Get List of Connected Plugins + */ + app.get(config.webPaths.apiPrefix+':auth/plugins/list', async (req,res) => { + s.auth(req.params, async (resp) => { + s.closeJsonResponse(res,{ + ok: true, + plugins: s.connectedDetectorPlugins + }) + },res,req) + }) s.onSocketAuthentication(onSocketAuthentication) s.onWebSocketDisconnection(onWebSocketDisconnection) s.onWebSocketConnection(onWebSocketConnection) + s.onMonitorStart(onMonitorUpdate) + s.onPluginConnected(sendCopyOfAllMonitorConfigs) } diff --git a/libs/plugins/superUser.js b/libs/plugins/superUser.js index b6e9d333..7cb07d40 100644 --- a/libs/plugins/superUser.js +++ b/libs/plugins/superUser.js @@ -94,12 +94,13 @@ module.exports = async (s,config,lang,app,io,currentUse) => { const downloadModule = (downloadUrl,packageName) => { const downloadPath = modulesBasePath + packageName try{ - fs.mkdirSync(downloadPath) + fs.mkdirSync(downloadPath, { recursive: true }) }catch(err){ s.debugLog(err) } return new Promise(async (resolve, reject) => { - fs.mkdir(downloadPath, () => { + fs.mkdir(downloadPath, { recursive: true }, (err) => { + if(err)console.error(err) fetchDownloadAndWrite(downloadUrl,downloadPath + '.zip', 1) .then((readStream) => { readStream.pipe(unzipper.Parse()) @@ -228,11 +229,15 @@ module.exports = async (s,config,lang,app,io,currentUse) => { } const enableModule = (name,status) => { // set status to `false` to enable - const modulePath = getModulePath(name) - const confJson = getModuleConfiguration(name) - const confPath = modulePath + 'conf.json' - confJson.enabled = status; - fs.writeFileSync(confPath,s.prettyPrint(confJson)) + try{ + const modulePath = getModulePath(name) + const confJson = getModuleConfiguration(name) + const confPath = modulePath + 'conf.json' + confJson.enabled = status; + fs.writeFileSync(confPath,s.prettyPrint(confJson)) + }catch(err){ + console.error('Failed to Toggle Enable Status for Module.', name, status) + } } const deleteModule = (name) => { // requires restart for changes to take effect diff --git a/libs/timelapse.js b/libs/timelapse.js index 4a543e89..a7eb0ddb 100644 --- a/libs/timelapse.js +++ b/libs/timelapse.js @@ -34,6 +34,7 @@ module.exports = function(s,config,lang,app,io){ s.createTimelapseFrameAndInsert = function(e,location,filename,eventTime,frameDetails){ //e = monitor object //location = file location + var monitorId = e.id || e.mid; var filePath = location + filename var fileStats = fs.statSync(filePath) var details = Object.assign({},frameDetails || {}) @@ -43,7 +44,7 @@ module.exports = function(s,config,lang,app,io){ const timeNow = eventTime || new Date() const queryInfo = { ke: e.ke, - mid: e.id, + mid: monitorId, details: s.s(details), filename: filename, size: fileStats.size, @@ -53,7 +54,7 @@ module.exports = function(s,config,lang,app,io){ var currentDate = s.formattedTime(timeNow,'YYYY-MM-DD') const childNodeData = { ke: e.ke, - mid: e.id, + mid: monitorId, time: currentDate, filename: filename, currentDate: currentDate, @@ -559,8 +560,7 @@ module.exports = function(s,config,lang,app,io){ actionParameter && ( isRestrictedApiKey && apiKeyPermissions.delete_videos_disallowed || isRestricted && !monitorPermissions[`${monitorId}_video_delete`] - ) || - !actionParameter && ( + ) || !actionParameter && ( isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || isRestricted && monitorId && !monitorPermissions[`${monitorId}_video_view`] ) diff --git a/libs/video/utils.js b/libs/video/utils.js index 65b33202..66342c60 100644 --- a/libs/video/utils.js +++ b/libs/video/utils.js @@ -1,6 +1,9 @@ const fs = require('fs') const { spawn } = require('child_process') const async = require('async'); +const path = require('path'); +const moment = require('moment'); +const fsP = require('fs').promises; module.exports = (s,config,lang) => { const { ffprobe, @@ -9,7 +12,9 @@ module.exports = (s,config,lang) => { const { copyFile, hmsToSeconds, + moveFile, } = require('../basic/utils.js')(process.cwd(),config) + const chunkReadSize = 4096; // orphanedVideoCheck : new function const checkIfVideoIsOrphaned = (monitor,videosDirectory,filename) => { const response = {ok: true} @@ -593,6 +598,7 @@ module.exports = (s,config,lang) => { time: video.time, } await s.insertFileBinEntry(fileBinInsertQuery) + s.notifyFileBinUploaded(fileBinInsertQuery) s.tx(Object.assign({ f: 'fileBin_item_added', slicedVideo: true, @@ -604,6 +610,374 @@ module.exports = (s,config,lang) => { } return response } + const mergingVideos = {}; + const mergeVideos = async function({ + groupKey, + monitorId, + filePaths, + outputFilePath, + videoCodec = 'libx265', + audioCodec = 'aac', + onStdout = (data) => {s.systemLog(`${data}`)}, + onStderr = (data) => {s.systemLog(`${data}`)}, + }) { + if (!Array.isArray(filePaths) || filePaths.length === 0) { + throw new Error('First parameter must be a non-empty array of absolute file paths.'); + } + if(mergingVideos[outputFilePath])return; + const currentDate = new Date(); + const fileExtensions = filePaths.map(file => path.extname(file).toLowerCase()); + const allSameExtension = fileExtensions.every(ext => ext === fileExtensions[0]); + const fileList = filePaths.map(file => `file '${file}'`).join('\n'); + const tempFileListPath = path.join(s.dir.streams, groupKey, monitorId, `video_merge_${currentDate}.txt`); + mergingVideos[outputFilePath] = currentDate; + try { + await fsP.writeFile(tempFileListPath, fileList); + let ffmpegArgs; + // if (allSameExtension) { + // ffmpegArgs = [ + // '-f', 'concat', + // '-safe', '0', + // '-i', tempFileListPath, + // '-c', 'copy', + // '-y', + // outputFilePath + // ]; + // } else { + ffmpegArgs = [ + '-loglevel', 'warning', + '-f', 'concat', + '-safe', '0', + '-i', tempFileListPath, + '-c:v', videoCodec, + '-c:a', audioCodec, + '-strict', '-2', + '-crf', '1', + '-y', + outputFilePath + ]; + // } + s.debugLog(fileList) + s.debugLog(ffmpegArgs) + + await new Promise((resolve, reject) => { + const ffmpegProcess = spawn(config.ffmpegDir, ffmpegArgs); + ffmpegProcess.stdout.on('data', onStdout); + ffmpegProcess.stderr.on('data', onStderr); + ffmpegProcess.on('close', (code) => { + delete(mergingVideos[outputFilePath]); + if (code === 0) { + console.log(`FFmpeg process exited with code ${code}`); + resolve(); + } else { + reject(new Error(`FFmpeg process exited with code ${code}`)); + } + }); + ffmpegProcess.on('error', (err) => { + reject(new Error(`FFmpeg error: ${err}`)); + }); + }); + } finally { + await fsP.unlink(tempFileListPath); + } + }; + async function mergeVideosAndBin(videos){ + const currentTime = new Date(); + const firstVideo = videos[0]; + const lastVideo = videos[videos.length - 1]; + const groupKey = firstVideo.ke; + const monitorId = firstVideo.mid; + const logTarget = { ke: groupKey, mid: '$USER' }; + try{ + try{ + await fsP.stat(outputFilePath) + return outputFilePath + }catch(err){ + + } + const filePaths = videos.map(video => { + const monitorConfig = s.group[video.ke].rawMonitorConfigurations[video.mid]; + const filePath = path.join(s.getVideoDirectory(video), `${s.formattedTime(video.time)}.mp4`); + return filePath + }); + const filename = `${s.formattedTime(firstVideo.time)}-${s.formattedTime(lastVideo.time)}-${filePaths.length}.mp4` + const fileBinFolder = s.getFileBinDirectory(firstVideo); + const outputFilePath = path.join(fileBinFolder, filename); + + s.userLog(logTarget,{ + type: 'mergeVideos ffmpeg START', + msg: { + monitorId, + numberOfVideos: filePaths.length, + } + }); + await mergeVideos({ + groupKey, + monitorId, + filePaths, + outputFilePath, + onStdout: (data) => { + s.debugLog(data.toString()) + s.userLog(logTarget,{ + type: 'mergeVideos ffmpeg LOG', + msg: `${data}` + }); + }, + onStderr: (data) => { + s.debugLog(data.toString()) + s.userLog(logTarget,{ + type: 'mergeVideos ffmpeg ERROR', + msg: `${data}` + }); + }, + }); + const fileSize = (await fsP.stat(outputFilePath)).size; + const fileBinInsertQuery = { + ke: groupKey, + mid: monitorId, + name: filename, + size: fileSize, + details: {}, + status: 1, + time: currentTime, + } + await s.insertFileBinEntry(fileBinInsertQuery); + s.notifyFileBinUploaded(fileBinInsertQuery); + return outputFilePath + }catch(err){ + console.log('mergeVideos process ERROR', err) + s.userLog(logTarget,{ + type: 'mergeVideos process ERROR', + msg: `${err.toString()}` + }); + return null; + } + } + + async function readChunkForMoov(filePath, start, end) { + const stream = fs.createReadStream(filePath, { start, end }); + let hasMoov = false; + + for await (const chunk of stream) { + if (chunk.includes('moov')) { + hasMoov = true; + break; + } + } + + return hasMoov; + } + + async function checkMoovAtBeginning(filePath) { + return await readChunkForMoov(filePath, 0, chunkReadSize - 1); + } + + async function checkMoovAtEnd(filePath) { + const stats = await fs.promises.stat(filePath); + const fileSize = stats.size; + return await readChunkForMoov(filePath, fileSize - chunkReadSize, fileSize - 1); + } + + async function hasMoovAtom(filePath) { + const foundAtBeginning = await checkMoovAtBeginning(filePath); + + if (foundAtBeginning) { + return true; + } + + const foundAtEnd = await checkMoovAtEnd(filePath); + return foundAtEnd; + } + const addMoovAtom = async (inputFilePath, outputFilePath, videoCodec = 'libx264', audioCodec = 'aac') => { + try { + const ffmpegArgs = [ + '-i', inputFilePath, + '-c:v', videoCodec, + ]; + if(audioCodec){ + ffmpegArgs.push('-c:a', audioCodec, '-strict', '-2') + }else{ + ffmpegArgs.push('-an') + } + ffmpegArgs.push( + '-movflags', '+faststart', + '-crf', '0', + '-q:a', '0', + outputFilePath + ); + console.log(config.ffmpegDir + ' ' + ffmpegArgs.join(' ')) + return new Promise((resolve, reject) => { + const ffmpegProcess = spawn(config.ffmpegDir, ffmpegArgs); + + ffmpegProcess.stdout.on('data', (data) => { + console.log(`FFmpeg stdout: ${data}`); + }); + + ffmpegProcess.stderr.on('data', (data) => { + console.error(`FFmpeg stderr: ${data}`); + }); + + ffmpegProcess.on('close', (code) => { + if (code === 0) { + resolve(outputFilePath); + } else { + reject(new Error(`FFmpeg process exited with code ${code}`)); + } + }); + + ffmpegProcess.on('error', (err) => { + reject(err); + }); + }); + } catch (error) { + throw new Error(`Failed to re-encode file: ${error.message}`); + } + }; + async function getVideoFrameAsJpeg(filePath, seconds = 7){ + return new Promise((resolve, reject) => { + const ffmpegArgs = [ + '-loglevel', 'warning', + '-ss', seconds.toString(), + '-i', filePath, + '-frames:v', '1', + '-q:v', '2', + '-f', 'image2pipe', + '-vcodec', 'mjpeg', + 'pipe:1' + ]; + const ffmpegProcess = spawn(config.ffmpegDir, ffmpegArgs); + let buffer = Buffer.alloc(0); + ffmpegProcess.stdout.on('data', (data) => { + buffer = Buffer.concat([buffer, data]); + }); + + ffmpegProcess.stderr.on('data', (data) => { + s.debugLog(`getVideoFrameAsJpeg FFmpeg stderr: ${data}`); + }); + + ffmpegProcess.on('close', (code) => { + if (code === 0) { + resolve(buffer); + } else { + reject(new Error(`FFmpeg process exited with code ${code}`)); + } + }); + + ffmpegProcess.on('error', (err) => { + reject(err); + }); + }); + }; + function getVideoPath(video){ + const videoPath = path.join(s.getVideoDirectory(video), `${s.formattedTime(video.time)}.${video.ext}`); + return videoPath + } + async function saveVideoFrameToTimelapse(video, secondsIn = 7){ + // console.error(video) + const monitorConfig = s.group[video.ke].rawMonitorConfigurations[video.mid]; + const activeMonitor = s.group[video.ke].activeMonitors[video.mid]; + const frameTime = moment(video.time).add(secondsIn, 'seconds'); + const frameDate = s.formattedTime(frameTime,'YYYY-MM-DD'); + const timelapseRecordingDirectory = s.getTimelapseFrameDirectory(monitorConfig); + const videoPath = getVideoPath(video); + const frameFilename = s.formattedTime(frameTime) + '.jpg'; + const location = timelapseRecordingDirectory + frameDate + '/'; + const framePath = path.join(location, frameFilename); + try{ + await fsP.stat(framePath) + }catch(err){ + try{ + const frameBuffer = await getVideoFrameAsJpeg(videoPath, secondsIn); + await fsP.mkdir(location, { recursive: true }) + await fsP.writeFile(framePath, frameBuffer) + await s.createTimelapseFrameAndInsert(activeMonitor,location,frameFilename, frameTime._d) + }catch(err){ + console.error(err) + } + } + // console.error('Completed Saving Frame from New Video!', framePath) + } + function getVideoCodecsFromMonitorConfig(video){ + const monitorConfig = s.group[video.ke].rawMonitorConfigurations[video.mid]; + const modeIsRecord = monitorConfig.mode === 'record' + let eventBasedVideoCodec = monitorConfig.details.detector_buffer_vcodec + let eventBasedAudioCodec = monitorConfig.details.detector_buffer_acodec + let recordingVideoCodec = monitorConfig.details.vcodec + let recordingAudioCodec = monitorConfig.details.acodec + switch(eventBasedVideoCodec){ + case null:case '':case undefined:case'auto': + eventBasedVideoCodec = 'libx264' + break; + } + switch(eventBasedAudioCodec){ + case null:case '':case undefined:case'auto': + eventBasedAudioCodec = 'aac' + break; + case'no': + eventBasedAudioCodec = null + break; + } + switch(recordingVideoCodec){ + case null:case '':case undefined:case'auto':case'default':case'copy': + recordingVideoCodec = 'libx264' + break; + } + switch(recordingAudioCodec){ + case null:case '':case undefined:case'auto':case'copy': + recordingAudioCodec = 'aac' + break; + case'no': + recordingAudioCodec = null + break; + } + return { + videoCodec: modeIsRecord ? recordingVideoCodec : eventBasedVideoCodec, + audioCodec: modeIsRecord ? recordingAudioCodec : eventBasedAudioCodec, + recordingVideoCodec, + recordingAudioCodec, + eventBasedVideoCodec, + eventBasedAudioCodec, + } + } + async function postProcessCompletedMp4Video(chosenVideo){ + try { + const video = Object.assign({ + ext: 'mp4' + },chosenVideo); + const videoPath = getVideoPath(video); + // const moovExists = await hasMoovAtom(videoPath); + // if (moovExists) { + // s.debugLog('The file already has a moov atom.'); + // } else { + // return true; + // // const { videoCodec, audioCodec } = getVideoCodecsFromMonitorConfig(video); + // // const tempPath = path.join(s.getVideoDirectory(video), `TEMP_${s.formattedTime(video.time)}.${video.ext}`); + // // await addMoovAtom(videoPath, tempPath, videoCodec, audioCodec); + // // await moveFile(tempPath, videoPath) + // // const newFileSize = (await fsP.stat(videoPath)).size; + // // const updateResponse = await s.knexQueryPromise({ + // // action: "update", + // // table: "Videos", + // // update: { + // // size: newFileSize + // // }, + // // where: [ + // // ['ke','=',video.ke], + // // ['mid','=',video.mid], + // // ['time','=',video.time], + // // ['end','=',video.end], + // // ['ext','=',video.ext], + // // ] + // // }); + // } + // await saveVideoFrameToTimelapse(video, 0) + await saveVideoFrameToTimelapse(video, 7) + return true; + } catch (error) { + console.error('Error processing MP4 file:', error); + return false; + } + }; return { reEncodeVideoAndReplace, stitchMp4Files, @@ -615,5 +989,13 @@ module.exports = (s,config,lang) => { reEncodeVideoAndBinOriginalAddToQueue, archiveVideo, sliceVideo, + mergeVideos, + mergeVideosAndBin, + saveVideoFrameToTimelapse, + postProcessCompletedMp4Video, + readChunkForMoov, + checkMoovAtBeginning, + checkMoovAtEnd, + hasMoovAtom } } diff --git a/libs/videos.js b/libs/videos.js index af766161..7c2f2fb4 100644 --- a/libs/videos.js +++ b/libs/videos.js @@ -5,6 +5,9 @@ module.exports = function(s,config,lang){ const { sendVideoToMasterNode, } = require('./childNode/childUtils.js')(s,config,lang) + const { + postProcessCompletedMp4Video, + } = require('./video/utils.js')(s,config,lang) /** * Gets the video directory of the supplied video or monitor database row. * @constructor @@ -141,7 +144,7 @@ module.exports = function(s,config,lang){ ext: k.ext, size: k.filesize, filesize: k.filesize, - objects: k.objects, + objects: k.objects.substring(0, 510), time: s.timeObject(k.startTime).format('YYYY-MM-DD HH:mm:ss'), end: s.timeObject(k.endTime).format('YYYY-MM-DD HH:mm:ss') } @@ -180,8 +183,11 @@ module.exports = function(s,config,lang){ }) s.insertDatabaseRow(e,k,(err,response) => { if(callback)callback(err,response); - s.insertCompletedVideoExtensions.forEach(function(extender){ - extender(e,k,response.insertQuery,response) + postProcessCompletedMp4Video(response.insertQuery).then((isGood) => { + if(!isGood)return console.error(`FAILED VIDEO INSERT`); + s.insertCompletedVideoExtensions.forEach(function(extender){ + extender(e,k,response.insertQuery,response) + }) }) }) } diff --git a/libs/webServerPaths.js b/libs/webServerPaths.js index 217e83cd..91d64083 100644 --- a/libs/webServerPaths.js +++ b/libs/webServerPaths.js @@ -32,6 +32,7 @@ module.exports = function(s,config,lang,app,io){ spawnSubstreamProcess, destroySubstreamProcess, removeSenstiveInfoFromMonitorConfig, + sendSubstreamEvent, } = require('./monitor/utils.js')(s,config,lang) const { sliceVideo, @@ -39,6 +40,7 @@ module.exports = function(s,config,lang,app,io){ reEncodeVideoAndReplace, reEncodeVideoAndBinOriginalAddToQueue, getVideosBasedOnTagFoundInMatrixOfAssociatedEvent, + mergeVideosAndBin, } = require('./video/utils.js')(s,config,lang) s.renderPage = function(req,res,paths,passables,callback){ passables.window = {} @@ -915,17 +917,39 @@ module.exports = function(s,config,lang,app,io){ const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId] const activeMonitor = s.group[groupKey].activeMonitors[monitorId] const substreamConfig = monitorConfig.details.substream + const theAction = req.query.action if( substreamConfig.output ){ - if(!activeMonitor.subStreamProcess){ - response.ok = true - activeMonitor.allowDestroySubstream = false; - spawnSubstreamProcess(monitorConfig) - }else{ - activeMonitor.allowDestroySubstream = true - await destroySubstreamProcess(activeMonitor) + switch(theAction){ + case'status': + response.ok = true + response.isRunning = !!activeMonitor.subStreamProcess; + response.channel = activeMonitor.subStreamChannel; + break; + case'stop': + activeMonitor.allowDestroySubstream = true + await destroySubstreamProcess(activeMonitor) + break; + default: + if(!activeMonitor.subStreamProcess){ + response.ok = true + activeMonitor.allowDestroySubstream = false; + spawnSubstreamProcess(monitorConfig) + response.channel = activeMonitor.subStreamChannel; + }else{ + sendSubstreamEvent(groupKey, monitorId) + } + break; } + // if(!activeMonitor.subStreamProcess){ + // response.ok = true + // activeMonitor.allowDestroySubstream = false; + // spawnSubstreamProcess(monitorConfig) + // }else{ + // activeMonitor.allowDestroySubstream = true + // await destroySubstreamProcess(activeMonitor) + // } }else{ response.msg = lang['Invalid Settings'] } @@ -1970,7 +1994,69 @@ module.exports = function(s,config,lang,app,io){ res.end(s.prettyPrint(response)); }) },res,req); - }) + }); + /** + * API : Merge Videos and Bin it + */ + app.post(config.webPaths.apiPrefix+':auth/mergeVideos/:ke/:id', function (req,res){ + s.auth(req.params, async function(user){ + const monitorId = req.params.id + const groupKey = req.params.ke + const { + monitorPermissions, + monitorRestrictions, + } = s.getMonitorsPermitted(user.details,monitorId,'video_view') + const { + isRestricted, + isRestrictedApiKey, + apiKeyPermissions, + } = s.checkPermission(user); + if( + isRestrictedApiKey && apiKeyPermissions.watch_videos_disallowed || + isRestricted && ( + monitorId && !monitorPermissions[`${monitorId}_video_view`] || + monitorRestrictions.length === 0 + ) + ){ + s.closeJsonResponse(res,{ok: false, msg: lang['Not Authorized'], videos: []}); + return + } + const response = { ok: false } + const selectedVideos = s.getPostData(req,'videos'); + console.log('selected',selectedVideos) + if(selectedVideos && selectedVideos.length > 1){ + const mergedFilePath = await mergeVideosAndBin(selectedVideos); + response.ok = !!mergedFilePath; + s.closeJsonResponse(res, response); + }else{ + s.sqlQueryBetweenTimesWithPermissions({ + table: 'Videos', + user: user, + noCount: true, + groupKey, + monitorId, + startTime: s.getPostData(req,'start'), + endTime: s.getPostData(req,'end'), + startTimeOperator: s.getPostData(req,'startOperator'), + endTimeOperator: s.getPostData(req,'endOperator'), + noLimit: s.getPostData(req,'noLimit'), + limit: s.getPostData(req,'limit'), + archived: s.getPostData(req,'archived'), + endIsStartTo: !!s.getPostData(req,'endIsStartTo'), + parseRowDetails: false, + rowName: 'videos', + monitorRestrictions: monitorRestrictions, + preliminaryValidationFailed: false + }, async ({ videos }) => { + if(videos){ + const mergedFilePath = await mergeVideosAndBin(videos); + response.ok = !!mergedFilePath; + } + s.closeJsonResponse(res, response); + }) + } + },res,req); + }); /** * API : Get Login Tokens */ diff --git a/plugins/pluginBase.js b/plugins/pluginBase.js index b027529c..dbaca12a 100644 --- a/plugins/pluginBase.js +++ b/plugins/pluginBase.js @@ -56,8 +56,11 @@ module.exports = function(__dirname, config){ if(!config.hostPort){config.hostPort = 8082} if(config.systemLog === undefined){config.systemLog = true} if(config.connectionType === undefined)config.connectionType = 'websocket' + const imageBuffers = {} s = { group: {}, + monitors: {}, + monitorInfo: {}, dir: {}, isWin: (process.platform === 'win32'), s: (json) => { @@ -217,16 +220,26 @@ module.exports = function(__dirname, config){ cn.emit('init',{ok:true,plug:config.plug,notice:config.notice,type:config.type}) } break; - case'init_monitor': - retryConnection = 0 - if(s.group[d.ke] && s.group[d.ke][d.id]){ - s.group[d.ke][d.id].numberOfTriggers = 0 - delete(s.group[d.ke][d.id].cords) - delete(s.group[d.ke][d.id].buffer) - s.onCameraInitExtensions.forEach((extender) => { - extender(d,cn,tx) - }) + case'monitorUpdate': + var monitorConfig = d.monitorConfig; + var groupKey = monitorConfig.ke; + var monitorId = monitorConfig.mid; + var monitorDetails = monitorConfig.details; + var monitorKey = `${groupKey}${monitorId}` + if(!s.monitors[monitorKey])s.monitors[monitorKey] = Object.assign({}, monitorConfig); + var isObjectDetectionSeparate = monitorDetails.detector_use_detect_object === '1' + var width = parseFloat(isObjectDetectionSeparate && monitorDetails.detector_scale_x_object ? monitorDetails.detector_scale_x_object : monitorDetails.detector_scale_x) + var height = parseFloat(isObjectDetectionSeparate && monitorDetails.detector_scale_y_object ? monitorDetails.detector_scale_y_object : monitorDetails.detector_scale_y) + s.monitorInfo[monitorKey] = { + isObjectDetectionSeparate, + width, + height, } + delete(imageBuffers[monitorKey]) + for(extender of s.onCameraInitExtensions){ + extender(monitorConfig, cn, tx) + } + // console.log(monitorId, 'registered', s.monitorInfo[monitorKey]) break; case'frameFromRam': if(!s.group[d.ke]){ @@ -239,29 +252,22 @@ module.exports = function(__dirname, config){ break; case'frame': try{ - if(!s.group[d.ke]){ - s.group[d.ke]={} - } - if(!s.group[d.ke][d.id]){ - s.group[d.ke][d.id] = {} - s.onCameraInitExtensions.forEach((extender) => { - extender(d,cn,tx) - }) - } - if(!s.group[d.ke][d.id].buffer){ - s.group[d.ke][d.id].buffer = [d.frame]; + const monitorKey = `${d.id}${d.ke}`; + imageBuffers[monitorKey] + if(!imageBuffers[monitorKey]){ + imageBuffers[monitorKey] = [d.frame]; }else{ - s.group[d.ke][d.id].buffer.push(d.frame) + imageBuffers[monitorKey].push(d.frame) } if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){ - var buffer = Buffer.concat(s.group[d.ke][d.id].buffer); + var buffer = Buffer.concat(imageBuffers[monitorKey]); processImage(buffer,d,tx) - s.group[d.ke][d.id].buffer = null + imageBuffers[monitorKey] = null } }catch(err){ if(err){ s.systemLog(err) - delete(s.group[d.ke][d.id].buffer) + delete(imageBuffers[monitorKey]) } } break; diff --git a/plugins/pluginWorkerBase.js b/plugins/pluginWorkerBase.js index 69ae375a..4a14f55d 100644 --- a/plugins/pluginWorkerBase.js +++ b/plugins/pluginWorkerBase.js @@ -32,8 +32,11 @@ module.exports = function(__dirname, config){ if(!config.hostPort){config.hostPort = 8082} if(config.systemLog === undefined){config.systemLog = true} if(config.connectionType === undefined)config.connectionType = 'websocket' + const imageBuffers = {} s = { group: {}, + monitors: {}, + monitorInfo: {}, dir: {}, isWin: (process.platform === 'win32'), s: (json) => { @@ -192,16 +195,26 @@ module.exports = function(__dirname, config){ cn.emit('init',{ok:true,plug:config.plug,notice:config.notice,type:config.type}) } break; - case'init_monitor': - retryConnection = 0 - if(s.group[d.ke] && s.group[d.ke][d.id]){ - s.group[d.ke][d.id].numberOfTriggers = 0 - delete(s.group[d.ke][d.id].cords) - delete(s.group[d.ke][d.id].buffer) - s.onCameraInitExtensions.forEach((extender) => { - extender(d,cn,tx) - }) + case'monitorUpdate': + var monitorConfig = d.monitorConfig; + var groupKey = monitorConfig.ke; + var monitorId = monitorConfig.mid; + var monitorDetails = monitorConfig.details; + var monitorKey = `${groupKey}${monitorId}` + if(!s.monitors[monitorKey])s.monitors[monitorKey] = Object.assign({}, monitorConfig); + var isObjectDetectionSeparate = monitorDetails.detector_use_detect_object === '1' + var width = parseFloat(isObjectDetectionSeparate && monitorDetails.detector_scale_x_object ? monitorDetails.detector_scale_x_object : monitorDetails.detector_scale_x) + var height = parseFloat(isObjectDetectionSeparate && monitorDetails.detector_scale_y_object ? monitorDetails.detector_scale_y_object : monitorDetails.detector_scale_y) + s.monitorInfo[monitorKey] = { + isObjectDetectionSeparate, + width, + height, } + delete(imageBuffers[monitorKey]) + for(extender of s.onCameraInitExtensions){ + extender(monitorConfig, cn, tx) + } + // console.log(monitorId, 'registered', s.monitorInfo[monitorKey]) break; case'frameFromRam': if(!s.group[d.ke]){ @@ -214,29 +227,22 @@ module.exports = function(__dirname, config){ break; case'frame': try{ - if(!s.group[d.ke]){ - s.group[d.ke]={} - } - if(!s.group[d.ke][d.id]){ - s.group[d.ke][d.id] = {} - s.onCameraInitExtensions.forEach((extender) => { - extender(d,cn,tx) - }) - } - if(!s.group[d.ke][d.id].buffer){ - s.group[d.ke][d.id].buffer = [d.frame]; + const monitorKey = `${d.id}${d.ke}`; + imageBuffers[monitorKey] + if(!imageBuffers[monitorKey]){ + imageBuffers[monitorKey] = [d.frame]; }else{ - s.group[d.ke][d.id].buffer.push(d.frame) + imageBuffers[monitorKey].push(d.frame) } if(d.frame[d.frame.length-2] === 0xFF && d.frame[d.frame.length-1] === 0xD9){ - var buffer = Buffer.concat(s.group[d.ke][d.id].buffer); + var buffer = Buffer.concat(imageBuffers[monitorKey]); processImage(buffer,d,tx) - s.group[d.ke][d.id].buffer = null + imageBuffers[monitorKey] = null } }catch(err){ if(err){ s.systemLog(err) - delete(s.group[d.ke][d.id].buffer) + delete(imageBuffers[monitorKey]) } } break; diff --git a/tools/downloadPlugins.js b/tools/downloadPlugins.js index 616a5683..3183513c 100644 --- a/tools/downloadPlugins.js +++ b/tools/downloadPlugins.js @@ -1,5 +1,5 @@ const fetch = require('node-fetch'); -const AdmZip = require('adm-zip'); +const unzipper = require('unzipper'); const fs = require('fs').promises; const path = require('path'); @@ -21,13 +21,14 @@ async function fetchAndDownloadFolder(folderName) { throw new Error(`Failed to download folder: ${response.statusText}`); } - const buffer = await response.buffer(); - const zip = new AdmZip(buffer); - - // Extract the ZIP to a temporary location + // Create a temporary path for extraction const tempExtractPath = path.join(process.cwd(), 'temp_extracted'); await fs.mkdir(tempExtractPath, { recursive: true }); - zip.extractAllTo(tempExtractPath, true); + + // Extract the ZIP using unzipper + await response.body + .pipe(unzipper.Extract({ path: tempExtractPath })) + .promise(); // Find the folder ending with the target name const extractedFolder = (await fs.readdir(tempExtractPath)).find(dir => dir.endsWith(folderName)); @@ -66,3 +67,5 @@ if (!folderName) { console.error('Usage: node script.js '); process.exit(1); } + +fetchAndDownloadFolder(folderName); diff --git a/web/assets/css/super.pluginManager.css b/web/assets/css/super.pluginManager.css index 10bb8054..c071d352 100644 --- a/web/assets/css/super.pluginManager.css +++ b/web/assets/css/super.pluginManager.css @@ -12,3 +12,6 @@ border: 1px solid #009dff; border-radius: 10px; } +#superPluginManager .badge-success { + background-color: #2fa523; +} diff --git a/web/assets/js/bs5.dashboard-base.js b/web/assets/js/bs5.dashboard-base.js index 65fc4445..791dec6f 100644 --- a/web/assets/js/bs5.dashboard-base.js +++ b/web/assets/js/bs5.dashboard-base.js @@ -1102,3 +1102,11 @@ function makeButton({ color, link, text, class: classes}){ function replaceBrokenImage(_this){ $(_this).attr('src', `${libURL}/libs/img/bg.jpg`) } +function getQueryString(){ + var theObject = {} + location.search.substring(1).split('&').forEach(function(string){ + var parts = string.split('=') + theObject[parts[0]] = parts[1] + }) + return theObject +} diff --git a/web/assets/js/bs5.embed.js b/web/assets/js/bs5.embed.js index b11081e5..9dc72f66 100644 --- a/web/assets/js/bs5.embed.js +++ b/web/assets/js/bs5.embed.js @@ -2,6 +2,7 @@ var loadedLiveGrids = {} var monitorPops = {} var liveGridElements = {} var runningJpegStreams = {} +var containerElement = $(`#monitors_live`) var liveGrid = $('#monitors_live .stream-element-container') var websocketPath = checkCorrectPathEnding(urlPrefix) + 'socket.io' // @@ -75,7 +76,7 @@ function buildLiveGridBlock(monitor){ var monitorDetails = safeJsonParse(monitor.details) var monitorLiveId = `monitor_live_${monitor.mid}` var subStreamChannel = monitor.subStreamChannel - var streamType = subStreamChannel ? monitorDetails.substream ? monitorDetails.substream.output.stream_type : 'hls' : monitorDetails.stream_type + var streamType = monitorDetails.stream_type === 'useSubstream' ? monitorDetails.substream.output.stream_type : monitorDetails.stream_type var streamElement = buildStreamElementHtml(streamType) var streamBlockInfo = definitions['Monitor Stream Window'] if(!loadedLiveGrids[monitor.mid])loadedLiveGrids[monitor.mid] = {} @@ -99,21 +100,18 @@ function buildLiveGridBlock(monitor){ function drawLiveGridBlock(monitorConfig,subStreamChannel){ var monitorId = monitorConfig.mid - if($('#monitor_live_' + monitorId).length === 0){ - var html = buildLiveGridBlock(monitorConfig) - liveGrid.html(html); - console.log(liveGrid.length,html) - var theBlock = $('#monitor_live_' + monitorId); - var streamElement = theBlock.find('.stream-element') - liveGridElements[monitorId] = { - monitorItem: theBlock, - streamElement: streamElement, - eventObjects: theBlock.find('.stream-objects'), - motionMeter: theBlock.find('.indifference .progress-bar'), - motionMeterText: theBlock.find('.indifference .progress-bar span'), - width: streamElement.width(), - height: streamElement.height(), - } + var html = buildLiveGridBlock(monitorConfig) + liveGrid.html(html); + var theBlock = $('#monitor_live_' + monitorId); + var streamElement = theBlock.find('.stream-element') + liveGridElements[monitorId] = { + monitorItem: theBlock, + streamElement: streamElement, + eventObjects: theBlock.find('.stream-objects'), + motionMeter: theBlock.find('.indifference .progress-bar'), + motionMeterText: theBlock.find('.indifference .progress-bar span'), + width: streamElement.width(), + height: streamElement.height(), } initiateLiveGridPlayer(loadedMonitors[monitorId],subStreamChannel) } @@ -131,15 +129,15 @@ function unmuteVideoPlayer(){ },3000) $('.unmute-embed-audio').remove() } -function initiateLiveGridPlayer(monitor,subStreamChannel){ +function initiateLiveGridPlayer(monitor){ var livePlayerElement = loadedLiveGrids[monitor.mid] var details = monitor.details var groupKey = monitor.ke var monitorId = monitor.mid var loadedMonitor = loadedMonitors[monitorId] var loadedPlayer = loadedLiveGrids[monitor.mid] - var containerElement = $(`#monitor_live_${monitor.mid}`) - var streamType = subStreamChannel ? details.substream ? details.substream.output.stream_type : 'hls' : details.stream_type + var subStreamChannel = loadedMonitor.subStreamChannel + var streamType = details.stream_type === 'useSubstream' ? details.substream.output.stream_type : details.stream_type switch(streamType){ case'jpeg': startJpegStream(monitorId) @@ -542,10 +540,20 @@ function toggleSubStream(monitorId,callback){ } if(monitor.subStreamToggleLock)return false; monitor.subStreamToggleLock = true - $.getJSON(getApiPrefix() + '/toggleSubstream/'+$user.ke+'/'+monitorId,function(d){ - monitor.subStreamToggleLock = false - debugLog(d) - if(callback)callback() + var substreamUrl = getApiPrefix() + '/toggleSubstream/'+$user.ke+'/'+monitorId; + $.getJSON(`${substreamUrl}?action=status`,function(response){ + if(!response.isRunning){ + $.getJSON(substreamUrl,function(d){ + monitor.subStreamChannel = d.channel + monitor.subStreamToggleLock = false + debugLog(d) + if(callback)callback() + }) + }else{ + monitor.subStreamChannel = response.channel + monitor.subStreamToggleLock = false + if(callback)callback() + } }) } $(document).ready(function(e){ @@ -595,8 +603,8 @@ $(document).ready(function(e){ case'monitor_watch_on': var monitorId = d.mid || d.id var loadedMonitor = loadedMonitors[monitorId] - var subStreamChannel = d.subStreamChannel - if(!loadedMonitor.subStreamChannel && loadedMonitor.details.stream_type === 'useSubstream'){ + var subStreamChannel = loadedMonitor.subStreamChannel || d.channel + if(loadedMonitor.details.stream_type === 'useSubstream'){ toggleSubStream(monitorId,function(){ drawLiveGridBlock(loadedMonitors[monitorId],subStreamChannel) }) @@ -619,7 +627,6 @@ $(document).ready(function(e){ $('body').addClass('jpegMode') break; case'detector_trigger': - console.log(d) var monitorId = d.id var liveGridElement = liveGridElements[monitorId] if(!window.dontShowDetection && liveGridElement){ diff --git a/web/assets/js/bs5.embed.utils.js b/web/assets/js/bs5.embed.utils.js index 239f7324..4b53051a 100644 --- a/web/assets/js/bs5.embed.utils.js +++ b/web/assets/js/bs5.embed.utils.js @@ -73,3 +73,11 @@ function drawMatrices(event,options){ }) theContainer.append(html) } +function getQueryString(){ + var theObject = {} + location.search.substring(1).split('&').forEach(function(string){ + var parts = string.split('=') + theObject[parts[0]] = parts[1] + }) + return theObject +} diff --git a/web/assets/js/bs5.monitorSettings.js b/web/assets/js/bs5.monitorSettings.js index 31d220d8..c82101a1 100644 --- a/web/assets/js/bs5.monitorSettings.js +++ b/web/assets/js/bs5.monitorSettings.js @@ -15,6 +15,7 @@ var monSectionPresets = $('#monSectionPresets') var copySettingsSelector = $('#copy_settings') var monitorPresetsSelection = $('#monitorPresetsSelection') var monitorPresetsNameField = $('#monitorPresetsName') +var detectorsSelected = $('#detectorsSelected') var monitorsList = monitorEditorWindow.find('.monitors_list') var editorForm = monitorEditorWindow.find('form') var tagsInput = monitorEditorWindow.find('[name="tags"]') @@ -566,7 +567,37 @@ function drawInputMapSelectorHtml(options,parent){ ` parent.prepend(html) } -function importIntoMonitorEditor(options){ +function getPluginsList(monitorConfig){ + return new Promise((resolve) => { + const chosenDetectors = safeJsonParse(monitorConfig.details).detectors_selected || []; + $.get(getApiPrefix() + '/plugins/list',function(data){ + var plugins = data.plugins || {}; + var pluginNames = Object.keys(plugins) + var disconnectedPlugins = chosenDetectors.filter(item => !pluginNames.includes(item)); + var html = createOptionHtml({ + value: 'all', + label: `${lang.All} (${lang.Default})` + }); + $.each(plugins, function(name, pluginInfo){ + html += createOptionHtml({ + value: name, + label: name, + selected: chosenDetectors.includes(name), + }) + }); + $.each(disconnectedPlugins, function(n, name){ + html += createOptionHtml({ + value: name, + label: `${name} (${lang.Disconnected})`, + selected: true, + }) + }); + detectorsSelected.html(html) + resolve(plugins) + }) + }) +} +async function importIntoMonitorEditor(options){ var monitorConfig = options.values || options var monitorId = monitorConfig.mid var monitorDetails = safeJsonParse(monitorConfig.details); @@ -686,6 +717,9 @@ function importIntoMonitorEditor(options){ } } }); + // + await getPluginsList(monitorConfig) + // copySettingsSelector.val('0').change() var tmp = ''; $.each(loadedMonitors,function(n,monitor){ @@ -1319,9 +1353,11 @@ editorForm.find('[name="type"]').change(function(e){ break; case'detector_plugged': addDetectorPlugin(d.plug,d) + if(monitorEditorSelectedMonitor)getPluginsList(monitorEditorSelectedMonitor); break; case'detector_unplugged': removeDetectorPlugin(d.plug) + if(monitorEditorSelectedMonitor)getPluginsList(monitorEditorSelectedMonitor); break; } }) @@ -1335,6 +1371,7 @@ editorForm.find('[name="type"]').change(function(e){ drawMonitorListToSelector(monitorsList.find('optgroup'),false,'host') monitorsList.val(theSelected) checkToOpenSideMenu() + if(monitorEditorSelectedMonitor)getPluginsList(monitorEditorSelectedMonitor) } addOnTabAway('monitorSettings', function(){ if(isSideBarMenuCollapsed()){ diff --git a/web/assets/js/bs5.monitorsUtils.js b/web/assets/js/bs5.monitorsUtils.js index 392f447d..6ee09b4a 100644 --- a/web/assets/js/bs5.monitorsUtils.js +++ b/web/assets/js/bs5.monitorsUtils.js @@ -1168,6 +1168,9 @@ function getRunningMonitors(asArray){ }) return asArray ? Object.values(foundMonitors) : foundMonitors } +function buildFileBinUrl(data){ + return apiBaseUrl + '/fileBin/' + data.ke + '/' + data.mid + '/' + data.name +} $(document).ready(function(){ $('body') .on('click','[system]',function(){ diff --git a/web/assets/js/bs5.timelapseViewer.js b/web/assets/js/bs5.timelapseViewer.js index cc6b00c8..4d44594e 100644 --- a/web/assets/js/bs5.timelapseViewer.js +++ b/web/assets/js/bs5.timelapseViewer.js @@ -311,9 +311,6 @@ $(document).ready(function(e){ function downloadTimelapseFrame(frame){ downloadFile(frame.href,frame.filename) } - function buildFileBinUrl(data){ - return apiBaseUrl + '/fileBin/' + data.ke + '/' + data.mid + '/' + data.name - } function downloadTimelapseVideo(data){ var downloadUrl = buildFileBinUrl(data) downloadFile(downloadUrl,data.name) diff --git a/web/assets/js/bs5.videos.js b/web/assets/js/bs5.videos.js index 95be4765..046569e6 100644 --- a/web/assets/js/bs5.videos.js +++ b/web/assets/js/bs5.videos.js @@ -442,35 +442,94 @@ function loadEventsData(videoEvents){ loadedEventsInMemory[`${anEvent.mid}${anEvent.time}`] = anEvent }) } +function getVideoSearchRequestQueries(options){ + var searchQuery = options.searchQuery + var requestQueries = [] + var monitorId = options.monitorId + var archived = options.archived + var customVideoSet = options.customVideoSet + var limit = options.limit + var eventLimit = options.eventLimit || 300 + var doLimitOnFames = options.doLimitOnFames || false + var eventStartTime + var eventEndTime + if(options.startDate){ + eventStartTime = formattedTimeForFilename(options.startDate,false) + requestQueries.push(`start=${eventStartTime}`) + } + if(options.endDate){ + eventEndTime = formattedTimeForFilename(options.endDate,false) + requestQueries.push(`end=${eventEndTime}`) + } + if(searchQuery){ + requestQueries.push(`search=${searchQuery}`) + } + if(archived){ + requestQueries.push(`archived=1`) + } + return { + searchQuery, + monitorId, + archived, + customVideoSet, + limit, + eventLimit, + doLimitOnFames, + eventStartTime, + eventEndTime, + requestQueries, + } +} +function mergeVideosAndBin(options,callback){ + const { + searchQuery, + monitorId, + archived, + customVideoSet, + limit, + eventLimit, + doLimitOnFames, + eventStartTime, + eventEndTime, + requestQueries, + } = getVideoSearchRequestQueries(options); + const videos = options.videos.map(video => { + const newVideo = { + ke: video.ke, + mid: video.mid, + time: video.time, + end: video.end, + ext: video.ext, + saveDir: video.saveDir, + details: video.details, + }; + delete(newVideo.timelapseFrames) + return newVideo + }); + console.log(videos) + return new Promise((resolve) => { + $.post(`${getApiPrefix(`mergeVideos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([limit ? `limit=${limit}` : `noLimit=1`]).join('&')}`, { + videos, + },function(data){ + resolve(data) + }) + }) +} function getVideos(options,callback,noEvents){ return new Promise((resolve,reject) => { options = options ? options : {} - var searchQuery = options.searchQuery - var requestQueries = [] - var monitorId = options.monitorId - var archived = options.archived - var customVideoSet = options.customVideoSet - var limit = options.limit - var eventLimit = options.eventLimit || 300 - var doLimitOnFames = options.doLimitOnFames || false - var eventStartTime - var eventEndTime - // var startDate = options.startDate - // var endDate = options.endDate - if(options.startDate){ - eventStartTime = formattedTimeForFilename(options.startDate,false) - requestQueries.push(`start=${eventStartTime}`) - } - if(options.endDate){ - eventEndTime = formattedTimeForFilename(options.endDate,false) - requestQueries.push(`end=${eventEndTime}`) - } - if(searchQuery){ - requestQueries.push(`search=${searchQuery}`) - } - if(archived){ - requestQueries.push(`archived=1`) - } + const { + searchQuery, + monitorId, + archived, + customVideoSet, + limit, + eventLimit, + doLimitOnFames, + eventStartTime, + eventEndTime, + requestQueries, + } = getVideoSearchRequestQueries(options); $.getJSON(`${getApiPrefix(customVideoSet ? customVideoSet : searchQuery ? `videosByEventTag` : `videos`)}${monitorId ? `/${monitorId}` : ''}?${requestQueries.concat([limit ? `limit=${limit}` : `noLimit=1`]).join('&')}`,function(data){ var videos = data.videos.map((video) => { return Object.assign({},video,{ diff --git a/web/assets/js/bs5.videosTable.js b/web/assets/js/bs5.videosTable.js index 112cb21b..af2b866f 100644 --- a/web/assets/js/bs5.videosTable.js +++ b/web/assets/js/bs5.videosTable.js @@ -189,7 +189,7 @@ $(document).ready(function(e){ ${timeAgo(file.time)} ${lang.Start} : ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')} ${lang.End} : ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}`, - objects: file.objects, + objects: `${file.objects}`, tags: ` ${file.ext ? `${file.ext}` : ''} ${!isLocalVideo ? `${file.type}` : ''} @@ -274,6 +274,43 @@ $(document).ready(function(e){ var downloadUrl = buildNewFileLink(data) downloadFile(downloadUrl,data.name) } + function mergeSelectedVideos(){ + var videos = getSelectedRows(true) + var dateRange = getSelectedTime(dateSelector); + var searchQuery = objectTagSearchField.val() || null; + var startDate = dateRange.startDate; + var endDate = dateRange.endDate; + var monitorId = monitorsList.val(); + var wantsArchivedVideo = getVideoSetSelected() === 'archive'; + if(!monitorId){ + new PNotify({ + title: lang['No Monitor Selected'], + text: lang['No Monitor Found, Ignoring Request'], + type: 'danger', + }) + return + } + $.confirm.create({ + title: lang["Merge Videos"], + body: `${videos.length > 0 ? lang.MergeAllSelected : lang.MergeAllInRange}`, + clickOptions: { + title: ' ' + lang.Save, + class: 'btn-success btn-sm' + }, + clickCallback: async function(){ + console.log('Merging Video...') + var result = await mergeVideosAndBin({ + monitorId, + startDate, + endDate, + videos, + searchQuery, + archived: wantsArchivedVideo, + }); + console.log('Merged Video! Check Filebin.') + } + }); + } $('body') .on('click','.open-videosTable',function(e){ e.preventDefault() @@ -307,6 +344,11 @@ $(document).ready(function(e){ zipVideosAndDownloadWithConfirm(videos) return false; }) + .on('click','.merge-selected-videos',function(e){ + e.preventDefault() + mergeSelectedVideos(); + return false; + }) .on('click','.refresh-data',function(e){ e.preventDefault() drawVideosTableViewElements() @@ -410,6 +452,14 @@ $(document).ready(function(e){ }) onWebSocketEvent((data) => { switch(data.f){ + case'fileBin_item_added': + new PNotify({ + title: lang['File Saved'], + text: `${lang.checkFileBinForNewFile}${lang.Download}`, + type: 'success', + sticky: true, + }) + break; case'video_delete': case'video_delete_cloud': if(tabTree.name === 'videosTableView'){ diff --git a/web/assets/js/bs5.wallview.js b/web/assets/js/bs5.wallview.js index 8beb50cf..a015a747 100644 --- a/web/assets/js/bs5.wallview.js +++ b/web/assets/js/bs5.wallview.js @@ -10,6 +10,14 @@ $(document).ready(function(){ var theWindow = $(window); var lastWindowWidth = theWindow.width() var lastWindowHeight = theWindow.height() + function getQueryString(){ + var theObject = {} + location.search.substring(1).split('&').forEach(function(string){ + var parts = string.split('=') + theObject[parts[0]] = parts[1] + }) + return theObject + } function featureIsActivated(showNotice){ if(userHasSubscribed){ return true @@ -61,6 +69,7 @@ $(document).ready(function(){ function selectMonitor(monitorId, css){ css = css || {}; + var embedHost = getQueryString().host || `/`; var isSelected = selectedMonitors[monitorId] if(isSelected)return; var numberOfSelected = Object.keys(selectedMonitors) @@ -69,7 +78,7 @@ $(document).ready(function(){ } ++selectedMonitorsCount selectedMonitors[monitorId] = Object.assign({}, loadedMonitors[monitorId]); - wallViewCanvas.append(``) + wallViewCanvas.append(``) wallViewCanvas.find(`[live-stream="${monitorId}"]`) .draggable({ grid: [40, 40], diff --git a/web/assets/js/super.pluginManager.js b/web/assets/js/super.pluginManager.js index ecf0a0ff..7d7eb694 100644 --- a/web/assets/js/super.pluginManager.js +++ b/web/assets/js/super.pluginManager.js @@ -1,21 +1,69 @@ $(document).ready(function(){ var loadedModules = {} + var downloadablePlugins = {} + var theEnclosure = $('#superPluginManager') var listElement = $('#pluginManagerList') + var downloadListElement = $('#pluginManagerDownloadble') var quickSelect = $('#pluginQuickSelect') var pluginDownloadForm = $('#downloadNewPlugin') var pluginCommandLine = $('#pluginCommandLine') + var pluginDownloadableSearch = $('#pluginManagerDownloadbleListSearch') var getModules = function(callback) { $.get(superApiPrefix + $user.sessionKey + '/plugins/list',callback) } + function getDlPluginId(plugin){ + return `${plugin.name}_${plugin.link}_${plugin.dir}` + } + function drawDownloadablePlugins(data){ + var html = '' + $.each(data,function(n,plugin){ + html += ` + + + + + ${plugin.type.map(item => `${item}`).join(', ')} + ${plugin.gpuRequired ? `${plugin.gpuRequired instanceof Array ? plugin.gpuRequired.join(', ') : lang['GPU Required']}` : ''} + ${plugin.experimental ? `${lang.Experimental}` : ''} + + ${plugin.name} + ${lang['Tested on']} : ${plugin.os.map(item => `${item}`).join(', ')} + ${plugin.engine} ${plugin.arch.map(item => `${item}`).join(' ')} + ${lang.Download} + + + ` + }) + downloadListElement.html(html) + } + function filterDownloadablePlugins(theSearch = '') { + var searchQuery = theSearch.trim().toLowerCase(); + if(searchQuery === ''){ + downloadListElement.find(`[dl-plugin]`).show() + return; + } + var rows = Object.values(downloadablePlugins); + var filtered = [] + rows.forEach((row) => { + var searchInString = JSON.stringify(row).toLowerCase(); + var theElement = downloadListElement.find(`[dl-plugin="${getDlPluginId(row)}"]`) + console.log(searchInString) + if(searchInString.indexOf(searchQuery) > -1){ + theElement.show() + }else{ + theElement.hide() + } + }) + return filtered + } function getDownloadableModules(callback) { return new Promise((resolve,reject) => { const pluginListUrl = `https://cdn.shinobi.video/plugins/list.json` $.getJSON(pluginListUrl,function(data){ - var html = '' $.each(data,function(n,plugin){ - html += `${plugin.name}` + downloadablePlugins[getDlPluginId(plugin)] = plugin; }) - quickSelect.html(html) + drawDownloadablePlugins(data) resolve(data) }) }) @@ -99,7 +147,11 @@ $(document).ready(function(){ $.post(superApiPrefix + $user.sessionKey + '/plugins/download',{ downloadUrl: url, packageRoot: packageRoot, - },callback) + },function(data){ + setTimeout(function(){ + callback(data) + },3000) + }) } }) } @@ -328,13 +380,14 @@ $(document).ready(function(){ break; } }) - pluginDownloadForm.submit(function(e){ - e.preventDefault(); - var el = $(this) - var form = el.serializeObject() - downloadModule(form.downloadUrl,form.packageRoot,function(data){ - console.log(data) + theEnclosure.on('click','[dl-plugin] .download',function(e){ + var pluginName = $(this).parents('[dl-plugin]').attr('dl-plugin') + console.log(pluginName) + var theDlPlugin = downloadablePlugins[pluginName] + downloadModule(theDlPlugin.link,theDlPlugin.dir,function(data){ if(data.ok){ + $('[data-bs-target="#pluginManagerList"],#pluginManagerList').addClass('active') + $('[data-bs-target="#pluginManagerDownloadbleList"],#pluginManagerDownloadbleList').removeClass('active') var theModule = data.newModule theModule.config.enabled = false drawModuleBlock(theModule) @@ -351,7 +404,6 @@ $(document).ready(function(){ } } }) - return false }) $('#pluginQuickSelectExec').click(function(){ var currentVal = quickSelect.val() @@ -362,6 +414,10 @@ $(document).ready(function(){ pluginDownloadForm.find(`[name="packageRoot"]`).val(packageRoot) pluginDownloadForm.submit() }) + pluginDownloadableSearch.keyup(function(){ + var searchQuery = $(this).val() + filterDownloadablePlugins(searchQuery) + }) function getObjectAlphabetically(theObject,key){ return Object.values(theObject).sort(function( a, b ) { const aName = new Date(a[key]).getTime() diff --git a/web/pages/blocks/home/videosTable.ejs b/web/pages/blocks/home/videosTable.ejs index 549062cc..8524836d 100644 --- a/web/pages/blocks/home/videosTable.ejs +++ b/web/pages/blocks/home/videosTable.ejs @@ -21,6 +21,7 @@ <%- lang.Unarchive %> <%- lang.Compress %> <%- lang['Zip and Download'] %> + <%- lang['Merge'] %> <%- lang.Delete %> diff --git a/web/pages/blocks/superPluginManager.ejs b/web/pages/blocks/superPluginManager.ejs index 5de20b29..876bf653 100644 --- a/web/pages/blocks/superPluginManager.ejs +++ b/web/pages/blocks/superPluginManager.ejs @@ -1,29 +1,18 @@ + - <%- lang['Download Plugins'] %> - - - <%- lang.pluginDownloadText %> - - - <% [ - { - "label": "tensorflow-4-1-0", - "value": "https://gitlab.com/Shinobi-Systems/shinobi-plugins/-/archive/master/shinobi-plugins-master.zip,plugins/tensorflow-4-1-0" - }, - { - "label": "tensorflow-2-3-0", - "value": "https://gitlab.com/Shinobi-Systems/shinobi-plugins/-/archive/master/shinobi-plugins-master.zip,plugins/tensorflow-2-3-0" - } - ].forEach((option) => { %> - <%- option.label %> - <% }) %> - - - <%- lang.Download %> + <%- lang['Plugin Manager'] %> + + + <%-lang['Downloaded']%> + + + <%-lang['Download Plugins']%> + + @@ -40,8 +29,16 @@ - + + + + + + + + +
<%- lang.pluginDownloadText %>