node-red-contrib-hik-media-buffer 1.1.41 → 1.1.43

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,25 @@ 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
33
+ const EventList = ["FieldDetection", "LineDetection"];
30
34
 
31
35
  // CARTELLA TEMPORANEA
32
36
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
@@ -36,66 +40,81 @@ module.exports = function(RED) {
36
40
 
37
41
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
38
42
 
39
- // --- PRENDE IL NOME DELLA TELECAMERA ---
40
- async function getCameraName(cam) {
41
- const camAuth = new AxiosDigestAuth({
42
- username: node.user,
43
- password: node.camPass
44
- });
45
-
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 });
46
46
  try {
47
- const res = await camAuth.request({
47
+ const res = await nvrAuth.request({
48
48
  method: 'GET',
49
- 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`,
50
50
  timeout: 5000,
51
51
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
52
52
  });
53
53
 
54
54
  const data = res.data.toString();
55
55
  const match = data.match(/<name>([^<]+)<\/name>/i);
56
-
57
- if (match && match[1]) {
58
- return match[1].trim();
59
- } else {
60
- return `Canale_${cam.channel}`;
61
- }
62
-
56
+ return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
63
57
  } catch (e) {
64
- return `Camera_${cam.channel}`;
58
+ return `Canale_${channelID}`;
65
59
  }
66
60
  }
67
61
 
68
- // --- CONTROLLO STATUS CAMERE ---
62
+ // --- CONTROLLO STATUS DINAMICO DEI CANALI DIGITALI DALL'NVR ---
69
63
  async function checkCameras() {
70
64
  if (isClosing) return;
71
- for (let cam of node.cameras) {
72
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
73
- try {
74
- await camAuth.request({
75
- method: 'GET',
76
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
77
- timeout: 5000,
78
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
79
- });
80
- if (statoCamera[cam.ip] === false) {
81
- const nomeOnline = await getCameraName(cam);
82
- 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" } });
83
- statoCamera[cam.ip] = true;
84
- } else if (statoCamera[cam.ip] === undefined) {
85
- statoCamera[cam.ip] = true;
86
- }
87
- } catch (e) {
88
- if (statoCamera[cam.ip] !== false) {
89
- 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" } });
90
- 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 [ps. 57-58 del manuale]
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>([^<]+)<\/status>/i);
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 senza inviare messaggi di stato a vuoto
106
+ statoCanale[ch] = isOnline;
107
+ }
91
108
  }
92
109
  }
110
+ } catch (e) {
111
+ nvrOnline = false; // Se fallisce la GET, è l'NVR ad essere irraggiungibile
93
112
  }
94
113
  updateNodeStatus();
95
114
  }
96
115
 
97
116
  function updateNodeStatus() {
98
- const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
117
+ const offlineCams = Object.values(statoCanale).filter(v => v === false).length;
99
118
  if (!nvrOnline) {
100
119
  node.status({fill:"red", shape:"ring", text:"NVR Offline"});
101
120
  } else if (offlineCams > 0) {
@@ -105,39 +124,33 @@ module.exports = function(RED) {
105
124
  }
106
125
  }
107
126
 
127
+ // Il controllo camere gira in background ogni 30 secondi interrogando solo l'NVR
108
128
  const heartbeatInterval = setInterval(checkCameras, 30000);
109
129
 
110
- // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
130
+ // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DALL'NVR ---
111
131
  async function downloadMedia(evento, channelID) {
112
- const camera = node.cameras.find(c => c.channel == channelID);
113
- if (!camera) return;
114
-
115
- const nomeCamera = await getCameraName(camera);
116
-
117
132
  const nowTime = Date.now();
118
- if(!lastTriggerTime[camera.ip]){
119
- lastTriggerTime[camera.ip] = 0;
120
- }
121
- if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
122
- lastTriggerTime[camera.ip] = nowTime;
133
+ if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
134
+ if (nowTime - lastTriggerTime[channelID] < 5000) return;
135
+ lastTriggerTime[channelID] = nowTime;
123
136
 
124
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
137
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
125
138
  const referenceTime = new Date();
126
139
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
127
140
  const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
128
141
  const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
129
142
 
130
143
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
131
- await new Promise(resolve => setTimeout(resolve, 6000));
144
+ const nomeCamera = await getCameraName(channelID);
132
145
 
133
- const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
146
+ await new Promise(resolve => setTimeout(resolve, 6000));
147
+ const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
134
148
 
135
- // struttura del payload per Python
136
149
  let output = {
137
150
  tipo_messaggio: "evento",
138
151
  nome_cliente: node.name,
139
152
  nome_telecamera: nomeCamera,
140
- ip_telecamera: camera.ip,
153
+ ip_nvr: node.host,
141
154
  tipo_evento: evento,
142
155
  timestamp_epoch: timestamp,
143
156
  stato_telecamera: "ONLINE",
@@ -146,15 +159,18 @@ module.exports = function(RED) {
146
159
  video_base64: null
147
160
  };
148
161
 
149
-
150
162
  let fileDaCancellare = [];
163
+ const parsedChannel = parseInt(channelID, 10);
164
+ const trackVideo = (parsedChannel * 100) + 1; // Formula dinamica NVR (*100+1) [cite: 958]
165
+ const trackFoto = (parsedChannel * 100) + 3; // Formula dinamica NVR (*100+3) [cite: 958]
166
+
167
+ const tracks = [{ id: trackVideo.toString(), type: "video" }, { id: trackFoto.toString(), type: "foto" }];
151
168
 
152
169
  try {
153
- const tracks = [{ id: "201" }, { id: "203" }];
154
170
  for (let t of tracks) {
155
171
  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>`;
156
172
 
157
- const resSearch = await camAuth.request({
173
+ const resSearch = await nvrAuth.request({
158
174
  method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
159
175
  });
160
176
 
@@ -171,24 +187,17 @@ module.exports = function(RED) {
171
187
  });
172
188
 
173
189
  let buffer = Buffer.from(resDown.data);
174
- if (t.id === "203") {
175
- // SALVA FOTO IN LOCALE
190
+ if (t.type === "foto") {
176
191
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
177
192
  fs.writeFileSync(fullImgPath, buffer);
178
-
179
- // La convertiamo subito in testo per il payload
180
193
  output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
181
-
182
- // Registriamo il file per la distruzione futura
183
194
  fileDaCancellare.push(fullImgPath);
184
195
  } else {
185
- // SALVA VIDEO IN LOCALE-
186
196
  if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
187
197
  const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
188
198
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
189
199
  fs.writeFileSync(rawPath, buffer);
190
200
 
191
- // Eseguiamo ffmpeg localmente
192
201
  await new Promise((resolve) => {
193
202
  exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
194
203
  if (!err && fs.existsSync(fixedPath)) {
@@ -205,20 +214,12 @@ module.exports = function(RED) {
205
214
  }
206
215
  }
207
216
 
208
- // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
209
217
  if (output.foto_base64 || output.video_base64) {
210
218
  node.send({ payload: output });
211
219
 
212
- // TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
213
220
  setTimeout(() => {
214
221
  for (let file of fileDaCancellare) {
215
- try {
216
- if (fs.existsSync(file)) {
217
- fs.unlinkSync(file);
218
- }
219
- } catch (err) {
220
- node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
221
- }
222
+ try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
222
223
  }
223
224
  }, 120000);
224
225
  }
@@ -226,7 +227,7 @@ module.exports = function(RED) {
226
227
  } catch (e) {
227
228
  node.error(`Errore Download Cam ${channelID}: ${e.message}`);
228
229
  }
229
- updateNodeStatus();
230
+ node.status({fill:"green", shape:"ring", text:"In ascolto"});
230
231
  }
231
232
 
232
233
  // --- ALERT STREAM ---
@@ -234,46 +235,27 @@ module.exports = function(RED) {
234
235
  if (isClosing) return;
235
236
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
236
237
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
238
+
237
239
  nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
238
240
  .then(response => {
239
241
  streamRequest = response;
240
242
  if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
241
243
  updateNodeStatus();
244
+
242
245
  response.data.on('data', (chunk) => {
243
- // Convertiamo in minuscolo per sicurezza nel parsing del testo
244
246
  const data = chunk.toString().toLowerCase();
245
247
 
246
- // Controlliamo che sia un evento attivo (allarme o heartbeat che sia)
247
248
  if (data.includes("active")) {
248
-
249
- // 1. ESTRAZIONE DINAMICA DELL'EVENTO NATIVO (Pagina 32 del manuale)
250
- let ev = data.match(/<eventtype>([^<]+)<\/eventtype>/i)?.[1] || "unknown";
251
- ev = ev.trim();
252
-
253
- // 2. FILTRO FONDAMENTALE: Se è l'heartbeat di test o è vuoto, FERMA TUTTO
254
- if (ev === "unknown" || ev === "heartbeat") {
255
- return; // Non entra in downloadMedia e azzera il loop infinito!
256
- }
257
-
258
- // 3. RECUPERO DEL CANALE REALE (Dall'XML della telecamera o NVR)
259
249
  const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
260
250
  if (chMatch) {
261
- let channelID = chMatch[1];
262
-
263
- // Logica universale:
264
- // Se node.host è l'IP della telecamera singola, per la tua funzione downloadMedia
265
- // passiamo il canale "2" in modo che il tuo elenco selezioni la riga corretta della termica,
266
- // ma l'evento "ev" (es. fielddetection) viene passato pulito in minuscolo come vuole la cam!
267
- const camTermica = node.cameras.find(c => c.channel == "2");
268
- if (camTermica && node.host === camTermica.ip && channelID === "1") {
269
- channelID = "2";
251
+ let ev = "Unknown";
252
+ for (let e of EventList) {
253
+ if (data.includes(e.toLowerCase())) {
254
+ ev = e;
255
+ break;
256
+ }
270
257
  }
271
-
272
- // Aspetta 2 secondi per dare tempo alla telecamera di scrivere l'immagine prima di chiedergliela
273
- setTimeout(() => {
274
- // Lancia la tua funzione downloadMedia originale che è giusta!
275
- downloadMedia(ev, channelID);
276
- }, 2000);
258
+ downloadMedia(ev, chMatch[1]);
277
259
  }
278
260
  }
279
261
  });
@@ -290,6 +272,7 @@ module.exports = function(RED) {
290
272
 
291
273
  startAlertStream();
292
274
  setTimeout(checkCameras, 2000);
275
+
293
276
  node.on('close', (done) => {
294
277
  isClosing = true;
295
278
  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.41",
3
+ "version": "1.1.43",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",