node-red-contrib-hik-media-buffer 1.1.42 → 1.1.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -11,19 +11,24 @@ This node only detects **_"FieldDetection"_** and **_"LineDetection"_** alarms b
11
11
 
12
12
  To configure the node you need to enter the **_IP, user and password of the NVR_**, you can also choose the **_protocol_** and **_port_** to use.</br>
13
13
  You must also enter, by pressing the **_"add"_** button, the **_channel and the correspective IP of the camera_**, finally you must enter the **_password of the cameras_**.</br>
14
+ The node name is the name of the customer.</br>
14
15
 
15
16
  This below is an example of msg output:</br>
16
17
 
17
18
  ```javascript
18
19
  msg = {
19
- payload: object,
20
- ip: "192.168.1.100", // IP of the camera
21
- channel: "2", // Channel of the camera
22
- event: "LineDetection", // Type of event deteced
23
- videoPath: "", // Path of the video
24
- imageBuffer: buffer[12360], // Buffer of the image
25
- status: "online", // Status of the camera
26
- _msgid: "45fd74589048966d",
20
+ payload: object
21
+ tipo_messaggio: "evento" // Type of alarm deteced (event or status)
22
+ nome_cliente: "test" // Customer name (name of the node)
23
+ nome_telecamera: "Ufficio" // Camera name on hiklvision
24
+ ip_telecamera: "192.168.62.9" // IP of the camera
25
+ tipo_evento: "LineDetection" // Type of event deteced
26
+ timestamp_epoch: 1780645403 // Timestamp of the event
27
+ stato_telecamera: "ONLINE" // Status of the camera
28
+ channel: "2" // Channel of the camera
29
+ foto_base64: "/9j/2wCEAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSop..." // Buffer of the image base64
30
+ video_base64: "AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAABtdtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPo..." // Buffer of the image base64
31
+ _msgid: "942a8c0c56860f42"
27
32
  };
28
33
  ```
29
34
  ## HIK SNAPSHOT NODE
