Infinity Castle
### Changelog #### July 2025 - Fix Sub-Account Manager - Populating the permission selections - Add network scanning tool - Simple tool to scan for Shinobi and Central servers on network - Update copySystemToNewServer.js - Create db during copy process - Add server configuration copy script - Create script that copies Shinobi configurations to another server #### June 2025 - Update createMonitorsJsonFromTxt.js - Ignore empty rows in processing - Fix Videos Table UI - Fix dropdown menu links - Live Grid improvements - Fix substream reload on scroll - Allow adding client-side delay in drawing detection boxes - Detection delay made into float (max value 10, step 0.1) - Disable debug logs for detection delay - Add user logging - Add log when video is deleted from UI - Add logging for command failure to userLog - ONVIF improvements - Refine ONVIF Event handler - Add response.ok true to toggleSubstream action stop - Fix syntax error in controls/onvif.js - Add new features - Add Copy Tags to Monitor Settings > Copy Settings - Add option to use 24-hour time in Videos Table - Server management - Central Management Pair Server wrapped in try script in case of failure to start - Documentation updates - Add argument information about extendersmerge-requests/537/head
parent
2262b37523
commit
ea13a4dc58
|
|
@ -354,6 +354,24 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"field": lang['Use 24 Hour Time in Videos Table'],
|
||||
attribute:'localStorage="use24HourTime"',
|
||||
"description": "",
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"possible": [
|
||||
{
|
||||
"name": lang.No,
|
||||
"value": "0"
|
||||
},
|
||||
{
|
||||
"name": lang.Yes,
|
||||
"value": "1"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"field": lang.Themes,
|
||||
"name": "detail=theme",
|
||||
|
|
|
|||
|
|
@ -2961,6 +2961,72 @@ module.exports = (s,config,lang) => {
|
|||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
"name": lang['Line Counter'],
|
||||
"color": "orange",
|
||||
id: "monSectionLineCounter",
|
||||
isSection: true,
|
||||
isFormGroupGroup: true,
|
||||
"section-class": "h_det_input h_det_1",
|
||||
"info": [
|
||||
{
|
||||
"name": "detail=detectorLineCounter",
|
||||
"field": lang.Enabled,
|
||||
"description": lang["fieldTextDetectorLineCounter"],
|
||||
"default": "0",
|
||||
"fieldType": "select",
|
||||
"selector": "h_det_line",
|
||||
"possible": yesNoPossibility
|
||||
},
|
||||
{
|
||||
"id": "monitorSettings-lineCounter-canvas-container",
|
||||
"form-group-class": "h_det_line_input h_det_line_1",
|
||||
"fieldType": "div",
|
||||
"style": "overflow-x: auto; width: 100%; text-align: center",
|
||||
},
|
||||
{
|
||||
"id": "detector-line-counter-spacing",
|
||||
"form-group-class": "h_det_line_input h_det_line_1",
|
||||
"field": lang['Line Spacing'],
|
||||
"fieldType": "number",
|
||||
"numberMin": "10",
|
||||
"placeholder": "40"
|
||||
},
|
||||
{
|
||||
"name": "detail=detectorLineCounterTags",
|
||||
"form-group-class": "h_det_line_input h_det_line_1",
|
||||
"field": lang['Objects to Count'],
|
||||
"placeholder": "person"
|
||||
},
|
||||
{
|
||||
"id": "monitorSettings-lineCounter-canvas-container",
|
||||
"class": "row",
|
||||
"fieldType": "div",
|
||||
"info": [
|
||||
{
|
||||
"id": "detector-line-counter-name-down",
|
||||
"form-group-class": "h_det_line_input h_det_line_1 col-md-6",
|
||||
"field": lang['Down Label'],
|
||||
"placeholder": "Down"
|
||||
},
|
||||
{
|
||||
"id": "detector-line-counter-name-up",
|
||||
"form-group-class": "h_det_line_input h_det_line_1 col-md-6",
|
||||
"field": lang['Up Label'],
|
||||
"placeholder": "Up"
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"id": "detector-line-counter-reset-daily",
|
||||
"form-group-class": "h_det_line_input h_det_line_1",
|
||||
"field": lang['Reset Daily'],
|
||||
"fieldType": "select",
|
||||
"possible": yesNoPossibility
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
hidden: true,
|
||||
"name": lang['Event-Based Recording'],
|
||||
|
|
@ -3746,6 +3812,17 @@ module.exports = (s,config,lang) => {
|
|||
"form-group-class-pre-layer": "col-md-6",
|
||||
"possible": yesNoPossibility
|
||||
},
|
||||
{
|
||||
"field": lang['Copy Tags'],
|
||||
"description": "",
|
||||
"default": "0",
|
||||
"example": "",
|
||||
"fieldType": "select",
|
||||
"attribute": `copy="field=tags"`,
|
||||
"form-group-class": "h_copy_settings_input h_copy_settings_1",
|
||||
"form-group-class-pre-layer": "col-md-6",
|
||||
"possible": yesNoPossibility
|
||||
},
|
||||
{
|
||||
"field": lang['Compress Completed Videos'],
|
||||
"default": "0",
|
||||
|
|
|
|||
|
|
@ -189,6 +189,8 @@
|
|||
"Connection": "Connection",
|
||||
"Video Set": "Video Set",
|
||||
"Video Accessed": "Video Accessed",
|
||||
"Video Deleted": "Video Deleted",
|
||||
"Cloud Video Deleted": "Cloud Video Deleted",
|
||||
"notEnoughFramesText1": "Not Enough Frames for compilation.",
|
||||
"Allow API Trigger": "Allow API Trigger",
|
||||
"When Detector is Off": "When Detector is Off",
|
||||
|
|
@ -266,6 +268,7 @@
|
|||
"Trigger Camera Groups": "Trigger Camera Groups",
|
||||
"Motion Detection": "Motion Detection",
|
||||
"Object Detection": "Object Detection",
|
||||
"Detection Draw Delay": "Detection Draw Delay",
|
||||
"Minimum Movement": "Minimum Movement",
|
||||
"inPercent": "In Percent",
|
||||
"Hide Detection on Stream": "Hide Detection on Stream",
|
||||
|
|
@ -598,6 +601,7 @@
|
|||
"Set New Videos Directory": "Set New Videos Directory?",
|
||||
"CSS": "CSS <small>Style your dashboard.</small>",
|
||||
"Don't Stretch Monitors": "Don't Stretch Monitors",
|
||||
"Use 24 Hour Time in Videos Table": "Use 24 Hour Time in Videos Table",
|
||||
"Force Monitors Per Row": "Force Monitors Per Row",
|
||||
"Monitors per row": "Monitors per row <small>for Montage</small>",
|
||||
"Browser Console Log": "Browser Console Log",
|
||||
|
|
@ -775,6 +779,7 @@
|
|||
"Custom": "Custom",
|
||||
"Detector": "Detector",
|
||||
"Detectors Selected": "Detectors Selected",
|
||||
"Line Counter": "Line Counter",
|
||||
"Audio Detector": "Audio Detector",
|
||||
"Audio Detection": "Audio Detection",
|
||||
"Minimum dB": "Minimum dB",
|
||||
|
|
@ -960,6 +965,7 @@
|
|||
"Output Method": "Output Method",
|
||||
"Webhook": "Webhook",
|
||||
"Event Webhook Error": "Event Webhook Error",
|
||||
"Event Command Error": "Event Command Error",
|
||||
"Webhook URL": "Webhook URL",
|
||||
"Command on Trigger": "Command on Trigger",
|
||||
"Frames": "Frames",
|
||||
|
|
@ -1423,13 +1429,18 @@
|
|||
"Batch": "Batch",
|
||||
"Subdivision": "Subdivision",
|
||||
"Map": "Map",
|
||||
"Reset Daily": "Reset Daily",
|
||||
"Up Label": "Up Label",
|
||||
"Down Label": "Down Label",
|
||||
"Delay for Snapshot": "Delay for Snapshot",
|
||||
"Add Map": "Add Map",
|
||||
"Add Input Feed": "Add Input Feed",
|
||||
"Add Channel": "Add Channel",
|
||||
"Automatic": "Automatic",
|
||||
"Line Spacing": "Line Spacing",
|
||||
"Max Latency": "Max Latency",
|
||||
"Loop Stream": "Loop Stream",
|
||||
"Objects to Count": "Objects to Count",
|
||||
"Object Count": "Object Count",
|
||||
"Object Tag": "Object Tag",
|
||||
"Search Object Tags": "Search Object Tags",
|
||||
|
|
@ -1445,6 +1456,7 @@
|
|||
"Show Regions of Interest": "Show Regions of Interest",
|
||||
"Confidence of Detection": "Confidence of Detection",
|
||||
"Edit Selected": "Edit Selected",
|
||||
"Copy Tags": "Copy Tags",
|
||||
"Copy Stream Channels": "Copy Stream Channels",
|
||||
"Copy to Selected Monitor(s)": "Copy to Selected Monitor(s)",
|
||||
"Copy Settings": "Copy Settings",
|
||||
|
|
@ -1870,6 +1882,8 @@
|
|||
"fieldTextDetectorNotriggerCommand": "The command that will run. This is the equivalent of running a shell command from terminal.",
|
||||
"fieldTextDetectorNotriggerCommandTimeout": "This value is a timer to allow the next running of your script. This value is in minutes.",
|
||||
"fieldTextDetectorAudio": "Check if Audio has occured at a certiain decible. Decible reading may not be accurate to real-world measurement.",
|
||||
"fieldTextDetectorLineCounter": "Add a Line Crossing Counter. When an object passes over a line a count is kept. Only works with Object Detection or plugins of similar nature.",
|
||||
"fieldTextDetectorLineCounterTags": "Set the object tags that will be counted.",
|
||||
"fieldTextDetectorUseDetectObject": "Create frames for sending to any connected Plugin.",
|
||||
"fieldTextDetectorSendFramesObject": "Push frames to the connected plugin to be analyzed.",
|
||||
"fieldTextDetectorObjCountInRegion": "Count Objects only inside Regions.",
|
||||
|
|
|
|||
|
|
@ -4,45 +4,49 @@ const app = express();
|
|||
var cors = require('cors');
|
||||
var bodyParser = require('body-parser');
|
||||
module.exports = (s,config,lang) => {
|
||||
const { modifyConfiguration, getConfiguration } = require('../../system/utils.js')(config)
|
||||
const pairPort = config.pairPort || 8091
|
||||
const bindIp = config.bindip
|
||||
const server = http.createServer(app);
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({extended: true}));
|
||||
app.use(cors());
|
||||
try{
|
||||
const { modifyConfiguration, getConfiguration } = require('../../system/utils.js')(config)
|
||||
const pairPort = config.pairPort || 8091
|
||||
const bindIp = config.bindip
|
||||
const server = http.createServer(app);
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.urlencoded({extended: true}));
|
||||
app.use(cors());
|
||||
|
||||
server.listen(pairPort, bindIp, function(){
|
||||
console.log('Management Pair Server Listening on '+pairPort);
|
||||
});
|
||||
server.listen(pairPort, bindIp, function(){
|
||||
console.log('Management Pair Server Listening on '+pairPort);
|
||||
});
|
||||
|
||||
const {
|
||||
addManagementServer,
|
||||
removeManagementServer,
|
||||
connectToManagementServer,
|
||||
connectAllManagementServers,
|
||||
} = require('../utils.js')(s,config,lang)
|
||||
const {
|
||||
addManagementServer,
|
||||
removeManagementServer,
|
||||
connectToManagementServer,
|
||||
connectAllManagementServers,
|
||||
} = require('../utils.js')(s,config,lang)
|
||||
|
||||
/**
|
||||
* API : Superuser : Save Management Server Settings
|
||||
*/
|
||||
app.post('/mgmt/connect', async function (req,res){
|
||||
// ws://127.0.0.1:8663
|
||||
let response = {ok: true};
|
||||
const managementServer = req.body.managementServer;
|
||||
if(!config.mgmtServers[managementServer]){
|
||||
const peerConnectKey = req.body.peerConnectKey;
|
||||
if(peerConnectKey){
|
||||
response = await addManagementServer(managementServer, peerConnectKey)
|
||||
await connectToManagementServer(managementServer, peerConnectKey)
|
||||
/**
|
||||
* API : Superuser : Save Management Server Settings
|
||||
*/
|
||||
app.post('/mgmt/connect', async function (req,res){
|
||||
// ws://127.0.0.1:8663
|
||||
let response = {ok: true};
|
||||
const managementServer = req.body.managementServer;
|
||||
if(!config.mgmtServers[managementServer]){
|
||||
const peerConnectKey = req.body.peerConnectKey;
|
||||
if(peerConnectKey){
|
||||
response = await addManagementServer(managementServer, peerConnectKey)
|
||||
await connectToManagementServer(managementServer, peerConnectKey)
|
||||
}else{
|
||||
response.ok = false;
|
||||
response.msg = 'No P2P API Key Provided';
|
||||
}
|
||||
}else{
|
||||
response.ok = false;
|
||||
response.msg = 'No P2P API Key Provided';
|
||||
response.msg = 'Already Configured';
|
||||
}
|
||||
}else{
|
||||
response.ok = false;
|
||||
response.msg = 'Already Configured';
|
||||
}
|
||||
s.closeJsonResponse(res,response)
|
||||
})
|
||||
s.closeJsonResponse(res,response)
|
||||
})
|
||||
}catch(err){
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
if(onvifOptions.mid && onvifOptions.ke){
|
||||
const groupKey = onvifOptions.ke
|
||||
const monitorId = onvifOptions.mid
|
||||
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({ id: e.mid, ke: e.ke })).device;
|
||||
const theDevice = s.group[groupKey].activeMonitors[monitorId].onvifConnection || (await s.createOnvifDevice({ id: monitorId, ke: groupKey })).device;
|
||||
if(theDevice){
|
||||
theUrl = addCredentialsToUrl({
|
||||
username: theDevice.user,
|
||||
|
|
|
|||
|
|
@ -530,22 +530,26 @@ function beginProcessing(){
|
|||
//events - alarms
|
||||
const deleteOldAlarms = function(v){
|
||||
return new Promise((resolve,reject) => {
|
||||
const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10
|
||||
if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){
|
||||
knexQuery({
|
||||
action: "delete",
|
||||
table: "Alarms",
|
||||
where: [
|
||||
['ke','=',v.ke],
|
||||
['time','<', sqlDate(daysOldForDeletion + ' DAY')],
|
||||
]
|
||||
},(err,rrr) => {
|
||||
if(config.alarmManagement){
|
||||
const daysOldForDeletion = v.d.event_days && !isNaN(v.d.event_days) ? parseFloat(v.d.event_days) : 10
|
||||
if(config.cron.deleteEvents === true && daysOldForDeletion !== 0){
|
||||
knexQuery({
|
||||
action: "delete",
|
||||
table: "Alarms",
|
||||
where: [
|
||||
['ke','=',v.ke],
|
||||
['time','<', sqlDate(daysOldForDeletion + ' DAY')],
|
||||
]
|
||||
},(err,rrr) => {
|
||||
resolve()
|
||||
if(err)return errorLog(err);
|
||||
if(rrr && rrr > 0 || config.debugLog === true){
|
||||
postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:'moment()'})
|
||||
}
|
||||
})
|
||||
}else{
|
||||
resolve()
|
||||
if(err)return errorLog(err);
|
||||
if(rrr && rrr > 0 || config.debugLog === true){
|
||||
postMessage({f:'deleteEvents',msg:rrr + ' SQL rows older than ' + daysOldForDeletion + ' days deleted',ke:v.ke,time:'moment()'})
|
||||
}
|
||||
})
|
||||
}
|
||||
}else{
|
||||
resolve()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
module.exports = async function(s,config){
|
||||
s.debugLog('Updating database to 2025-04-13')
|
||||
const {
|
||||
addColumn,
|
||||
} = require('../utils.js')(s,config)
|
||||
await addColumn('Events Counts',[
|
||||
{name: 'name', length: 255, type: 'string'},
|
||||
])
|
||||
}
|
||||
|
|
@ -111,6 +111,7 @@ module.exports = function(s,config){
|
|||
{name: 'mid', length: 100, type: 'string'},
|
||||
{name: 'tag', length: 30, type: 'string'},
|
||||
{name: 'details', type: 'text'},
|
||||
{name: 'name', length: 255, type: 'string'},
|
||||
{name: 'count', type: 'integer', length: 10, defaultTo: 1},
|
||||
{name: 'time', type: 'timestamp', defaultTo: currentTimestamp()},
|
||||
{name: 'end', type: 'timestamp', defaultTo: currentTimestamp()},
|
||||
|
|
@ -214,6 +215,7 @@ module.exports = function(s,config){
|
|||
await require('./migrate/2022-12-18.js')(s,config)
|
||||
await require('./migrate/2023-03-11.js')(s,config)
|
||||
await require('./migrate/2025-03-05.js')(s,config)
|
||||
await require('./migrate/2025-04-13.js')(s,config)
|
||||
delete(s.preQueries)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
module.exports = function(s,config,lang){
|
||||
require('./events/onvif.js')(s,config,lang)
|
||||
require('./events/noEventsDetector.js')(s,config,lang)
|
||||
require('./events/lineCrossCounter.js')(s,config,lang);
|
||||
const { bindTagLegendForMonitors } = require('./events/utils.js')(s,config,lang)
|
||||
s.onAccountSave(function(theGroup,formDetails,user){
|
||||
const groupKey = user.ke
|
||||
|
|
|
|||
|
|
@ -0,0 +1,268 @@
|
|||
class LineCrossCounter {
|
||||
constructor(imageWidth, imageHeight, lines = [], tags = ['person']) {
|
||||
this.imageWidth = imageWidth;
|
||||
this.imageHeight = imageHeight;
|
||||
|
||||
// Line configuration (default: two horizontal lines)
|
||||
this.lines = lines.length >= 2 ? lines : [
|
||||
{ start: { x: 0, y: 194 }, end: { x: imageWidth, y: 194 } },
|
||||
{ start: { x: 0, y: 220 }, end: { x: imageWidth, y: 220 } }
|
||||
];
|
||||
|
||||
this.tags = Array.isArray(tags) ? tags : [tags];
|
||||
this.offset = 15;
|
||||
|
||||
// Tracking state
|
||||
this.resetCounters();
|
||||
this.frameCount = 0;
|
||||
this.startTime = new Date();
|
||||
this.lastFrameTime = null;
|
||||
this.autoResetEnabled = false;
|
||||
this.resetHour = 0; // Midnight (12:00 AM)
|
||||
this.resetMinute = 0;
|
||||
this.lastResetCheck = null;
|
||||
|
||||
this.calculateLineEquations();
|
||||
|
||||
this.trackingHistory = new Map();
|
||||
this.historyDuration = 10000;
|
||||
}
|
||||
|
||||
enableDailyReset(hour = 0, minute = 0) {
|
||||
this.autoResetEnabled = true;
|
||||
this.resetHour = hour;
|
||||
this.resetMinute = minute;
|
||||
this.lastResetCheck = new Date();
|
||||
return `Daily reset enabled at ${hour}:${minute.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
disableDailyReset() {
|
||||
this.autoResetEnabled = false;
|
||||
return "Daily reset disabled";
|
||||
}
|
||||
|
||||
checkForDailyReset() {
|
||||
if (!this.autoResetEnabled) return false;
|
||||
|
||||
const now = new Date();
|
||||
this.lastResetCheck = now;
|
||||
|
||||
// Check if we've crossed the reset time
|
||||
if (now.getHours() === this.resetHour &&
|
||||
now.getMinutes() === this.resetMinute) {
|
||||
|
||||
// Don't reset multiple times in the same minute
|
||||
const lastReset = this.lastDailyReset || new Date(0);
|
||||
if (now.getTime() - lastReset.getTime() > 60000) {
|
||||
this.resetCounters();
|
||||
this.lastDailyReset = now;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
resetCounters() {
|
||||
this.counts = {
|
||||
total: { down: 0, up: 0 },
|
||||
byTag: this.createEmptyTagCounts()
|
||||
};
|
||||
this.currentNearLine1 = new Set(); // Format: `${id}:${tag}`
|
||||
this.currentNearLine2 = new Set();
|
||||
}
|
||||
|
||||
createEmptyTagCounts() {
|
||||
return this.tags.reduce((acc, tag) => {
|
||||
acc[tag] = { down: 0, up: 0 };
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
calculateLineEquations() {
|
||||
this.lineEqs = this.lines.map(line => {
|
||||
const { start, end } = line;
|
||||
const A = end.y - start.y;
|
||||
const B = start.x - end.x;
|
||||
const C = (end.x * start.y) - (start.x * end.y);
|
||||
|
||||
return {
|
||||
A, B, C,
|
||||
minX: Math.min(start.x, end.x),
|
||||
maxX: Math.max(start.x, end.x),
|
||||
minY: Math.min(start.y, end.y),
|
||||
maxY: Math.max(start.y, end.y)
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
isPointNearLine(x, y, lineIndex) {
|
||||
const line = this.lineEqs[lineIndex];
|
||||
if (x < line.minX - this.offset || x > line.maxX + this.offset ||
|
||||
y < line.minY - this.offset || y > line.maxY + this.offset) {
|
||||
return false;
|
||||
}
|
||||
const distance = Math.abs(line.A * x + line.B * y + line.C) /
|
||||
Math.sqrt(line.A * line.A + line.B * line.B);
|
||||
console.log('distance',distance, 'line : ' + lineIndex,', pass :',distance <= this.offset)
|
||||
return distance <= this.offset;
|
||||
}
|
||||
|
||||
processDetections(detections) {
|
||||
this.checkForDailyReset();
|
||||
this.lastFrameTime = new Date();
|
||||
this.frameCount++;
|
||||
|
||||
// Clean up old history entries
|
||||
const now = Date.now();
|
||||
for (const [id, entry] of this.trackingHistory.entries()) {
|
||||
if (now - entry.lastSeen > this.historyDuration) {
|
||||
this.trackingHistory.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.frameCount % 3 !== 0) {
|
||||
return {
|
||||
frameResult: this.getCounts(),
|
||||
changedCount: {
|
||||
total: { down: 0, up: 0 },
|
||||
byTag: {}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const filtered = detections.filter(d => d.tag && this.tags.includes(d.tag));
|
||||
const newNearLine1 = new Set();
|
||||
const newNearLine2 = new Set();
|
||||
const changedCount = {
|
||||
total: { down: 0, up: 0 },
|
||||
byTag: this.createEmptyTagCounts()
|
||||
};
|
||||
|
||||
// Process each detection
|
||||
filtered.forEach(detection => {
|
||||
const { x, y, width, height, id, tag } = detection;
|
||||
const cx = Math.floor(x + width / 2);
|
||||
const cy = Math.floor(y + height / 2);
|
||||
const idTag = `${id}:${tag}`;
|
||||
|
||||
// Update tracking history
|
||||
if (!this.trackingHistory.has(id)) {
|
||||
this.trackingHistory.set(id, {
|
||||
lastSeen: now,
|
||||
positions: [],
|
||||
tag: tag
|
||||
});
|
||||
}
|
||||
const history = this.trackingHistory.get(id);
|
||||
history.lastSeen = now;
|
||||
history.positions.push({ x: cx, y: cy, timestamp: now });
|
||||
|
||||
// Keep only recent positions
|
||||
history.positions = history.positions.filter(
|
||||
pos => now - pos.timestamp <= this.historyDuration
|
||||
);
|
||||
|
||||
// Check line proximity
|
||||
const nearLine1 = this.isPointNearLine(cx, cy, 0);
|
||||
const nearLine2 = this.isPointNearLine(cx, cy, 1);
|
||||
|
||||
if (nearLine1) newNearLine1.add(idTag);
|
||||
if (nearLine2) newNearLine2.add(idTag);
|
||||
|
||||
// Check historical positions for crossings
|
||||
if (history.positions.length > 1) {
|
||||
const crossedLine1To2 = this.checkHistoricalCrossing(history.positions, 0, 1);
|
||||
const crossedLine2To1 = this.checkHistoricalCrossing(history.positions, 1, 0);
|
||||
|
||||
if (crossedLine1To2 && !this.countedCrossings.has(`${id}:down`)) {
|
||||
this.counts.total.down++;
|
||||
this.counts.byTag[tag].down++;
|
||||
changedCount.total.down++;
|
||||
changedCount.byTag[tag].down++;
|
||||
this.countedCrossings.add(`${id}:down`);
|
||||
}
|
||||
|
||||
if (crossedLine2To1 && !this.countedCrossings.has(`${id}:up`)) {
|
||||
this.counts.total.up++;
|
||||
this.counts.byTag[tag].up++;
|
||||
changedCount.total.up++;
|
||||
changedCount.byTag[tag].up++;
|
||||
this.countedCrossings.add(`${id}:up`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.currentNearLine1 = newNearLine1;
|
||||
this.currentNearLine2 = newNearLine2;
|
||||
|
||||
const filteredChangedTags = Object.fromEntries(
|
||||
Object.entries(changedCount.byTag).filter(
|
||||
([_, counts]) => counts.down !== 0 || counts.up !== 0
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
frameResult: this.getCounts(),
|
||||
changedCount: {
|
||||
total: changedCount.total,
|
||||
byTag: filteredChangedTags
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
checkHistoricalCrossing(positions, fromLineIndex, toLineIndex) {
|
||||
// Check if object crossed from one line to another in its position history
|
||||
let wasNearFromLine = false;
|
||||
|
||||
for (const pos of positions) {
|
||||
const nearFromLine = this.isPointNearLine(pos.x, pos.y, fromLineIndex);
|
||||
const nearToLine = this.isPointNearLine(pos.x, pos.y, toLineIndex);
|
||||
|
||||
if (nearFromLine) wasNearFromLine = true;
|
||||
if (wasNearFromLine && nearToLine) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
getCounts() {
|
||||
return {
|
||||
total: this.counts.total,
|
||||
byTag: this.counts.byTag,
|
||||
lines: this.lines,
|
||||
frameCount: this.frameCount,
|
||||
activeTags: this.tags,
|
||||
timestamps: {
|
||||
start: this.startTime.toISOString(),
|
||||
lastFrame: this.lastFrameTime?.toISOString() || null
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateLines(newLines) {
|
||||
if (newLines.length >= 2) {
|
||||
this.lines = newLines;
|
||||
this.calculateLineEquations();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
updateTags(newTags) {
|
||||
this.tags = Array.isArray(newTags) ? newTags : [newTags];
|
||||
// Initialize counts for new tags
|
||||
this.tags.forEach(tag => {
|
||||
if (!this.counts.byTag[tag]) {
|
||||
this.counts.byTag[tag] = { down: 0, up: 0 };
|
||||
}
|
||||
});
|
||||
// Remove old tags
|
||||
Object.keys(this.counts.byTag).forEach(tag => {
|
||||
if (!this.tags.includes(tag)) {
|
||||
delete this.counts.byTag[tag];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = LineCrossCounter;
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
class Tracker {
|
||||
constructor() {
|
||||
this.center_points = {};
|
||||
this.id_count = 0;
|
||||
}
|
||||
|
||||
update(objects_rect) {
|
||||
const objects_bbs_ids = [];
|
||||
|
||||
for (const rect of objects_rect) {
|
||||
const [x, y, w, h] = rect;
|
||||
const cx = Math.floor((x + x + w) / 2);
|
||||
const cy = Math.floor((y + y + h) / 2);
|
||||
|
||||
let same_object_detected = false;
|
||||
for (const [id, pt] of Object.entries(this.center_points)) {
|
||||
const dist = Math.hypot(cx - pt[0], cy - pt[1]);
|
||||
|
||||
if (dist < 35) {
|
||||
this.center_points[id] = [cx, cy];
|
||||
objects_bbs_ids.push([x, y, w, h, parseInt(id)]);
|
||||
same_object_detected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!same_object_detected) {
|
||||
this.center_points[this.id_count] = [cx, cy];
|
||||
objects_bbs_ids.push([x, y, w, h, this.id_count]);
|
||||
this.id_count += 1;
|
||||
}
|
||||
}
|
||||
|
||||
const new_center_points = {};
|
||||
for (const obj_bb_id of objects_bbs_ids) {
|
||||
const object_id = obj_bb_id[4];
|
||||
new_center_points[object_id] = this.center_points[object_id];
|
||||
}
|
||||
|
||||
this.center_points = new_center_points;
|
||||
return objects_bbs_ids;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Tracker;
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
const LineCrossCounter = require('./libs/lineCrossCounter.js');
|
||||
module.exports = (s,config,lang) => {
|
||||
function setupLineCounter(monitorId, groupKey){
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
const monitorConfig = s.group[groupKey].rawMonitorConfigurations[monitorId]
|
||||
const monitorDetails = monitorConfig.details;
|
||||
const lineCounterEnabled = monitorDetails.detectorLineCounter === '1';
|
||||
if(lineCounterEnabled){
|
||||
if(!activeMonitor.lineCounter){
|
||||
const lineCounterSettings = monitorDetails.detectorLineCounterSettings;
|
||||
const downName = lineCounterSettings.downName || 'Down';
|
||||
const upName = lineCounterSettings.upName || 'Up';
|
||||
const resetDaily = lineCounterSettings.resetDaily;
|
||||
const lineCounterTags = monitorDetails.detectorLineCounterTags.split(',');
|
||||
const imageWidth = parseInt(monitorDetails.detector_scale_x_object) || 1280
|
||||
const imageHeight = parseInt(monitorDetails.detector_scale_y_object) || 720
|
||||
if(lineCounterTags.length === 0)lineCounterTags.push('person');
|
||||
// console.log('lineCounterSettings.lines',lineCounterSettings.lines)
|
||||
const counter = new LineCrossCounter(imageWidth, imageHeight, lineCounterSettings.lines, lineCounterTags);
|
||||
counter.name = {
|
||||
down: downName,
|
||||
up: upName,
|
||||
};
|
||||
if(resetDaily){
|
||||
counter.enableDailyReset()
|
||||
}
|
||||
activeMonitor.lineCounter = counter;
|
||||
}
|
||||
}else{
|
||||
delete(activeMonitor.lineCounter)
|
||||
}
|
||||
}
|
||||
function destroyLineCounter(monitorId, groupKey){
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
delete(activeMonitor.lineCounter)
|
||||
}
|
||||
function resetLineCounter(monitorId, groupKey){
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
activeMonitor.lineCounter.resetCounters()
|
||||
}
|
||||
async function saveEventCount(monitorId, groupKey, changedCount, frameResult, eventTime){
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
const lineCounter = activeMonitor.lineCounter;
|
||||
const byTag = changedCount.byTag;
|
||||
for(tag in byTag){
|
||||
const tagCounts = byTag[tag]
|
||||
for(direction in tagCounts){
|
||||
const newCount = tagCounts[direction];
|
||||
if(newCount > 0){
|
||||
// console.log(JSON.stringify(frameResult,null,3))
|
||||
// console.log(JSON.stringify(byTag,null,3))
|
||||
const theCount = frameResult.byTag[tag][direction];
|
||||
// console.log({
|
||||
// action: "insert",
|
||||
// table: "Events Counts",
|
||||
// insert: {
|
||||
// ke: groupKey,
|
||||
// mid: monitorId,
|
||||
// tag: tag,
|
||||
// name: lineCounter.name[direction],
|
||||
// count: theCount,
|
||||
// time: eventTime,
|
||||
// end: eventTime,
|
||||
// details: '{}'
|
||||
// }
|
||||
// })
|
||||
const insertResponse = await s.knexQueryPromise({
|
||||
action: "insert",
|
||||
table: "Events Counts",
|
||||
insert: {
|
||||
ke: groupKey,
|
||||
mid: monitorId,
|
||||
tag: tag,
|
||||
name: lineCounter.name[direction],
|
||||
count: theCount,
|
||||
time: eventTime,
|
||||
end: eventTime,
|
||||
details: '{}'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
async function processDetectionWithLineCounter(monitorId, groupKey, matrices = [], eventTime){
|
||||
const activeMonitor = s.group[groupKey].activeMonitors[monitorId]
|
||||
if(activeMonitor.lineCounter && matrices.length > 0){
|
||||
const { frameResult, changedCount } = activeMonitor.lineCounter.processDetections(matrices);
|
||||
await saveEventCount(monitorId, groupKey, changedCount, frameResult, eventTime)
|
||||
// console.log(frameResult)
|
||||
}
|
||||
}
|
||||
s.onMonitorStart(function(monitorConfig){
|
||||
setupLineCounter(monitorConfig.mid, monitorConfig.ke)
|
||||
})
|
||||
s.onMonitorSave(function(monitorConfig){
|
||||
destroyLineCounter(monitorConfig.mid, monitorConfig.ke)
|
||||
})
|
||||
s.onEventTrigger(function(d,filter,eventTime){
|
||||
const monitorId = d.mid || d.id;
|
||||
const groupKey = d.ke;
|
||||
const eventDetails = d.details;
|
||||
if(eventDetails.reason !== 'motion'){
|
||||
processDetectionWithLineCounter(monitorId, groupKey, eventDetails.matrices, eventTime)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -3,30 +3,59 @@ function hasOnvifEventsEnabled(monitorConfig) {
|
|||
}
|
||||
|
||||
module.exports = function (s, config, lang) {
|
||||
const {Cam} = require("onvif");
|
||||
const { Cam } = require("onvif");
|
||||
const {
|
||||
triggerEvent,
|
||||
} = require('./utils.js')(s, config, lang)
|
||||
|
||||
function handleEvent(event, monitorConfig, onvifEventLog) {
|
||||
const eventValue = event.message?.message?.data?.simpleItem?.$?.Value;
|
||||
if (eventValue === false) {
|
||||
onvifEventLog(`ONVIF Event Stopped`, `topic ${event.topic?._}`)
|
||||
return
|
||||
}
|
||||
onvifEventLog(`ONVIF Event Detected!`, `topic ${event.topic?._}`)
|
||||
triggerEvent({
|
||||
f: 'trigger',
|
||||
id: monitorConfig.mid,
|
||||
ke: monitorConfig.ke,
|
||||
details: {
|
||||
plug: 'onvifEvent',
|
||||
name: 'onvifEvent',
|
||||
reason: event.topic?._,
|
||||
confidence: 100,
|
||||
[event.message?.message?.data?.simpleItem?.$?.Name]: eventValue
|
||||
function handleEvent(event, monitorConfig, onvifEventLog, callback = () => {}) {
|
||||
try{
|
||||
const monitorId = monitorConfig.mid;
|
||||
const groupKey = monitorConfig.ke;
|
||||
s.runExtensionsForArray('onOnvifEventTrigger', null, [event, groupKey, monitorId]);
|
||||
const message = event.message && event.message.message ? event.message.message : event.message;
|
||||
if(!message){
|
||||
console.error(`Missing Message in ONVIF Event from ${monitorConfig.name}`)
|
||||
console.error(err)
|
||||
console.error(JSON.stringify(event,null,3))
|
||||
return
|
||||
}
|
||||
})
|
||||
if(!message.source){
|
||||
// ignore event with no source
|
||||
return;
|
||||
}
|
||||
const topicInternalName = event.topic?._;
|
||||
const sourceSimpleItem = message.source?.simpleItem;
|
||||
const sourceSelected = (sourceSimpleItem instanceof Array ? sourceSimpleItem[sourceSimpleItem.length - 1] : sourceSimpleItem).$;
|
||||
const data = message.data?.simpleItem?.$;
|
||||
const topicShortName = sourceSelected.Value;
|
||||
const eventValue = data.Value;
|
||||
if(topicShortName === "Processor_Usage"){
|
||||
// ignore camera CPU stats
|
||||
return
|
||||
}else if (eventValue === false) {
|
||||
onvifEventLog(`ONVIF Event Stopped`, `topic ${topicInternalName}`)
|
||||
return
|
||||
}
|
||||
onvifEventLog(`ONVIF Event Detected!`, `topic ${topicInternalName}`)
|
||||
callback({
|
||||
f: 'trigger',
|
||||
id: monitorId,
|
||||
ke: groupKey,
|
||||
details: {
|
||||
plug: 'onvifEvent',
|
||||
name: 'onvifEvent',
|
||||
reason: topicInternalName,
|
||||
confidence: 100,
|
||||
token: topicShortName,
|
||||
[data.Name]: eventValue
|
||||
}
|
||||
})
|
||||
}catch(err){
|
||||
console.error(`Failure to parse ONVIF Event from ${monitorConfig.name}`)
|
||||
console.error(err)
|
||||
console.error(JSON.stringify(event,null,3))
|
||||
}
|
||||
}
|
||||
|
||||
function configureOnvif(monitorConfig, onvifEventLog) {
|
||||
|
|
@ -46,8 +75,13 @@ module.exports = function (s, config, lang) {
|
|||
onvifEventLog(`ONVIF Event Error`,error)
|
||||
return
|
||||
}
|
||||
this.on('event', function (event) {
|
||||
this.on('event', config.noEventTriggerForOnvifEvent ? function (event) {
|
||||
handleEvent(event, monitorConfig, onvifEventLog);
|
||||
} : function (event) {
|
||||
handleEvent(event, monitorConfig, onvifEventLog, triggerEvent);
|
||||
})
|
||||
this.on('eventsError', function (e) {
|
||||
onvifEventLog(`ONVIF Event Error`,e)
|
||||
})
|
||||
this.on('eventsError', function (e) {
|
||||
onvifEventLog(`ONVIF Event Error`,e)
|
||||
|
|
|
|||
|
|
@ -480,7 +480,10 @@ module.exports = (s,config,lang) => {
|
|||
var detector_command = addEventDetailsToString(d,monitorDetails.detector_command)
|
||||
if(detector_command === '')return
|
||||
exec(detector_command,{detached: true},function(err){
|
||||
if(err)s.debugLog(err)
|
||||
if(err){
|
||||
s.userLog(d, {type:lang["Event Command Error"],msg:{error:err,cmd:detector_command}})
|
||||
s.debugLog(d.ke, monitorId, detector_command, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -883,23 +886,31 @@ module.exports = (s,config,lang) => {
|
|||
if(!passedMotionLock)return
|
||||
}
|
||||
const thisHasMatrices = hasMatrices(eventDetails)
|
||||
if(thisHasMatrices && monitorDetails.detector_obj_region === '1'){
|
||||
var regions = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].parsedObjects.cordsForObjectDetection
|
||||
var matricesInRegions = isAtleastOneMatrixInRegion(regions,eventDetails.matrices)
|
||||
eventDetails.matrices = matricesInRegions
|
||||
if(matricesInRegions.length === 0)return;
|
||||
if(filter.countObjects && monitorDetails.detector_obj_count === '1' && monitorDetails.detector_obj_count_in_region === '1' && !didCountingAlready){
|
||||
countObjects(eventDetails.matrices)
|
||||
if(thisHasMatrices){
|
||||
if(monitorDetails.detector_obj_region === '1'){
|
||||
var regions = s.group[monitorConfig.ke].activeMonitors[monitorConfig.mid].parsedObjects.cordsForObjectDetection
|
||||
var matricesInRegions = isAtleastOneMatrixInRegion(regions,eventDetails.matrices)
|
||||
eventDetails.matrices = matricesInRegions
|
||||
if(matricesInRegions.length === 0)return;
|
||||
if(filter.countObjects && monitorDetails.detector_obj_count === '1' && monitorDetails.detector_obj_count_in_region === '1' && !didCountingAlready){
|
||||
countObjects(eventDetails.matrices)
|
||||
}
|
||||
}
|
||||
if(monitorDetails.detector_object_ignore_not_move === '1'){
|
||||
const trackerId = `${groupKey}${monitorId}`
|
||||
trackObjectWithTimeout(trackerId,eventDetails.matrices)
|
||||
const trackedObjects = getTracked(trackerId)
|
||||
const objectsThatMoved = getAllMatricesThatMoved(monitorConfig,trackedObjects)
|
||||
setLastTracked(trackerId, trackedObjects)
|
||||
if(objectsThatMoved.length === 0)return;
|
||||
eventDetails.matrices = objectsThatMoved
|
||||
}else if(activeMonitor.lineCounter){
|
||||
const trackerId = `${groupKey}${monitorId}`
|
||||
trackObjectWithTimeout(trackerId,eventDetails.matrices)
|
||||
const trackedObjects = getTracked(trackerId)
|
||||
setLastTracked(trackerId, trackedObjects)
|
||||
eventDetails.matrices = trackedObjects
|
||||
}
|
||||
}
|
||||
if(thisHasMatrices && monitorDetails.detector_object_ignore_not_move === '1'){
|
||||
const trackerId = `${groupKey}${monitorId}`
|
||||
trackObjectWithTimeout(trackerId,eventDetails.matrices)
|
||||
const trackedObjects = getTracked(trackerId)
|
||||
const objectsThatMoved = getAllMatricesThatMoved(monitorConfig,trackedObjects)
|
||||
setLastTracked(trackerId, trackedObjects)
|
||||
if(objectsThatMoved.length === 0)return;
|
||||
eventDetails.matrices = objectsThatMoved
|
||||
}
|
||||
//
|
||||
d.doObjectDetection = (
|
||||
|
|
|
|||
|
|
@ -43,65 +43,191 @@ module.exports = function(s,config){
|
|||
await extender(...args)
|
||||
}
|
||||
}
|
||||
////// INFO //////
|
||||
// Arguments for callback noted below each Extension.
|
||||
// Example use of arguments : s.onSocketAuthentication((userDatabaseRow,socketIoConnection,initiateData,sendDataToClient) => { console.log(userDatabaseRow) })
|
||||
|
||||
////// USER //////
|
||||
createExtension(`onSocketAuthentication`)
|
||||
// [0] userDatabaseRow : Object of User database row
|
||||
// [1] socketIoConnection : Socket.IO Connection Handler
|
||||
// [2] initiateData : Data that was used to initiate the socket authentication
|
||||
// [3] sendDataToClient : function to send data to authenticated connection
|
||||
createExtension(`onUserLog`)
|
||||
// [0] logEvent : the databse row being inserted
|
||||
createExtension(`loadGroupExtender`,`loadGroupExtensions`)
|
||||
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
|
||||
createExtension(`loadGroupAppExtender`,`loadGroupAppExtensions`)
|
||||
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
|
||||
createExtension(`unloadGroupAppExtender`,`unloadGroupAppExtensions`)
|
||||
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
|
||||
createExtension(`onAccountSave`)
|
||||
// [0] groupLoadedInMemory : Object of group initiated in memory.
|
||||
// [1] userDetails : Additional data about the user. If Admin user it will have group data.
|
||||
// [2] userDatabaseRow : Object of User database row.
|
||||
createExtension(`beforeAccountSave`)
|
||||
// [0] infoObject : form (complete post of user data to be update), formDetails (form posted details), userDetails (details in database before save)
|
||||
createExtension(`onTwoFactorAuthCodeNotification`)
|
||||
// [0] userDatabaseRow : Object of User database row
|
||||
createExtension(`onStalePurgeLock`)
|
||||
createExtension(`onVideoAccess`)
|
||||
// [0] groupKey : The ID of the group.
|
||||
// [1] usedSpace : Currently used space (mb).
|
||||
// [2] sizeLimit : Maximum Usable Space (mb).
|
||||
createExtension(`onLogout`)
|
||||
// [0] userDatabaseRow : Object of User database row.
|
||||
// [1] groupKey : The ID of the group.
|
||||
// [2] userId : The ID of the user.
|
||||
// [3] clientIp : IP Address of the user logging out.
|
||||
////// EVENTS //////
|
||||
createExtension(`onEventTrigger`)
|
||||
// [0] eventData : The object that triggered the Event.
|
||||
// [1] filter : The current state of the filters being used for this event.
|
||||
// [2] eventTime : Current Date and Time of the event.
|
||||
createExtension(`onEventTriggerBeforeFilter`)
|
||||
// [0] eventData : The object that triggered the Event.
|
||||
// [1] filter : The state of the filters being modified for this event.
|
||||
createExtension(`onFilterEvent`)
|
||||
// unused
|
||||
createExtension(`onOnvifEventTrigger`)
|
||||
// [0] onvifEventData : The ONVIF Event object that triggered.
|
||||
// [1] groupKey : The ID of the group.
|
||||
// [2] monitorId : The ID of the Monitor.
|
||||
|
||||
////// MONITOR //////
|
||||
createExtension(`onMonitorInit`)
|
||||
// [0] initiateData : Data that was used to initiate the Monitor into memory. Is usually database row of Monitor.
|
||||
createExtension(`onMonitorStart`)
|
||||
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
|
||||
// [1] initiateData : Data that was used to initiate the action.
|
||||
createExtension(`onMonitorStop`)
|
||||
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
|
||||
// [1] initiateData : Data that was used to initiate the action.
|
||||
createExtension(`onMonitorSave`)
|
||||
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
|
||||
// [1] formData : Data that was used to update the monitor.
|
||||
// [2] endData : Response for the update.
|
||||
createExtension(`onMonitorUnexpectedExit`)
|
||||
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
|
||||
// [1] initiateData : Data that was used to initiate the monitor.
|
||||
createExtension(`onDetectorNoTriggerTimeout`)
|
||||
// [0] initiateData : Data that was used to initiate the monitor.
|
||||
createExtension(`onFfmpegCameraStringCreation`)
|
||||
// [0] initiateData : Data that was used to initiate the monitor.
|
||||
// [1] ffmpegCommand : The final FFmpeg command used for the main process of the monitor.
|
||||
createExtension(`onFfmpegBuildMainStream`)
|
||||
// [0] streamType : The Stream Type that was chosen for the main stream.
|
||||
// [1] streamFlags : The complete set of flags used for this output.
|
||||
// [2] initiateData : Data that was used to initiate the monitor.
|
||||
createExtension(`onFfmpegBuildStreamChannel`)
|
||||
// [0] streamType : The Stream Type that was chosen for this output.
|
||||
// [1] streamFlags : The complete set of flags used for this output.
|
||||
// [2] number : The number of this output.
|
||||
// [3] initiateData : Data that was used to initiate the monitor.
|
||||
createExtension(`onMonitorPingFailed`)
|
||||
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
|
||||
// [1] initiateData : Data that was used to initiate the monitor.
|
||||
createExtension(`onMonitorDied`)
|
||||
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
|
||||
// [1] initiateData : Data that was used to initiate the monitor.
|
||||
createExtension(`onMonitorCreateStreamPipe`)
|
||||
// This extension should be used wisely. It can be used to add new stream types.
|
||||
// [0] streamType : The Stream Type that was chosen for this output.
|
||||
// [1] initiateData : Data that was used to initiate the monitor.
|
||||
// [2] resetStreamCheck : Function to reset the timer that indicates if the output is stale.
|
||||
|
||||
///////// SYSTEM ////////
|
||||
createExtension(`onProcessReady`)
|
||||
// [0] ready : This is always true.
|
||||
createExtension(`onProcessExit`)
|
||||
// no arguments
|
||||
createExtension(`onLoadedUsersAtStartup`)
|
||||
// no arguments
|
||||
createExtension(`onBeforeDatabaseLoad`)
|
||||
// [0] config : The Configuration object used to initiate the Shinobi core process.
|
||||
createExtension(`onFFmpegLoaded`)
|
||||
// no arguments
|
||||
createExtension(`beforeMonitorsLoadedOnStartup`)
|
||||
// no arguments
|
||||
createExtension(`onWebSocketConnection`)
|
||||
// [0] socketIoConnection : Socket.IO Connection Handler
|
||||
// [1] validatedAndBindAuthenticationToSocketConnection : N/A
|
||||
// [2] createStreamEmitter : For creating a handler on a stream output
|
||||
createExtension(`onWebSocketDisconnection`)
|
||||
// [0] socketIoConnection : Socket.IO Connection Handler
|
||||
createExtension(`onWebsocketMessageSend`)
|
||||
// [0] socketData : The Data being sent over Socket.IO
|
||||
// [1] socketId : The identifier of where the Socket.IO data is sent to.
|
||||
// [2] originSocket : This is not always set. The socketId that is sending the data to another socketId.
|
||||
createExtension(`onOtherWebSocketMessages`)
|
||||
// [0] socketData : The Data being sent over Socket.IO to the server from the client.
|
||||
// [1] socketIoConnection : Socket.IO Connection Handler of the sender (client)
|
||||
// [2] sendDataToClient : function to send data back to the socketIoConnection.
|
||||
createExtension(`onGetCpuUsage`)
|
||||
// [0] cpuUsagePercent : the percent of CPU Usage.
|
||||
createExtension(`onGetRamUsage`)
|
||||
// [0] ramUsagePercent : the percent of RAM Usage.
|
||||
createExtension(`onSubscriptionCheck`)
|
||||
// unused
|
||||
createExtension(`onDataPortMessage`)
|
||||
// [0] dataPortObject : Data sent back to server on Data Port channel.
|
||||
createExtension(`onHttpRequestUpgrade`,null,true)
|
||||
// [0] request : Request to http.createServer(app) on Upgrade.
|
||||
// [1] socket : Socket of http.createServer(app) on Upgrade.
|
||||
// [2] head : Header sent to http.createServer(app) on Upgrade.
|
||||
createExtension(`onPluginConnected`)
|
||||
// [0] request : Request to http.createServer(app) on Upgrade.
|
||||
createExtension(`onPluginDisconnected`)
|
||||
// [0] pluginName : The internal name of the plugin.
|
||||
// [1] newDetector : Detector information loaded into memory.
|
||||
|
||||
/////// CRON ////////
|
||||
createExtension(`onCronGroupProcessed`)
|
||||
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
|
||||
createExtension(`onCronGroupProcessedAwaited`)
|
||||
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
|
||||
createExtension(`onCronGroupBeforeProcessed`)
|
||||
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
|
||||
createExtension(`onCronGroupBeforeProcessedAwaited`)
|
||||
// [0] userDatabaseRow : Object of User database row. This will always be an Admin user.
|
||||
|
||||
/////// VIDEOS ////////
|
||||
createExtension(`insertCompletedVideoExtender`,`insertCompletedVideoExtensions`)
|
||||
// [0] monitorObject : Active Monitor loaded into memory.
|
||||
// [1] rawInfoAboutVideo : Currently processing information about video.
|
||||
// [2] insertQuery : The insert query used to save the video into the database.
|
||||
createExtension(`onEventBasedRecordingComplete`)
|
||||
// [0] endData : Response for the process.
|
||||
// [1] monitorConfig : Copy of Monitor Configuration loaded into memory.
|
||||
createExtension(`onEventBasedRecordingStart`)
|
||||
// [0] monitorConfig : Copy of Monitor Configuration loaded into memory.
|
||||
// [1] filename : Filename of video saved.
|
||||
createExtension(`onBeforeInsertCompletedVideo`)
|
||||
// [0] monitorObject : Active Monitor loaded into memory.
|
||||
// [1] rawInfoAboutVideo : Currently processing information about video.
|
||||
createExtension(`onVideoAccess`)
|
||||
// [0] videoDatabaseRow : Object of Video database row
|
||||
// [1] userDatabaseRow : Object of User database row. The one accessing the data.
|
||||
// [2] groupKey : The ID of the group.
|
||||
// [3] monitorId : The ID of the Monitor.
|
||||
// [4] clientIp : IP Address of the user accessing the data.
|
||||
createExtension(`onVideoDeleteByUser`)
|
||||
// [0] videoDatabaseRow : Object of Video database row
|
||||
// [1] userDatabaseRow : Object of User database row. The one accessing the data.
|
||||
// [2] groupKey : The ID of the group.
|
||||
// [3] monitorId : The ID of the Monitor.
|
||||
// [4] clientIp : IP Address of the user deleting the data.
|
||||
createExtension(`onCloudVideoDeleteByUser`)
|
||||
// [0] videoDatabaseRow : Object of Video database row
|
||||
// [1] userDatabaseRow : Object of User database row. The one accessing the data.
|
||||
// [2] groupKey : The ID of the group.
|
||||
// [3] monitorId : The ID of the Monitor.
|
||||
// [4] clientIp : IP Address of the user accessing the data.
|
||||
createExtension(`onCloudVideoUploaded`)
|
||||
// [0] insertQuery : The insert query used to save the cloud video info into the database.
|
||||
|
||||
/////// TIMELAPSE ////////
|
||||
createExtension(`onInsertTimelapseFrame`)
|
||||
// [0] initiateData : Data that was used to initiate the monitor.
|
||||
// [1] insertQuery : The insert query used to save the Timelapse frame into the database.
|
||||
// [2] filePath : The filesystem path of the file that was saved.
|
||||
}
|
||||
|
|
|
|||
|
|
@ -854,7 +854,7 @@ module.exports = function(s,config,lang){
|
|||
apiKeyPermissions: {},
|
||||
userPermissions: {},
|
||||
}
|
||||
const permissions = user.permissions
|
||||
const permissions = user.permissions || {}
|
||||
const details = user.details;
|
||||
[
|
||||
'auth_socket',
|
||||
|
|
|
|||
|
|
@ -15,6 +15,38 @@ module.exports = function(s,config,lang){
|
|||
}
|
||||
})
|
||||
})
|
||||
s.onVideoDeleteByUser((videoRow,user,groupKey,monitorId,ip) => {
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
mid: '$USER',
|
||||
},{
|
||||
type: lang['Video Deleted'],
|
||||
msg: {
|
||||
user: {
|
||||
mail: user.mail,
|
||||
uid: user.uid,
|
||||
ip,
|
||||
},
|
||||
video: videoRow
|
||||
}
|
||||
})
|
||||
})
|
||||
s.onCloudVideoDeleteByUser((videoRow,user,groupKey,monitorId,ip) => {
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
mid: '$USER',
|
||||
},{
|
||||
type: lang['Cloud Video Deleted'],
|
||||
msg: {
|
||||
user: {
|
||||
mail: user.mail,
|
||||
uid: user.uid,
|
||||
ip,
|
||||
},
|
||||
video: videoRow
|
||||
}
|
||||
})
|
||||
})
|
||||
s.onLogout((user,groupKey,userId,ip) => {
|
||||
s.userLog({
|
||||
ke: groupKey,
|
||||
|
|
|
|||
|
|
@ -735,6 +735,7 @@ module.exports = function(s,config,lang,app,io){
|
|||
response.channel = activeMonitor.subStreamChannel;
|
||||
break;
|
||||
case'stop':
|
||||
response.ok = true
|
||||
activeMonitor.allowDestroySubstream = true
|
||||
await destroySubstreamProcess(activeMonitor)
|
||||
break;
|
||||
|
|
@ -1816,11 +1817,14 @@ module.exports = function(s,config,lang,app,io){
|
|||
break;
|
||||
case'delete':
|
||||
response.ok = true;
|
||||
const clientIp = s.getClientIp(req)
|
||||
switch(videoParam){
|
||||
case'cloudVideos':
|
||||
s.runExtensionsForArray('onCloudVideoDeleteByUser', null, [r, user, groupKey, monitorId, clientIp])
|
||||
s.deleteVideoFromCloud(r,details.type || r.type || 's3')
|
||||
break;
|
||||
default:
|
||||
s.runExtensionsForArray('onVideoDeleteByUser', null, [r, user, groupKey, monitorId, clientIp])
|
||||
s.deleteVideo(r)
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,225 @@
|
|||
const path = require('path');
|
||||
const fs = require('fs').promises;
|
||||
const { spawn } = require('child_process');
|
||||
const { Client } = require('ssh2');
|
||||
const os = require('os');
|
||||
|
||||
// ── Shinobi helpers ───────────────────────────────────────────────────────────
|
||||
const configRaw = require('../conf.json'); // <- will be copied
|
||||
const s = require('../libs/process.js')(process);
|
||||
const config = require('../libs/config.js')(s);
|
||||
const s2 = { mainDirectory: process.cwd() };
|
||||
|
||||
// ── CLI parsing ───────────────────────────────────────────────────────────────
|
||||
const [
|
||||
, , host,
|
||||
sshUsername,
|
||||
sshPassword,
|
||||
sqlUsername = 'majesticflame',
|
||||
sqlPassword = '',
|
||||
sqlDatabase = 'ccio',
|
||||
sqlPort = 3306,
|
||||
shinobiPath = '/home/Shinobi', // <─ NEW default
|
||||
] = process.argv.map(String);
|
||||
|
||||
if (!host || !sshUsername || !sshPassword || !sqlUsername) {
|
||||
console.log(`** Invalid parameters provided!`)
|
||||
if(!host)console.log('Missing Host!')
|
||||
if(!sshUsername)console.log('Missing SSH Username!')
|
||||
if(!sshPassword)console.log('Missing SSH Password!')
|
||||
console.log(`## Example Usage :`)
|
||||
console.log(`=============`)
|
||||
console.log('node tools/copySystemToNewServer.js HOST SSH_USER SSH_PASS SQL_USER SQL_PASS SQL_DB SQL_PORT SHINOBI_PATH');
|
||||
console.log(`=============`)
|
||||
console.log(`## Usage Parameters :`)
|
||||
console.log(`=============`)
|
||||
console.log(`# HOST : The Server IP Address or Domain Name. Required.`)
|
||||
console.log(`# SSH_USER : The username to login to SSH. Required.`)
|
||||
console.log(`# SSH_PASS : The password to login to SSH. To use RSA passwordless login do "__RSA:/path/to/keyfile". Required.`)
|
||||
console.log(`# SQL_USER : The username for the MySQL (MariaDB) DATABASE. Default is "majesticflame". Optional.`)
|
||||
console.log(`# SQL_PASS : The password for the MySQL (MariaDB) DATABASE. Default is blank. Optional.`)
|
||||
console.log(`# SQL_DB : The database name. Default is "ccio". Optional.`)
|
||||
console.log(`# SQL_PORT : The database port. Default is "3306". Optional.`)
|
||||
console.log(`# SHINOBI_PATH : The path where Shinobi is installed. Default is "/home/Shinobi" Optional.`)
|
||||
console.log(`=============`)
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Source-side DB info (conf.json) ───────────────────────────────────────────
|
||||
const localDbConf = (configRaw.db || {
|
||||
host : '127.0.0.1',
|
||||
user : 'majesticflame',
|
||||
password: '',
|
||||
database: 'ccio',
|
||||
port : 3306,
|
||||
});
|
||||
|
||||
// ── Paths ─────────────────────────────────────────────────────────────────────
|
||||
const dumpName = `shinobi_dump_${Date.now()}.sql`;
|
||||
const dumpPath = path.join(os.tmpdir(), dumpName);
|
||||
const remoteDump = `/tmp/${dumpName}`;
|
||||
|
||||
const localTmpConf = path.join(os.tmpdir(), `shinobi_conf_${Date.now()}.json`);
|
||||
const remoteConf = `${shinobiPath.replace(/\/+$/, '')}/conf.json`;
|
||||
|
||||
// ── Helper — promisified spawn ------------------------------------------------
|
||||
function run(cmd, args, opts = {}) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const p = spawn(cmd, args, { stdio: 'inherit', ...opts });
|
||||
p.on('close', code => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
|
||||
});
|
||||
}
|
||||
|
||||
// ── 1. Dump the local database ───────────────────────────────────────────────
|
||||
async function dumpLocalDb() {
|
||||
console.log(`\n[1/5] Dumping local DB ⇒ ${dumpPath}`);
|
||||
const args = [
|
||||
`-h${localDbConf.host}`,
|
||||
`-P${localDbConf.port}`,
|
||||
`-u${localDbConf.user}`,
|
||||
`-p${localDbConf.password}`,
|
||||
'--single-transaction', '--routines', '--triggers',
|
||||
localDbConf.database,
|
||||
];
|
||||
await run('mysqldump', args, {
|
||||
stdio: ['ignore', await fs.open(dumpPath, 'w'), 'inherit'],
|
||||
});
|
||||
console.log(' ✓ Dump complete');
|
||||
}
|
||||
|
||||
// ── SSH helpers ───────────────────────────────────────────────────────────────
|
||||
function connectSSH() {
|
||||
return new Promise((resolve, reject) => {
|
||||
const conn = new Client();
|
||||
const opts = { host, port: 22, username: sshUsername };
|
||||
|
||||
if (sshPassword.startsWith('__RSA:')) {
|
||||
opts.privateKey = require('fs').readFileSync(sshPassword.slice(6));
|
||||
} else {
|
||||
opts.password = sshPassword;
|
||||
}
|
||||
|
||||
conn.on('ready', () => resolve(conn))
|
||||
.on('error', reject)
|
||||
.connect(opts);
|
||||
});
|
||||
}
|
||||
|
||||
function getSftp(conn) {
|
||||
return new Promise((res, rej) => conn.sftp((e, s) => (e ? rej(e) : res(s))));
|
||||
}
|
||||
|
||||
// ── 2. Copy dump to target ────────────────────────────────────────────────────
|
||||
async function uploadDump(conn, sftp) {
|
||||
console.log(`[2/5] Uploading dump ⇒ ${host}:${remoteDump}`);
|
||||
await new Promise((res, rej) =>
|
||||
sftp.fastPut(dumpPath, remoteDump, {}, err => (err ? rej(err) : res())),
|
||||
);
|
||||
console.log(' ✓ Upload complete');
|
||||
}
|
||||
|
||||
// ── 3. Copy conf.json to target ───────────────────────────────────────────────
|
||||
async function uploadConf(conn, sftp) {
|
||||
console.log(`[3/5] Uploading conf.json ⇒ ${host}:${remoteConf}`);
|
||||
// 3a. make sure remote Shinobi dir exists
|
||||
await new Promise((resolve, reject) => {
|
||||
conn.exec(`mkdir -p '${shinobiPath.replace(/'/g, `'\\''`)}'`, (err, stream) => {
|
||||
if (err) return reject(err);
|
||||
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mkdir exited ${code}`))));
|
||||
});
|
||||
});
|
||||
|
||||
// 3b. write local temp conf then push
|
||||
await fs.writeFile(localTmpConf, JSON.stringify(configRaw, null, 2));
|
||||
await new Promise((res, rej) =>
|
||||
sftp.fastPut(localTmpConf, remoteConf, {}, err => (err ? rej(err) : res())),
|
||||
);
|
||||
console.log(' ✓ conf.json uploaded');
|
||||
}
|
||||
|
||||
// ── 4. Ensure DB / privileges on target ──────────────────────────────────────
|
||||
async function ensureDatabase(conn) {
|
||||
console.log('[4/6] Ensuring database & privileges …');
|
||||
|
||||
const pwdClause = sqlPassword
|
||||
? `IDENTIFIED BY '${sqlPassword.replace(/'/g, `'\\''`)}'`
|
||||
: '';
|
||||
|
||||
const sql = `
|
||||
CREATE DATABASE IF NOT EXISTS \\\`${sqlDatabase}\\\`;
|
||||
GRANT ALL PRIVILEGES ON \\\`${sqlDatabase}\\\`.* TO '${sqlUsername}'@'127.0.0.1' ${pwdClause};
|
||||
FLUSH PRIVILEGES;
|
||||
`;
|
||||
|
||||
const cmd = [
|
||||
'mysql',
|
||||
`-u${sqlUsername}`,
|
||||
sqlPassword ? `-p'${sqlPassword.replace(/'/g, `'\\''`)}'` : '',
|
||||
`-P${sqlPort}`,
|
||||
`-e "${sql.replace(/\n/g, ' ')}"`,
|
||||
].join(' ');
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
conn.exec(cmd, (err, stream) => {
|
||||
if (err) return reject(err);
|
||||
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mysql exited ${code}`))))
|
||||
.stderr.pipe(process.stderr);
|
||||
stream.pipe(process.stdout);
|
||||
});
|
||||
});
|
||||
console.log(' ✓ Database ready');
|
||||
}
|
||||
|
||||
// ── 5. Restore DB on target ───────────────────────────────────────────────────
|
||||
async function importOnTarget(conn) {
|
||||
console.log('[5/6] Importing dump on target …');
|
||||
|
||||
const restoreCmd = [
|
||||
`mysql`,
|
||||
`-u${sqlUsername}`,
|
||||
sqlPassword ? `-p'${sqlPassword.replace(/'/g, `'\\''`)}'` : '',
|
||||
`-P${sqlPort}`,
|
||||
`${sqlDatabase} < ${remoteDump}`,
|
||||
].join(' ');
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
conn.exec(restoreCmd, (err, stream) => {
|
||||
if (err) return reject(err);
|
||||
stream.on('close', code => (code === 0 ? resolve() : reject(new Error(`mysql exited ${code}`))))
|
||||
.stderr.pipe(process.stderr);
|
||||
stream.pipe(process.stdout);
|
||||
});
|
||||
});
|
||||
console.log(' ✓ Import complete');
|
||||
}
|
||||
|
||||
// ── 6. Cleanup ────────────────────────────────────────────────────────────────
|
||||
async function cleanup(conn) {
|
||||
console.log('[6/6] Cleaning up …');
|
||||
await fs.unlink(dumpPath).catch(() => {});
|
||||
await fs.unlink(localTmpConf).catch(() => {});
|
||||
await new Promise(res => {
|
||||
conn.exec(`rm -f '${remoteDump}'`, () => res()); // ignore errors
|
||||
});
|
||||
conn.end();
|
||||
console.log(' ✓ All done!');
|
||||
}
|
||||
|
||||
// ── Main orchestrator ─────────────────────────────────────────────────────────
|
||||
(async () => {
|
||||
try {
|
||||
await dumpLocalDb();
|
||||
|
||||
const conn = await connectSSH();
|
||||
const sftp = await getSftp(conn);
|
||||
|
||||
await uploadDump(conn, sftp);
|
||||
await uploadConf(conn, sftp);
|
||||
await ensureDatabase(conn);
|
||||
await importOnTarget(conn);
|
||||
await cleanup(conn);
|
||||
} catch (err) {
|
||||
console.error('‼ Error:', err.message);
|
||||
process.exit(1);
|
||||
}
|
||||
})();
|
||||
|
|
@ -43,6 +43,7 @@ function makeConfig(streamUrl){
|
|||
// streamUrl = 'rtsp://1.1.1.1:554/'
|
||||
const copyOfBaseConfig = Object.assign({},monitorBase)
|
||||
const urlParts = getUrlParts(streamUrl)
|
||||
console.log(streamUrl)
|
||||
copyOfBaseConfig.mid = generateId()
|
||||
copyOfBaseConfig.name = urlParts.hostname
|
||||
copyOfBaseConfig.host = urlParts.hostname
|
||||
|
|
@ -58,7 +59,7 @@ function run(){
|
|||
const newMonitorsList = []
|
||||
const fileName = `${importFilePath}.json`
|
||||
importList.forEach((streamUrl) => {
|
||||
newMonitorsList.push(makeConfig(streamUrl))
|
||||
if(streamUrl)newMonitorsList.push(makeConfig(streamUrl))
|
||||
})
|
||||
console.log(`New JSON written to ${fileName}`)
|
||||
fs.writeFileSync(fileName,JSON.stringify(newMonitorsList));
|
||||
|
|
|
|||
|
|
@ -0,0 +1,86 @@
|
|||
const net = require('net');
|
||||
const os = require('os');
|
||||
|
||||
// Function to check if a port is open on a given IP
|
||||
async function checkPort(ip, port, timeout = 1000) {
|
||||
return new Promise((resolve) => {
|
||||
const socket = new net.Socket();
|
||||
let status = 'closed';
|
||||
|
||||
socket.setTimeout(timeout);
|
||||
socket.on('connect', () => {
|
||||
status = 'open';
|
||||
socket.destroy();
|
||||
resolve(status);
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
socket.destroy();
|
||||
resolve(status);
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
socket.destroy();
|
||||
resolve(status);
|
||||
});
|
||||
|
||||
socket.connect(port, ip);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to get all local network interfaces
|
||||
function getLocalIPRanges() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
const ranges = [];
|
||||
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name]) {
|
||||
// Skip internal and non-IPv4 addresses
|
||||
if (iface.internal || iface.family !== 'IPv4') continue;
|
||||
|
||||
// Get network range (simple /24 assumption)
|
||||
const ipParts = iface.address.split('.').slice(0, 3);
|
||||
ranges.push(ipParts.join('.') + '.0/24');
|
||||
}
|
||||
}
|
||||
|
||||
return ranges;
|
||||
}
|
||||
|
||||
// Function to generate all IPs in a range
|
||||
function* generateIPsFromRange(ipRange) {
|
||||
const [base, mask] = ipRange.split('/');
|
||||
const baseParts = base.split('.').map(Number);
|
||||
|
||||
// Simple handling for /24 ranges
|
||||
if (mask === '24') {
|
||||
for (let i = 1; i <= 254; i++) {
|
||||
yield `${baseParts[0]}.${baseParts[1]}.${baseParts[2]}.${i}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Main scanning function
|
||||
async function scanNetwork() {
|
||||
const ipRanges = getLocalIPRanges();
|
||||
const portsToCheck = [8080, 8005];
|
||||
const portNamesToCheck = {"8080": "CORE", "8005": "CENTRAL"};
|
||||
|
||||
console.log(`Starting scan of local network ranges: ${ipRanges.join(', ')}`);
|
||||
console.log(`Checking ports: ${portsToCheck.join(', ')}`);
|
||||
|
||||
for (const range of ipRanges) {
|
||||
console.log(`\nScanning range: ${range}`);
|
||||
|
||||
for (const ip of generateIPsFromRange(range)) {
|
||||
for (const port of portsToCheck) {
|
||||
checkPort(ip, port).then((status) => {
|
||||
if(status === 'open')console.log(`${ip}, Port: ${port} - ${status}`, portNamesToCheck[port]);
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run the scan
|
||||
scanNetwork().catch(console.error);
|
||||
|
|
@ -9,6 +9,7 @@ var liveGridOpenCount = 0
|
|||
var liveGridPauseScrollTimeout = null;
|
||||
var liveGridPlayingNow = {};
|
||||
var currentPtzPresetPosition = {};
|
||||
var detectionDrawDelays = {};
|
||||
//
|
||||
var onLiveStreamInitiateExtensions = []
|
||||
function onLiveStreamInitiate(callback){
|
||||
|
|
@ -169,6 +170,7 @@ function buildLiveGridBlock(monitor){
|
|||
var streamBlockInfo = definitions['Monitor Stream Window']
|
||||
var wasLiveGridLogStreamOpenBefore = isLiveGridLogStreamOpenBefore(monitorId)
|
||||
if(!loadedLiveGrids[monitor.mid])loadedLiveGrids[monitor.mid] = {}
|
||||
var detectionDrawDelay = detectionDrawDelays[monitorId];
|
||||
var quickLinkHtml = ''
|
||||
$.each(streamBlockInfo.quickLinks,function(n,button){
|
||||
if(button.eval && !eval(button.eval))return;
|
||||
|
|
@ -189,6 +191,11 @@ function buildLiveGridBlock(monitor){
|
|||
${streamBlockInfo.streamBlockHudHtml || ''}
|
||||
<div class="controls">
|
||||
${streamBlockInfo.streamBlockHudControlsHtml || ''}
|
||||
<div class="text-start">
|
||||
<div class="slider-container">
|
||||
<input type="range" class="slider detection-delay" step="0.1" min="0" max="10" title="${lang['Detection Draw Delay']} : ${detectionDrawDelay}" value="${detectionDrawDelay || 0}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${streamElement}
|
||||
|
|
@ -978,7 +985,8 @@ function pauseMonitorItem(monitorId){
|
|||
function resumeMonitorItem(monitorId){
|
||||
// needs to know about substream
|
||||
liveGridPlayingNow[monitorId] = true
|
||||
resetMonitorCanvas(monitorId,true,null)
|
||||
var monitor = loadedMonitors[monitorId];
|
||||
resetMonitorCanvas(monitorId,true,monitor.subStreamChannel)
|
||||
}
|
||||
function isScrolledIntoView(elem){
|
||||
var el = $(elem)
|
||||
|
|
@ -1066,12 +1074,29 @@ function showHideSubstreamActiveIcon(monitorId, show){
|
|||
|
||||
}
|
||||
}
|
||||
function setDetectionDrawDelay(monitorId, value){
|
||||
var theValue = parseFloat(value) || 0
|
||||
if(theValue == 0){
|
||||
delete(detectionDrawDelays[monitorId])
|
||||
}else{
|
||||
detectionDrawDelays[monitorId] = theValue
|
||||
}
|
||||
dashboardOptions('detectionDrawDelays', detectionDrawDelays)
|
||||
}
|
||||
$(document).ready(function(e){
|
||||
liveGrid
|
||||
.on('dblclick','.stream-block',function(){
|
||||
var monitorItem = $(this).parents('[data-mid]');
|
||||
fullScreenLiveGridStream(monitorItem)
|
||||
})
|
||||
.on('change','.detection-delay',function(){
|
||||
var el = $(this);
|
||||
var monitorId = el.parents('[data-mid]').attr('data-mid');
|
||||
var value = el.val()
|
||||
el.attr('title', `${lang['Detection Draw Delay']} : ${value}`)
|
||||
// console.log('setDetectionDrawDelay',monitorId, value)
|
||||
setDetectionDrawDelay(monitorId, value)
|
||||
})
|
||||
$('body')
|
||||
.resize(function(){
|
||||
resetAllLiveGridDimensionsInMemory()
|
||||
|
|
@ -1352,7 +1377,7 @@ $(document).ready(function(e){
|
|||
onToggleSideBarMenuHide(function (isHidden){
|
||||
setTimeout(updateAllLiveGridElementsHeightWidth,2000)
|
||||
})
|
||||
onWebSocketEvent(function (d){
|
||||
onWebSocketEvent(async function (d){
|
||||
switch(d.f){
|
||||
case'control_ptz_preset_changed':
|
||||
currentPtzPresetPosition[d.mid] = d.profileToken;
|
||||
|
|
@ -1446,7 +1471,7 @@ $(document).ready(function(e){
|
|||
monitorElement.removeClass('doObjectDetection')
|
||||
}
|
||||
if(matrices && matrices.length > 0){
|
||||
drawMatrices(d,{
|
||||
await drawMatrices(d,{
|
||||
theContainer: liveGridElement.eventObjects,
|
||||
height: liveGridElement.height,
|
||||
width: liveGridElement.width,
|
||||
|
|
@ -1556,4 +1581,5 @@ $(document).ready(function(e){
|
|||
window.monitorsWatchOnLiveGrid = monitorsWatchOnLiveGrid;
|
||||
window.closeAllLiveGridPlayers = closeAllLiveGridPlayers;
|
||||
window.closeLiveGridPlayers = closeLiveGridPlayers;
|
||||
detectionDrawDelays = dashboardOptions().detectionDrawDelays || {}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -454,6 +454,7 @@ window.getMonitorEditFormFields = function(){
|
|||
monitorConfig.details.input_map_choices = getInputMapDesignations()
|
||||
monitorConfig.details.stream_channels = getStreamChannelConnectionInfo()
|
||||
monitorConfig.details.triggerMonitorsPtzTargets = getSelectedEventBasedPtzPresets()
|
||||
monitorConfig.details.detectorLineCounterSettings = getLineCounterState()
|
||||
|
||||
// if(monitorConfig.protocol=='rtsp'){monitorConfig.ext='mp4',monitorConfig.type='rtsp'}
|
||||
if(errorsFound.length > 0){
|
||||
|
|
@ -724,7 +725,14 @@ async function importIntoMonitorEditor(options){
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
//
|
||||
if(monitorDetails.detectorLineCounter == '1'){
|
||||
setTimeout(function(){
|
||||
drawLineCounterCanvas(Object.assign({}, monitorConfig, { details: monitorDetails }))
|
||||
},500)
|
||||
}else{
|
||||
clearLineCounterCanvas()
|
||||
}
|
||||
//
|
||||
await getPluginsList(monitorConfig)
|
||||
//
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
$(document).ready(function(){
|
||||
const lineCounterCanvas = $('#monitorSettings-lineCounter-canvas-container')
|
||||
const monitorEditorWindow = $('#tab-monitorSettings')
|
||||
const lineCounterSpacingField = $('#detector-line-counter-spacing')
|
||||
const lineCounterUpName = $('#detector-line-counter-name-up')
|
||||
const lineCounterDownName = $('#detector-line-counter-name-down')
|
||||
const lineCounterResetDailyField = $('#detector-line-counter-reset-daily')
|
||||
const lineCounterToggle = monitorEditorWindow.find('[detail=detectorLineCounter]')
|
||||
const scaleXObject = monitorEditorWindow.find('[detail=detector_scale_x_object]')
|
||||
const scaleYObject = monitorEditorWindow.find('[detail=detector_scale_y_object]')
|
||||
let currentLineCounterCanvas = null;
|
||||
let lastEmbedUrl = '';
|
||||
function loadLineCounterForMonitorSettings(monitor){
|
||||
drawLineCounterCanvas(monitor)
|
||||
}
|
||||
function clearLineCounterCanvas(){
|
||||
if(currentLineCounterCanvas){
|
||||
currentLineCounterCanvas.destroy()
|
||||
currentLineCounterCanvas = null;
|
||||
lineCounterCanvas.empty()
|
||||
lastEmbedUrl = null;
|
||||
}
|
||||
}
|
||||
function getLineCounterSettings(monitor){
|
||||
const lineCounterSettings = monitor.details.detectorLineCounterSettings || {};
|
||||
const imageWidth = parseInt(monitor.details.detector_scale_x_object) || 1280
|
||||
const imageHeight = parseInt(monitor.details.detector_scale_y_object) || 720
|
||||
const {
|
||||
lineSpacing = 40,
|
||||
refreshRate = 10,
|
||||
lines,
|
||||
} = lineCounterSettings;
|
||||
return {
|
||||
lineSpacing,
|
||||
refreshRate,
|
||||
lines,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
}
|
||||
}
|
||||
function drawLineCounterCanvas(monitor){
|
||||
clearLineCounterCanvas()
|
||||
const {
|
||||
lineSpacing,
|
||||
refreshRate,
|
||||
lines,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
} = getLineCounterSettings(monitor)
|
||||
const containerWidth = lineCounterCanvas.width()
|
||||
const ratio = containerWidth / imageWidth / 2
|
||||
const newWidth = imageWidth * ratio
|
||||
const newHeight = imageHeight * ratio
|
||||
lineCounterCanvas.html(`<div style="position:relative;display:inline-block;height:${newHeight}px;width:${newWidth}px;text-align: initial;">
|
||||
<iframe style="position:absolute" width="${newWidth}" height="${newHeight}"></iframe>
|
||||
<canvas id="monitorSettings-lineCounter-canvas" width="${newWidth}" height="${newHeight}" style="position:absolute"></canvas>
|
||||
</div>`)
|
||||
currentLineCounterCanvas = new ParallelLineDrawer('monitorSettings-lineCounter-canvas', {
|
||||
lineSpacing,
|
||||
refreshRate,
|
||||
originalWidth: imageWidth,
|
||||
originalHeight: imageHeight,
|
||||
});
|
||||
currentLineCounterCanvas.scaleCanvas(ratio);
|
||||
if(monitor){
|
||||
currentLineCounterCanvas.loadState({
|
||||
lines
|
||||
});
|
||||
}
|
||||
loadLineCounterTagsField(monitor)
|
||||
loadLineCounterLiveStream(monitor)
|
||||
lineCounterSpacingField.val(lineSpacing)
|
||||
}
|
||||
function getLineCounterState(){
|
||||
const {
|
||||
lineSpacing,
|
||||
refreshRate,
|
||||
lines,
|
||||
} = currentLineCounterCanvas ? currentLineCounterCanvas.getState() : {};
|
||||
const upName = lineCounterUpName.val().trim()
|
||||
const downName = lineCounterDownName.val().trim()
|
||||
const resetDaily = lineCounterResetDailyField.val() === '1'
|
||||
return {
|
||||
lineSpacing,
|
||||
refreshRate,
|
||||
lines,
|
||||
upName,
|
||||
downName,
|
||||
resetDaily,
|
||||
}
|
||||
}
|
||||
function resetLineCounterState(){
|
||||
if(currentLineCounterCanvas){
|
||||
currentLineCounterCanvas.reset();
|
||||
}
|
||||
}
|
||||
function loadLineCounterTagsField(monitorConfig){
|
||||
const monitorEditorWindow = $('#tab-monitorSettings')
|
||||
const lineCounterTagsInput = monitorEditorWindow.find('[detail=detectorLineCounterTags]')
|
||||
const monitorTags = monitorConfig && monitorConfig.details.detectorLineCounterTags ? monitorConfig.details.detectorLineCounterTags.split(',') : []
|
||||
lineCounterTagsInput.tagsinput('removeAll');
|
||||
monitorTags.forEach((tag) => {
|
||||
lineCounterTagsInput.tagsinput('add',tag);
|
||||
});
|
||||
}
|
||||
function loadLineCounterLiveStream(monitor){
|
||||
const monitorId = monitor.mid
|
||||
const liveElement = lineCounterCanvas.find('iframe')
|
||||
const imageWidth = parseInt(monitor.details.detector_scale_x_object) || 1280
|
||||
const imageHeight = parseInt(monitor.details.detector_scale_y_object) || 720
|
||||
if(monitor.mode === 'stop'){
|
||||
var apiUrl = placeholder.getData(placeholder.plcimg({
|
||||
bgcolor: '#000',
|
||||
text: lang[`Cannot watch a monitor that isn't running.`],
|
||||
size: imageWidth+'x'+imageHeight
|
||||
}))
|
||||
liveElement.attr('src',apiUrl)
|
||||
}else{
|
||||
var apiUrl = `${getApiPrefix('embed')}/${monitorId}/fullscreen|jquery|gui|relative?host=${location.pathname}`
|
||||
liveElement.attr('src',apiUrl)
|
||||
lastEmbedUrl = apiUrl
|
||||
}
|
||||
}
|
||||
function setIframeSource(embedUrl){
|
||||
lineCounterCanvas.find('iframe').attr('src', embedUrl || 'about:blank')
|
||||
}
|
||||
function isLineCounterEnabled(){
|
||||
lineCounterToggle.val() === '1'
|
||||
}
|
||||
lineCounterToggle.change(function(e){
|
||||
if($(this).val() === '1'){
|
||||
drawLineCounterCanvas(monitorEditorSelectedMonitor)
|
||||
}else{
|
||||
clearLineCounterCanvas()
|
||||
}
|
||||
});
|
||||
scaleXObject.change(function(e){
|
||||
if(lineCounterToggle.val() === '1'){
|
||||
drawLineCounterCanvas(monitorEditorSelectedMonitor)
|
||||
}
|
||||
});
|
||||
scaleYObject.change(function(e){
|
||||
if(lineCounterToggle.val() === '1'){
|
||||
drawLineCounterCanvas(monitorEditorSelectedMonitor)
|
||||
}
|
||||
});
|
||||
lineCounterSpacingField.change(function(e){
|
||||
var value = parseInt($(this).val()) || 40;
|
||||
currentLineCounterCanvas.setLineSpacing(value)
|
||||
});
|
||||
addOnTabAway('monitorSettings', function(){
|
||||
setIframeSource()
|
||||
})
|
||||
addOnTabOpen('monitorSettings', function(){
|
||||
|
||||
})
|
||||
addOnTabReopen('monitorSettings', function(){
|
||||
if(lastEmbedUrl)setIframeSource(lastEmbedUrl)
|
||||
})
|
||||
window.drawLineCounterCanvas = drawLineCounterCanvas
|
||||
window.clearLineCounterCanvas = clearLineCounterCanvas
|
||||
window.getLineCounterState = getLineCounterState
|
||||
window.isLineCounterEnabled = isLineCounterEnabled
|
||||
})
|
||||
|
|
@ -722,7 +722,7 @@ function buildPosePoints(bodyParts, x, y){
|
|||
}
|
||||
return theArray;
|
||||
}
|
||||
function drawMatrices(event, options, autoRemoveTimeout, drawTrails){
|
||||
async function drawMatrices(event, options, autoRemoveTimeout, drawTrails){
|
||||
var theContainer = options.theContainer
|
||||
var height = options.height
|
||||
var width = options.width
|
||||
|
|
@ -786,6 +786,15 @@ function drawMatrices(event, options, autoRemoveTimeout, drawTrails){
|
|||
}
|
||||
$.each(event.details.matrices, processMatrix);
|
||||
$.each(moreMatrices, processMatrix);
|
||||
var detectionDrawDelay = detectionDrawDelays[monitorId];
|
||||
if(detectionDrawDelay){
|
||||
await (new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
resolve()
|
||||
}, detectionDrawDelay * 1000)
|
||||
}))
|
||||
// console.log(`Delayed Draw by ${detectionDrawDelay} seconds`)
|
||||
}
|
||||
var addedEls = theContainer.append(html)
|
||||
if(autoRemoveTimeout){
|
||||
addedEls = addedEls.find('.fresh-detected-object').removeClass('fresh-detected-object')
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ $(document).ready(function(){
|
|||
notifyColor = 'success'
|
||||
if(data.user){
|
||||
var account = data.user
|
||||
account.details = safeJsonParse(account.details)
|
||||
loadedSubAccounts[account.uid] = account;
|
||||
drawSubAccountRow(account)
|
||||
theWindowForm.find('[name="uid"]').val(account.uid)
|
||||
|
|
@ -105,6 +106,7 @@ $(document).ready(function(){
|
|||
$.each(form,function(n,v){
|
||||
account[n] = v
|
||||
});
|
||||
account.details = safeJsonParse(account.details);
|
||||
accountTable.find(`[uid="${uid}"] .mail`).text(form.mail)
|
||||
new PNotify({
|
||||
title : lang['Account Edited'],
|
||||
|
|
@ -152,7 +154,12 @@ $(document).ready(function(){
|
|||
label: lang['Can Delete Videos and Events'],
|
||||
},
|
||||
];
|
||||
var drawSelectableForPermissionForm = function(account){
|
||||
var drawSelectableForPermissionForm = async function(account){
|
||||
var details = account ? safeJsonParse(account.details) : {}
|
||||
if(account){
|
||||
await loadPermissions()
|
||||
details = Object.assign({}, details, loadedPermissions[details.permissionSet] || {})
|
||||
}
|
||||
var html = `
|
||||
<thead class="text-center">
|
||||
<tr>
|
||||
|
|
@ -167,7 +174,7 @@ $(document).ready(function(){
|
|||
html += `<td class="text-start">${monitor.name} (${monitor.mid})</td>`
|
||||
html += `<td>${(monitor.tags || '').split(',').map(item => `<span class="label label-primary">${item}</span>`)}</td>`
|
||||
$.each(permissionTypeNames,function(n,permissionType){
|
||||
const isChecked = account && (account.details[permissionType.name] || []).indexOf(monitor.mid) > -1;
|
||||
const isChecked = account && (details[permissionType.name] || []).indexOf(monitor.mid) > -1;
|
||||
html += `<td><input class="form-check-input" type="checkbox" data-monitor="${monitor.mid}" value="${permissionType.name}" ${isChecked ? 'checked' : ''}></td>`
|
||||
})
|
||||
html += `</tr>`
|
||||
|
|
@ -191,8 +198,9 @@ $(document).ready(function(){
|
|||
}
|
||||
var setPermissionSelectionsToFields = async function(uid){
|
||||
var account = loadedSubAccounts[uid]
|
||||
var details = account.details
|
||||
var details = safeJsonParse(account.details)
|
||||
await loadPermissions()
|
||||
details = Object.assign({}, details, loadedPermissions[details.permissionSet] || {})
|
||||
// load values to Account Information : email, password, etc.
|
||||
$.each(account,function(n,v){
|
||||
theWindowForm.find('[name="'+n+'"]').val(v)
|
||||
|
|
@ -207,7 +215,7 @@ $(document).ready(function(){
|
|||
}
|
||||
var openSubAccountEditor = async function(uid){
|
||||
var account = loadedSubAccounts[uid]
|
||||
drawSelectableForPermissionForm(account)
|
||||
await drawSelectableForPermissionForm(account)
|
||||
await setPermissionSelectionsToFields(uid)
|
||||
theWindowForm.find('[name="pass"],[name="password_again"]').val('')
|
||||
permissionsSection.show()
|
||||
|
|
@ -273,7 +281,7 @@ $(document).ready(function(){
|
|||
var defaultValue = el.attr('data-default')
|
||||
el.val(defaultValue)
|
||||
})
|
||||
drawSelectableForPermissionForm()
|
||||
await drawSelectableForPermissionForm()
|
||||
setSubmitButtonState(lang['Add New'],'plus')
|
||||
theWindowForm.find('input').val('')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -712,7 +712,7 @@ async function unarchiveVideos(videos){
|
|||
}
|
||||
function buildDefaultVideoMenuItems(file,options){
|
||||
var isLocalVideo = !file.videoSet || file.videoSet === 'videos'
|
||||
var href = getApiHost() + file.href + `${!isLocalVideo ? `?type=${file.type}` : ''}`
|
||||
var href = file.href + `${!isLocalVideo ? `?type=${file.type}` : ''}`
|
||||
options = options ? options : {play: true}
|
||||
return `
|
||||
<li><a class="dropdown-item" href="${href}" download>${lang.Download}</a></li>
|
||||
|
|
|
|||
|
|
@ -79,6 +79,7 @@ $(document).ready(function(e){
|
|||
pageNumber = pageNumber || 1;
|
||||
pageSize = pageSize || 10;
|
||||
|
||||
var use24HourTime = dashboardOptions().use24HourTime == '1';
|
||||
var dateRange = getSelectedTime(dateSelector);
|
||||
var searchQuery = objectTagSearchField.val() || null;
|
||||
var andOnly = objectTagSearchFieldAndOnly.val() || '0';
|
||||
|
|
@ -179,8 +180,8 @@ $(document).ready(function(e){
|
|||
mid: file.mid,
|
||||
time: `
|
||||
<div>${timeAgo(file.time)}</div>
|
||||
<div><small><b>${lang.Start} :</b> ${formattedTime(file.time, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>
|
||||
<div><small><b>${lang.End} :</b> ${formattedTime(file.end, 'DD-MM-YYYY hh:mm:ss AA')}</small></div>`,
|
||||
<div><small><b>${lang.Start} :</b> ${formattedTime(file.time, !use24HourTime)}</small></div>
|
||||
<div><small><b>${lang.End} :</b> ${formattedTime(file.end, !use24HourTime)}</small></div>`,
|
||||
objects: `<div style="word-break: break-word;max-width:125px;">${file.objects}</div>`,
|
||||
tags: `
|
||||
${file.ext ? `<span class="badge badge-${file.ext ==='webm' ? `primary` : 'danger'}">${file.ext}</span>` : ''}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,236 @@
|
|||
class ParallelLineDrawer {
|
||||
constructor(canvasId, options = {}) {
|
||||
// Configuration with defaults
|
||||
this.lineSpacing = options.lineSpacing || 40;
|
||||
this.refreshRate = options.refreshRate || 10;
|
||||
this.onLinesDrawn = options.onLinesDrawn || null;
|
||||
|
||||
// Ratio handling
|
||||
this.originalWidth = options.originalWidth || null;
|
||||
this.originalHeight = options.originalHeight || null;
|
||||
this.currentRatio = 1; // Default ratio (no scaling)
|
||||
|
||||
// Canvas setup
|
||||
this.canvas = document.getElementById(canvasId);
|
||||
this.ctx = this.canvas.getContext("2d");
|
||||
|
||||
// Set initial canvas size if original dimensions provided
|
||||
if (this.originalWidth && this.originalHeight) {
|
||||
this.resizeCanvas(this.originalWidth, this.originalHeight);
|
||||
}
|
||||
|
||||
// State variables
|
||||
this.cursorX = 0;
|
||||
this.cursorY = 0;
|
||||
this.firstClick = [0, 0];
|
||||
this.intervalLoop = null;
|
||||
this.isActive = false;
|
||||
this.currentLines = null;
|
||||
|
||||
// Bind event handlers
|
||||
this.handleMouseMove = this.handleMouseMove.bind(this);
|
||||
this.startDragLine = this.startDragLine.bind(this);
|
||||
this.stopDragLine = this.stopDragLine.bind(this);
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
resizeCanvas(width, height) {
|
||||
this.originalWidth = width;
|
||||
this.originalHeight = height;
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
this.currentRatio = 1;
|
||||
this.redrawLines();
|
||||
}
|
||||
|
||||
scaleCanvas(scaleFactor) {
|
||||
if (!this.originalWidth || !this.originalHeight) return;
|
||||
|
||||
this.currentRatio = scaleFactor;
|
||||
this.canvas.width = this.originalWidth * scaleFactor;
|
||||
this.canvas.height = this.originalHeight * scaleFactor;
|
||||
this.redrawLines();
|
||||
}
|
||||
|
||||
initialize() {
|
||||
document.addEventListener('mousemove', this.handleMouseMove);
|
||||
this.canvas.addEventListener('mousedown', this.startDragLine);
|
||||
this.canvas.addEventListener('mouseup', this.stopDragLine);
|
||||
this.canvas.addEventListener('mouseleave', this.stopDragLine);
|
||||
this.isActive = true;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.stopDragLine();
|
||||
document.removeEventListener('mousemove', this.handleMouseMove);
|
||||
this.canvas.removeEventListener('mousedown', this.startDragLine);
|
||||
this.canvas.removeEventListener('mouseup', this.stopDragLine);
|
||||
this.canvas.removeEventListener('mouseleave', this.stopDragLine);
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
// Load previous state (lines, spacing, refresh rate)
|
||||
loadState(state) {
|
||||
if (!state) return;
|
||||
|
||||
// Restore configuration
|
||||
if (state.lineSpacing) this.lineSpacing = state.lineSpacing;
|
||||
if (state.refreshRate) this.refreshRate = state.refreshRate;
|
||||
if (state.originalWidth && state.originalHeight) {
|
||||
this.originalWidth = state.originalWidth;
|
||||
this.originalHeight = state.originalHeight;
|
||||
this.scaleCanvas(state.currentRatio || 1);
|
||||
}
|
||||
|
||||
// Restore lines with ratio adjustment
|
||||
if (state.lines && Array.isArray(state.lines) && state.lines.length === 2) {
|
||||
this.currentLines = state.lines.map(line => ({
|
||||
start: {
|
||||
x: line.start.x * this.currentRatio,
|
||||
y: line.start.y * this.currentRatio
|
||||
},
|
||||
end: {
|
||||
x: line.end.x * this.currentRatio,
|
||||
y: line.end.y * this.currentRatio
|
||||
}
|
||||
}));
|
||||
this.redrawLines();
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw existing lines
|
||||
redrawLines() {
|
||||
if (!this.currentLines) return;
|
||||
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.drawLine(this.currentLines[0], '#FF0000'); // Red
|
||||
this.drawLine(this.currentLines[1], '#0000FF'); // Blue
|
||||
}
|
||||
|
||||
handleMouseMove(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.cursorX = e.clientX - rect.left;
|
||||
this.cursorY = e.clientY - rect.top;
|
||||
}
|
||||
|
||||
startDragLine(e) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
this.firstClick = [
|
||||
e.clientX - rect.left,
|
||||
e.clientY - rect.top
|
||||
];
|
||||
|
||||
this.intervalLoop = setInterval(() => {
|
||||
// Check if mouse is at edge of screen
|
||||
if (this.cursorX <= 0 || this.cursorY <= 0 ||
|
||||
this.cursorX >= this.canvas.width || this.cursorY >= this.canvas.height) {
|
||||
this.stopDragLine();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
|
||||
// Calculate offsets for parallel lines
|
||||
const angle = Math.atan2(
|
||||
this.cursorY - this.firstClick[1],
|
||||
this.cursorX - this.firstClick[0]
|
||||
);
|
||||
const perpendicularAngle = angle + Math.PI/2;
|
||||
const offsetX = Math.cos(perpendicularAngle) * (this.lineSpacing/2);
|
||||
const offsetY = Math.sin(perpendicularAngle) * (this.lineSpacing/2);
|
||||
|
||||
// Calculate line coordinates
|
||||
this.currentLines = [
|
||||
{
|
||||
start: {
|
||||
x: this.firstClick[0] - offsetX,
|
||||
y: this.firstClick[1] - offsetY
|
||||
},
|
||||
end: {
|
||||
x: this.cursorX - offsetX,
|
||||
y: this.cursorY - offsetY
|
||||
}
|
||||
},
|
||||
{
|
||||
start: {
|
||||
x: this.firstClick[0] + offsetX,
|
||||
y: this.firstClick[1] + offsetY
|
||||
},
|
||||
end: {
|
||||
x: this.cursorX + offsetX,
|
||||
y: this.cursorY + offsetY
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Draw lines with colors
|
||||
this.drawLine(this.currentLines[0], '#FF0000'); // Red
|
||||
this.drawLine(this.currentLines[1], '#0000FF'); // Blue
|
||||
|
||||
}, this.refreshRate);
|
||||
}
|
||||
|
||||
drawLine(coords, color) {
|
||||
this.ctx.beginPath();
|
||||
this.ctx.moveTo(coords.start.x, coords.start.y);
|
||||
this.ctx.lineTo(coords.end.x, coords.end.y);
|
||||
this.ctx.strokeStyle = color;
|
||||
this.ctx.lineWidth = 2;
|
||||
this.ctx.stroke();
|
||||
}
|
||||
|
||||
stopDragLine() {
|
||||
if (this.intervalLoop) {
|
||||
clearInterval(this.intervalLoop);
|
||||
this.intervalLoop = null;
|
||||
|
||||
if (this.onLinesDrawn && this.currentLines) {
|
||||
this.onLinesDrawn(this.getCurrentLines());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns current state (lines + configuration)
|
||||
getState() {
|
||||
return {
|
||||
lines: this.getCurrentLines(),
|
||||
lineSpacing: this.lineSpacing,
|
||||
refreshRate: this.refreshRate,
|
||||
originalWidth: this.originalWidth,
|
||||
originalHeight: this.originalHeight,
|
||||
currentRatio: this.currentRatio
|
||||
};
|
||||
}
|
||||
|
||||
getCurrentLines() {
|
||||
if (!this.currentLines) return null;
|
||||
|
||||
// Return lines in original coordinates (divide by current ratio)
|
||||
const newLines = this.currentLines.map(line => ({
|
||||
start: {
|
||||
x: line.start.x / this.currentRatio,
|
||||
y: line.start.y / this.currentRatio
|
||||
},
|
||||
end: {
|
||||
x: line.end.x / this.currentRatio,
|
||||
y: line.end.y / this.currentRatio
|
||||
}
|
||||
}));
|
||||
console.log(newLines,this.currentLines[0].start.x,this.currentRatio)
|
||||
return newLines
|
||||
}
|
||||
|
||||
// Configuration setters
|
||||
setLineSpacing(spacing) {
|
||||
this.lineSpacing = spacing;
|
||||
}
|
||||
|
||||
setRefreshRate(rate) {
|
||||
this.refreshRate = rate;
|
||||
}
|
||||
|
||||
setCallback(callback) {
|
||||
this.onLinesDrawn = callback;
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@
|
|||
<script src="<%-window.libURL%>assets/vendor/bootstrap5/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/bootstrap-table/bootstrap-table.min.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/bootstrap5-tags/bootstrap-tagsinput.min.js"></script>
|
||||
<script src="<%-window.libURL%>assets/vendor/lineDrawForCanvas.js"></script>
|
||||
<!-- <script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js" integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE" crossorigin="anonymous"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js" integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha" crossorigin="anonymous"></script> -->
|
||||
<script src="<%-window.libURL%>assets/vendor/js/daterangepicker.js"></script>
|
||||
|
|
@ -34,6 +35,7 @@
|
|||
<script src="<%-window.libURL%>assets/js/bs5.subAccountManager.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.apiKeys.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.monitorSettings.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.monitorSettings.lineCounter.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.monitorsUtils.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.monitorStates.js"></script>
|
||||
<script src="<%-window.libURL%>assets/js/bs5.schedules.js"></script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue