node-red-contrib-hik-media-buffer 1.1.55 → 1.1.56

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.
Files changed (2) hide show
  1. package/hik-media-buffer.js +40 -27
  2. package/package.json +1 -1
@@ -12,7 +12,7 @@ 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;
@@ -25,7 +25,11 @@ module.exports = function(RED) {
25
25
  let statoCanale = {};
26
26
 
27
27
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
28
+
29
+ // EventList per l'alertStream
28
30
  const EventList = ["FieldDetection", "LineDetection"];
31
+
32
+ // CARTELLA TEMPORANEA
29
33
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
30
34
  if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
31
35
 
@@ -33,6 +37,7 @@ module.exports = function(RED) {
33
37
 
34
38
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
35
39
 
40
+ // --- PRENDE IL NOME DEL CANALE DIRETTAMENTE DALL'NVR ---
36
41
  async function getCameraName(channelID) {
37
42
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
38
43
  try {
@@ -42,6 +47,7 @@ module.exports = function(RED) {
42
47
  timeout: 5000,
43
48
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
44
49
  });
50
+
45
51
  const data = res.data.toString();
46
52
  const match = data.match(/<name>([^<]+)<\/name>/i);
47
53
  return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
@@ -50,9 +56,11 @@ module.exports = function(RED) {
50
56
  }
51
57
  }
52
58
 
59
+ // --- CONTROLLO STATUS DINAMICO DEI CANALI DIGITALI DALL'NVR ---
53
60
  async function checkCameras() {
54
61
  if (isClosing) return;
55
62
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
63
+
56
64
  try {
57
65
  const res = await nvrAuth.request({
58
66
  method: 'GET',
@@ -60,19 +68,23 @@ module.exports = function(RED) {
60
68
  timeout: 5000,
61
69
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
62
70
  });
71
+
63
72
  nvrOnline = true;
64
73
  const xmlData = res.data.toString();
74
+
65
75
  const bloccoRegex = /<InputProxyChannelStatus[\s\\S]*?>([\s\S]*?)<\/InputProxyChannelStatus>/gi;
66
76
  let matchBlocco;
67
77
 
68
78
  while ((matchBlocco = bloccoRegex.exec(xmlData)) !== null) {
69
79
  const bloccoContenuto = matchBlocco[1];
80
+
70
81
  const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
71
82
  const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i);
72
83
 
73
84
  if (idMatch && statusMatch) {
74
85
  const ch = idMatch[1];
75
86
  const statusStr = statusMatch[1].toLowerCase();
87
+
76
88
  const isOnline = statusStr.includes("online") || statusStr.includes("recording");
77
89
 
78
90
  if (isOnline && statoCanale[ch] === false) {
@@ -106,7 +118,7 @@ module.exports = function(RED) {
106
118
 
107
119
  const heartbeatInterval = setInterval(checkCameras, 30000);
108
120
 
109
- // --- DOWNLOAD CON DEBUG COPIOSO ---
121
+ // --- DOWNLOAD MULTIMEDIALE (XML PER FOTO + JSON PER VIDEO) ---
110
122
  async function downloadMedia(evento, channelID) {
111
123
  node.warn(`[STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
112
124
 
@@ -119,12 +131,11 @@ module.exports = function(RED) {
119
131
  const referenceTime = new Date();
120
132
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
121
133
 
122
- // Finestre temporali per i due endpoint
123
- const startTimeZ = toHikDate(new Date(referenceTime.getTime() - (20 * 1000))); // Per XML (es. 2026-06-08T15:00:00Z)
134
+ // Finestre temporali standard UTC
135
+ const startTimeZ = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
124
136
  const endTimeZ = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
125
137
 
126
- // Per il JSON creiamo il formato con l'offset del fuso orario italiano "+02:00" (o "+01:00" a seconda di come risponde il sistema)
127
- // Sfruttiamo una formattazione pulita per l'orario del JSON dell'NVR
138
+ // Finestre temporali per il JSON dell'NVR (con specifica dell'offset locale, es. "+02:00")
128
139
  const formatJsonDate = (d) => {
129
140
  const pad = (num) => String(num).padStart(2, '0');
130
141
  return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} 02:00`;
@@ -135,14 +146,13 @@ module.exports = function(RED) {
135
146
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
136
147
  const nomeCamera = await getCameraName(channelID);
137
148
 
138
- node.warn(`[STEP 3] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
149
+ node.warn(`[STEP 2] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
139
150
  await new Promise(resolve => setTimeout(resolve, 6000));
140
151
 
141
152
  const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
142
153
 
143
- // Stringhe per i due formati
144
- const eventoMinuscolo = evento.toLowerCase(); // "linedetection" o "fielddetection" per il JSON
145
- let eventoNVR = "FieldDetection"; // "FieldDetection" o "LineDetection" per l'XML
154
+ const eventoMinuscolo = evento.toLowerCase();
155
+ let eventoNVR = "FieldDetection";
146
156
  if (eventoMinuscolo.includes("linedetection")) eventoNVR = "LineDetection";
147
157
 
148
158
  let output = {
@@ -159,11 +169,11 @@ module.exports = function(RED) {
159
169
  };
160
170
 
161
171
  const parsedChannel = parseInt(channelID, 10);
162
- const trackFoto = (parsedChannel * 100) + 3; // Es. 203
172
+ const trackFoto = (parsedChannel * 100) + 3; // Es. Canale 2 -> 203
163
173
  let fileDaCancellare = [];
164
174
 
165
- // --- PARTE 1: SCARICHIAMO IL VIDEO USANDO IL JSON SUL NUOVO ENDPOINT ---
166
- node.warn(`[STEP 4 - VIDEO] Invio POST a eventRecordSearch con JSON dell'ispeziona...`);
175
+ // --- PARTE 1: RICERCA E SCARICO VIDEO VIA JSON ---
176
+ node.warn(`[STEP 3 - VIDEO] Invio query JSON a eventRecordSearch per canale ${parsedChannel}...`);
167
177
  const searchJsonPayload = {
168
178
  "EventSearchDescription": {
169
179
  "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
@@ -190,13 +200,12 @@ module.exports = function(RED) {
190
200
  headers: { "Content-Type": "application/json" }
191
201
  });
192
202
 
193
- // Il payload JSON restituisce una struttura differente, estraiamo l'url di download (playbackURI)
194
203
  const resDataStr = JSON.stringify(resSearchVideo.data);
195
204
  const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
196
205
 
197
206
  if (videoUriMatch) {
198
207
  let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
199
- node.warn(`[STEP 4.1 - VIDEO] Trovato URI video nel JSON: ${rawVideoUri}`);
208
+ node.warn(`[STEP 3.1 - VIDEO] Trovato URI video nel JSON: ${rawVideoUri}`);
200
209
 
201
210
  const resDownVideo = await camAuth.request({
202
211
  method: 'GET',
@@ -217,6 +226,7 @@ module.exports = function(RED) {
217
226
  if (!err && fs.existsSync(fixedPath)) {
218
227
  output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
219
228
  fileDaCancellare.push(fixedPath);
229
+ node.warn(`[STEP 3.2 - VIDEO] Convertito video con successo.`);
220
230
  } else {
221
231
  output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
222
232
  }
@@ -225,15 +235,16 @@ module.exports = function(RED) {
225
235
  });
226
236
  });
227
237
  } else {
228
- node.warn(`[STEP 4.2 - VIDEO] Nessun URI video trovato nella risposta JSON.`);
238
+ node.warn(`[STEP 3.3 - VIDEO] Nessun file video indicizzato per questa finestra temporale.`);
229
239
  }
230
240
  } catch (videoErr) {
231
- node.error(`[VIDEO ERR] Errore durante la ricerca video JSON: ${videoErr.message}`);
241
+ node.error(`[VIDEO ERR] Errore durante la ricerca JSON: ${videoErr.message}`);
232
242
  }
233
243
 
234
- // --- PARTE 2: SCARICHIAMO LA FOTO USANDO L'XML SUL VECCHIO ENDPOINT ---
235
- node.warn(`[STEP 5 - FOTO] Invio POST /search in XML per la traccia foto ${trackFoto}...`);
236
- const searchXmlFoto = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${trackFoto}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTimeZ}</startTime><endTime>${endTimeZ}</endTime></timeSpan></timeSpanList><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
244
+ // --- PARTE 2: RICERCA E SCARICO FOTO VIA XML (CORRETTO) ---
245
+ node.warn(`[STEP 4 - FOTO] Invio query XML a /search per traccia foto ${trackFoto}...`);
246
+
247
+ const searchXmlFoto = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${trackFoto}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTimeZ}</startTime><endTime>${endTimeZ}</endTime></timeSpan></timeSpanList><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPosition>0</searchResultPosition><metadataList><metadataDescriptor>recordType.meta.hikvision.com/timing</metadataDescriptor></metadataList></CMSearchDescription>`;
237
248
 
238
249
  try {
239
250
  const resSearchFoto = await camAuth.request({
@@ -248,7 +259,7 @@ module.exports = function(RED) {
248
259
 
249
260
  if (uriMatch) {
250
261
  const rawUriFoto = uriMatch[1].replace(/&amp;/g, '&');
251
- node.warn(`[STEP 5.1 - FOTO] Trovato playbackURI foto: ${rawUriFoto}`);
262
+ node.warn(`[STEP 4.1 - FOTO] Trovato URI foto: ${rawUriFoto}`);
252
263
 
253
264
  const resDownFoto = await camAuth.request({
254
265
  method: 'GET',
@@ -261,16 +272,17 @@ module.exports = function(RED) {
261
272
  fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
262
273
  output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
263
274
  fileDaCancellare.push(fullImgPath);
275
+ node.warn(`[STEP 4.2 - FOTO] Foto salvata e convertita correttamente.`);
264
276
  } else {
265
- node.warn(`[STEP 5.2 - FOTO] Nessun playbackURI foto trovato nell'XML.`);
277
+ node.warn(`[STEP 4.3 - FOTO] Nessuna foto trovata per questa finestra temporale.`);
266
278
  }
267
279
  } catch (fotoErr) {
268
- node.error(`[FOTO ERR] Errore durante la ricerca foto XML: ${fotoErr.message}`);
280
+ node.error(`[FOTO ERR] Errore durante la ricerca XML: ${fotoErr.message}`);
269
281
  }
270
282
 
271
- // --- FINE: INVIO DATI A NODE-RED ---
283
+ // --- PARTE 3: SPEDIZIONE VERSO PYTHON ---
272
284
  if (output.foto_base64 || output.video_base64) {
273
- node.warn(`[STEP 6] Payload multimediale spedito correttamente!`);
285
+ node.warn(`[STEP 5] Spedizione payload multimediale completata!`);
274
286
  node.send({ payload: output });
275
287
 
276
288
  setTimeout(() => {
@@ -279,11 +291,12 @@ module.exports = function(RED) {
279
291
  }
280
292
  }, 120000);
281
293
  } else {
282
- node.warn(`[STEP 6] Nessun file base64 generato.`);
294
+ node.warn(`[STEP 5] Nessun file multimediale scaricato, nessun pacchetto inviato.`);
283
295
  }
284
- node.status({fill:"green", shape:"ring", text:"In ascolto"});
296
+ updateNodeStatus();
285
297
  }
286
298
 
299
+ // --- ALERT STREAM ---
287
300
  function startAlertStream() {
288
301
  if (isClosing) return;
289
302
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.55",
3
+ "version": "1.1.56",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",