@@ -12,21 +12,24 @@ module.exports = function(RED) {
12
12
  const node = this;
13
13
 
14
14
  node.name = config.name || "TEST";
15
- node.host = config.host;
15
+ node.host = config.host; // IP dell'NVR centrale
16
16
  node.port = config.port || "80";
17
17
  node.protocol = config.protocol || "http";
18
18
  node.user = config.user;
19
19
  node.pass = config.pass;
20
- node.camPass = config.camPass || config.pass;
21
- node.cameras = config.cameras || [];
22
20
 
23
21
  let streamRequest = null;
24
22
  let isClosing = false;
25
23
  let lastTriggerTime = {};
26
24
  let nvrOnline = true;
27
- let statoCamera = {};
25
+
26
+ // Strutture dinamiche per monitorare canali e stati senza liste fisse
27
+ let statoCanale = {};
28
+ let ultimoAllarmeTempo = {};
28
29
 
29
30
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
31
+
32
+ // La tua EventList originale per l'alertStream
30
33
  const EventList = ["FieldDetection", "LineDetection"];
31
34
 
32
35
  // CARTELLA TEMPORANEA
@@ -37,66 +40,81 @@ module.exports = function(RED) {
37
40
 
38
41
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
39
42
 
40
- // --- PRENDE IL NOME DELLA TELECAMERA ---
41
- async function getCameraName(cam) {
42
- const camAuth = new AxiosDigestAuth({
43
- username: node.user,
44
- password: node.camPass
45
- });
46
-
43
+ // --- PRENDE IL NOME DEL CANALE DIRETTAMENTE DALL'NVR ---
44
+ async function getCameraName(channelID) {
45
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
47
46
  try {
48
- const res = await camAuth.request({
47
+ const res = await nvrAuth.request({
49
48
  method: 'GET',
50
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
49
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
51
50
  timeout: 5000,
52
51
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
53
52
  });
54
53
 
55
54
  const data = res.data.toString();
56
55
  const match = data.match(/<name>([^<]+)<\/name>/i);
57
-
58
- if (match && match[1]) {
59
- return match[1].trim();
60
- } else {
61
- return `Canale_${cam.channel}`;
62
- }
63
-
56
+ return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
64
57
  } catch (e) {
65
- return `Camera_${cam.channel}`;
58
+ return `Canale_${channelID}`;
66
59
  }
67
60
  }
68
61
 
69
- // --- CONTROLLO STATUS CAMERE ---
62
+ // --- CONTROLLO STATUS DINAMICO DEI CANALI DIGITALI DALL'NVR ---
70
63
  async function checkCameras() {
71
64
  if (isClosing) return;
72
- for (let cam of node.cameras) {
73
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
74
- try {
75
- await camAuth.request({
76
- method: 'GET',
77
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
78
- timeout: 5000,
79
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
80
- });
81
- if (statoCamera[cam.ip] === false) {
82
- const nomeOnline = await getCameraName(cam);
83
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
84
- statoCamera[cam.ip] = true;
85
- } else if (statoCamera[cam.ip] === undefined) {
86
- statoCamera[cam.ip] = true;
87
- }
88
- } catch (e) {
89
- if (statoCamera[cam.ip] !== false) {
90
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${cam.channel}`, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
91
- statoCamera[cam.ip] = false;
65
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
66
+
67
+ try {
68
+ // Interroghiamo l'NVR sullo stato globale dei suoi canali proxy
69
+ const res = await nvrAuth.request({
70
+ method: 'GET',
71
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
72
+ timeout: 5000,
73
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
74
+ });
75
+
76
+ nvrOnline = true;
77
+ const xmlData = res.data.toString();
78
+
79
+ // Regex globale per estrarre TUTTI i blocchi <InputProxyChannelStatus> presenti nell'XML dell'NVR
80
+ const bloccoRegex = /<InputProxyChannelStatus[\s\\S]*?>([\s\S]*?)<\/InputProxyChannelStatus>/gi;
81
+ let matchBlocco;
82
+
83
+ while ((matchBlocco = bloccoRegex.exec(xmlData)) !== null) {
84
+ const bloccoContenuto = matchBlocco[1];
85
+
86
+ // Estraiamo l'ID del canale e il suo stato dal blocco attuale
87
+ const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
88
+ const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i); // Corretta regex di chiusura tag
89
+
90
+ if (idMatch && statusMatch) {
91
+ const ch = idMatch[1];
92
+ const statusStr = statusMatch[1].toLowerCase();
93
+
94
+ // Determiniamo se per l'NVR la telecamera su questo canale è attiva
95
+ const isOnline = statusStr.includes("online") || statusStr.includes("recording");
96
+
97
+ if (isOnline && statoCanale[ch] === false) {
98
+ const nomeOnline = await getCameraName(ch);
99
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_nvr: node.host, channel: ch.toString(), msg: "Camera ripristinata su NVR" } });
100
+ statoCanale[ch] = true;
101
+ } else if (!isOnline && statoCanale[ch] !== false && statoCanale[ch] !== undefined) {
102
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Canale_${ch}`, ip_nvr: node.host, channel: ch.toString(), msg: "Camera scollegata o offline su NVR" } });
103
+ statoCanale[ch] = false;
104
+ } else if (statoCanale[ch] === undefined) {
105
+ // Primo avvio: memorizziamo lo stato iniziale
106
+ statoCanale[ch] = isOnline;
107
+ }
92
108
  }
93
109
  }
110
+ } catch (e) {
111
+ nvrOnline = false; // Se fallisce la GET, è l'NVR ad essere irraggiungibile
94
112
  }
95
113
  updateNodeStatus();
96
114
  }
97
115
 
98
116
  function updateNodeStatus() {
99
- const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
117
+ const offlineCams = Object.values(statoCanale).filter(v => v === false).length;
100
118
  if (!nvrOnline) {
101
119
  node.status({fill:"red", shape:"ring", text:"NVR Offline"});
102
120
  } else if (offlineCams > 0) {
@@ -108,37 +126,31 @@ module.exports = function(RED) {
108
126
 
109
127
  const heartbeatInterval = setInterval(checkCameras, 30000);
110
128
 
111
- // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
129
+ // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DALL'NVR ---
112
130
  async function downloadMedia(evento, channelID) {
113
- const camera = node.cameras.find(c => c.channel == channelID);
114
- if (!camera) return;
115
-
116
- const nomeCamera = await getCameraName(camera);
117
-
118
131
  const nowTime = Date.now();
119
- if(!lastTriggerTime[camera.ip]){
120
- lastTriggerTime[camera.ip] = 0;
121
- }
122
- if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
123
- lastTriggerTime[camera.ip] = nowTime;
132
+ if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
133
+ if (nowTime - lastTriggerTime[channelID] < 5000) return;
134
+ lastTriggerTime[channelID] = nowTime;
124
135
 
125
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
136
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
126
137
  const referenceTime = new Date();
127
138
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
128
- const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
129
- const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
139
+ const startTime = toHikDate(new Date(referenceTime.getTime() - (15 * 1000))); // Allargato a 15s per sicurezza
140
+ const endTime = toHikDate(new Date(referenceTime.getTime() + (15 * 1000)));
130
141
 
131
142
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
132
- await new Promise(resolve => setTimeout(resolve, 6000));
143
+ const nomeCamera = await getCameraName(channelID);
133
144
 
134
- const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
145
+ // Ritardo tecnico per dare tempo all'NVR di chiudere e indicizzare il file su HDD
146
+ await new Promise(resolve => setTimeout(resolve, 6000));
147
+ const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
135
148
 
136
- // struttura del payload per Python
137
149
  let output = {
138
150
  tipo_messaggio: "evento",
139
151
  nome_cliente: node.name,
140
152
  nome_telecamera: nomeCamera,
141
- ip_telecamera: camera.ip,
153
+ ip_nvr: node.host,
142
154
  tipo_evento: evento,
143
155
  timestamp_epoch: timestamp,
144
156
  stato_telecamera: "ONLINE",
@@ -147,15 +159,18 @@ module.exports = function(RED) {
147
159
  video_base64: null
148
160
  };
149
161
 
150
-
151
162
  let fileDaCancellare = [];
163
+ const parsedChannel = parseInt(channelID, 10);
164
+ const trackVideo = (parsedChannel * 100) + 1; // Formula dinamica NVR (*100+1) e.g. 201
165
+ const trackFoto = (parsedChannel * 100) + 3; // Formula dinamica NVR (*100+3) e.g. 203
166
+
167
+ const tracks = [{ id: trackVideo.toString(), type: "video" }, { id: trackFoto.toString(), type: "foto" }];
152
168
 
153
169
  try {
154
- const tracks = [{ id: "201" }, { id: "203" }];
155
170
  for (let t of tracks) {
156
- const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${t.id}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>100</maxResults><metadataList><metadataDescriptor>//recordType.meta.std-cgi.com/${evento}</metadataDescriptor></metadataList></CMSearchDescription>`;
171
+ const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>NVR_CATCH</searchID><trackIDList><trackID>${t.id}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>100</maxResults><metadataList><metadataDescriptor>//recordType.meta.std-cgi.com/${output.tipo_evento}</metadataDescriptor></metadataList></CMSearchDescription>`;
157
172
 
158
- const resSearch = await camAuth.request({
173
+ const resSearch = await nvrAuth.request({
159
174
  method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
160
175
  });
161
176
 
@@ -164,7 +179,7 @@ module.exports = function(RED) {
164
179
 
165
180
  if (uriMatch) {
166
181
  const rawUri = uriMatch[1].replace(/&amp;/g, '&');
167
- const resDown = await camAuth.request({
182
+ const resDown = await nvrAuth.request({
168
183
  method: 'GET',
169
184
  url: `${baseUrl}/download`,
170
185
  data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
@@ -172,24 +187,17 @@ module.exports = function(RED) {
172
187
  });
173
188
 
174
189
  let buffer = Buffer.from(resDown.data);
175
- if (t.id === "203") {
176
- // SALVA FOTO IN LOCALE
190
+ if (t.type === "foto") {
177
191
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
178
192
  fs.writeFileSync(fullImgPath, buffer);
179
-
180
- // La convertiamo subito in testo per il payload
181
193
  output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
182
-
183
- // Registriamo il file per la distruzione futura
184
194
  fileDaCancellare.push(fullImgPath);
185
195
  } else {
186
- // SALVA VIDEO IN LOCALE-
187
196
  if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
188
197
  const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
189
198
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
190
199
  fs.writeFileSync(rawPath, buffer);
191
200
 
192
- // Eseguiamo ffmpeg localmente
193
201
  await new Promise((resolve) => {
194
202
  exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
195
203
  if (!err && fs.existsSync(fixedPath)) {
@@ -206,28 +214,20 @@ module.exports = function(RED) {
206
214
  }
207
215
  }
208
216
 
209
- // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
210
217
  if (output.foto_base64 || output.video_base64) {
211
218
  node.send({ payload: output });
212
219
 
213
- // TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
214
220
  setTimeout(() => {
215
221
  for (let file of fileDaCancellare) {
216
- try {
217
- if (fs.existsSync(file)) {
218
- fs.unlinkSync(file);
219
- }
220
- } catch (err) {
221
- node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
222
- }
222
+ try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
223
223
  }
224
224
  }, 120000);
225
225
  }
226
226
 
227
227
  } catch (e) {
228
- node.error(`Errore Download Cam ${channelID}: ${e.message}`);
228
+ node.error(`Errore Download NVR per Canale ${channelID}: ${e.message}`);
229
229
  }
230
- updateNodeStatus();
230
+ node.status({fill:"green", shape:"ring", text:"In ascolto"});
231
231
  }
232
232
 
233
233
  // --- ALERT STREAM ---
@@ -235,19 +235,33 @@ module.exports = function(RED) {
235
235
  if (isClosing) return;
236
236
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
237
237
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
238
+
238
239
  nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
239
240
  .then(response => {
240
241
  streamRequest = response;
241
242
  if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
242
243
  updateNodeStatus();
244
+
243
245
  response.data.on('data', (chunk) => {
244
246
  const data = chunk.toString().toLowerCase();
247
+
245
248
  if (data.includes("active")) {
246
249
  const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
247
250
  if (chMatch) {
251
+ let channelID = chMatch[1];
248
252
  let ev = "Unknown";
249
- for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
250
- downloadMedia(ev, chMatch[1]);
253
+
254
+ for (let e of EventList) {
255
+ if (data.includes(e.toLowerCase())) {
256
+ ev = e;
257
+ break;
258
+ }
259
+ }
260
+
261
+ // Se non è un allarme presente in lista o è un heartbeat, fermati qui
262
+ if (ev === "Unknown") return;
263
+
264
+ downloadMedia(ev, channelID);
251
265
  }
252
266
  }
253
267
  });
@@ -264,6 +278,7 @@ module.exports = function(RED) {
264
278
 
265
279
  startAlertStream();
266
280
  setTimeout(checkCameras, 2000);
281
+
267
282
  node.on('close', (done) => {
268
283
  isClosing = true;
269
284
  clearInterval(heartbeatInterval);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.42",
3
+ "version": "1.1.44",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",