node-red-contrib-hik-media-buffer 1.1.57 → 1.1.59

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 +122 -143
  2. package/package.json +2 -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; // IP dell'NVR centrale
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,19 +25,19 @@ 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
30
28
  const EventList = ["FieldDetection", "LineDetection"];
31
29
 
32
- // CARTELLA TEMPORANEA
33
30
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
34
31
  if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
35
32
 
36
33
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
37
34
 
38
- function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
35
+ // FORMATTAZIONE DATA CORRETTA PER HARD DISK NVR (Rimosso lo zulu 'Z' che mandava in 400 il database)
36
+ function toHikDate(d) {
37
+ const pad = (num) => String(num).padStart(2, '0');
38
+ return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
39
+ }
39
40
 
40
- // --- PRENDE IL NOME DEL CANALE DIRETTAMENTE DALL'NVR ---
41
41
  async function getCameraName(channelID) {
42
42
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
43
43
  try {
@@ -47,7 +47,6 @@ module.exports = function(RED) {
47
47
  timeout: 5000,
48
48
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
49
49
  });
50
-
51
50
  const data = res.data.toString();
52
51
  const match = data.match(/<name>([^<]+)<\/name>/i);
53
52
  return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
@@ -56,11 +55,9 @@ module.exports = function(RED) {
56
55
  }
57
56
  }
58
57
 
59
- // --- CONTROLLO STATUS DINAMICO DEI CANALI DIGITALI DALL'NVR ---
60
58
  async function checkCameras() {
61
59
  if (isClosing) return;
62
60
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
63
-
64
61
  try {
65
62
  const res = await nvrAuth.request({
66
63
  method: 'GET',
@@ -68,23 +65,19 @@ module.exports = function(RED) {
68
65
  timeout: 5000,
69
66
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
70
67
  });
71
-
72
68
  nvrOnline = true;
73
69
  const xmlData = res.data.toString();
74
-
75
70
  const bloccoRegex = /<InputProxyChannelStatus[\s\\S]*?>([\s\S]*?)<\/InputProxyChannelStatus>/gi;
76
71
  let matchBlocco;
77
72
 
78
73
  while ((matchBlocco = bloccoRegex.exec(xmlData)) !== null) {
79
74
  const bloccoContenuto = matchBlocco[1];
80
-
81
75
  const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
82
76
  const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i);
83
77
 
84
78
  if (idMatch && statusMatch) {
85
79
  const ch = idMatch[1];
86
80
  const statusStr = statusMatch[1].toLowerCase();
87
-
88
81
  const isOnline = statusStr.includes("online") || statusStr.includes("recording");
89
82
 
90
83
  if (isOnline && statoCanale[ch] === false) {
@@ -118,42 +111,42 @@ module.exports = function(RED) {
118
111
 
119
112
  const heartbeatInterval = setInterval(checkCameras, 30000);
120
113
 
121
- // --- DOWNLOAD MULTIMEDIALE (XML PER FOTO + JSON PER VIDEO) ---
114
+ // --- IL TUO DOWNLOAD MEDIA INIZIALE CORRETTO (100% XML SEPARATO) ---
122
115
  async function downloadMedia(evento, channelID) {
123
- node.warn(`[STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
116
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
124
117
 
125
118
  const nowTime = Date.now();
126
119
  if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
127
- if (nowTime - lastTriggerTime[channelID] < 5000) return;
120
+ if (nowTime - lastTriggerTime[channelID] < 5000) {
121
+ node.warn(`[warn] [hik-media-buffer:test] Antirimbalzo bloccato.`);
122
+ return;
123
+ }
128
124
  lastTriggerTime[channelID] = nowTime;
129
125
 
130
126
  const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
131
127
  const referenceTime = new Date();
132
128
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
133
129
 
134
- // Finestre temporali standard UTC
135
- const startTimeZ = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
136
- const endTimeZ = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
137
-
138
- // Finestre temporali per il JSON dell'NVR (con specifica dell'offset locale, es. "+02:00")
139
- const formatJsonDate = (d) => {
140
- const pad = (num) => String(num).padStart(2, '0');
141
- return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} 02:00`;
142
- };
143
- const startTimeJson = formatJsonDate(new Date(referenceTime.getTime() - (20 * 1000)));
144
- const endTimeJson = formatJsonDate(new Date(referenceTime.getTime() + (20 * 1000)));
130
+ // Generazione finestre temporali pulite per il database dei dischi dell'NVR
131
+ const startTime = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
132
+ const endTime = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
133
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 2] Finestra temporale impostata: ${startTime} -> ${endTime}`);
145
134
 
146
135
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
147
136
  const nomeCamera = await getCameraName(channelID);
148
137
 
149
- node.warn(`[STEP 2] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
138
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 3] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
150
139
  await new Promise(resolve => setTimeout(resolve, 6000));
151
140
 
152
141
  const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
153
142
 
154
- const eventoMinuscolo = evento.toLowerCase();
155
- let eventoNVR = "FieldDetection";
156
- if (eventoMinuscolo.includes("linedetection")) eventoNVR = "LineDetection";
143
+ let eventoNVR = "FieldDetection";
144
+ for (let e of EventList) {
145
+ if (evento.toLowerCase() === e.toLowerCase()) {
146
+ eventoNVR = e;
147
+ break;
148
+ }
149
+ }
157
150
 
158
151
  let output = {
159
152
  tipo_messaggio: "evento",
@@ -169,144 +162,129 @@ module.exports = function(RED) {
169
162
  };
170
163
 
171
164
  const parsedChannel = parseInt(channelID, 10);
172
- const trackFoto = (parsedChannel * 100) + 3; // Es. Canale 2 -> 203
165
+ const trackVideo = (parsedChannel * 100) + 1;
166
+ const trackFoto = (parsedChannel * 100) + 3;
167
+
168
+ // Le due tracce originali del tuo ciclo
169
+ const tracks = [
170
+ { id: trackVideo.toString(), isFoto: false, label: "VIDEO" },
171
+ { id: trackFoto.toString(), isFoto: true, label: "FOTO" }
172
+ ];
173
173
  let fileDaCancellare = [];
174
174
 
175
- // --- PARTE 1: RICERCA E SCARICO VIDEO VIA JSON ---
176
- node.warn(`[STEP 3 - VIDEO] Invio query JSON a eventRecordSearch per canale ${parsedChannel}...`);
177
- const searchJsonPayload = {
178
- "EventSearchDescription": {
179
- "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
180
- "searchResultPosition": 0,
181
- "maxResults": 30,
182
- "timeSpanList": [{
183
- "startTime": startTimeJson,
184
- "endTime": endTimeJson
185
- }],
186
- "type": "all",
187
- "channels": [parsedChannel],
188
- "eventType": "behavior",
189
- "behavior": {
190
- "behaviorEventType": eventoMinuscolo
191
- }
192
- }
193
- };
194
-
195
175
  try {
196
- const resSearchVideo = await camAuth.request({
197
- method: 'POST',
198
- url: `${baseUrl}/eventRecordSearch?format=json`,
199
- data: searchJsonPayload,
200
- headers: { "Content-Type": "application/json" }
201
- });
202
-
203
- const resDataStr = JSON.stringify(resSearchVideo.data);
204
- const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
205
-
206
- if (videoUriMatch) {
207
- let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
208
- node.warn(`[STEP 3.1 - VIDEO] Trovato URI video nel JSON: ${rawVideoUri}`);
176
+ for (let t of tracks) {
177
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 4] Elaborazione traccia ${t.label} (ID: ${t.id})`);
209
178
 
210
- const resDownVideo = await camAuth.request({
211
- method: 'GET',
212
- url: `${baseUrl}/download`,
213
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawVideoUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
214
- responseType: 'arraybuffer'
215
- });
179
+ // SEPARAZIONE DEI DESCRITTORI: timing per la traccia video, evento per lo snapshot foto
180
+ let descriptorTarget = t.isFoto ? `recordType.meta.hikvision.com/${eventoNVR}` : `recordType.meta.hikvision.com/timing`;
216
181
 
217
- let videoBuffer = Buffer.from(resDownVideo.data);
218
- if (videoBuffer.slice(0, 4).toString() === 'IMKH') videoBuffer = videoBuffer.slice(40);
219
-
220
- const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
221
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
222
- fs.writeFileSync(rawPath, videoBuffer);
223
-
224
- await new Promise((resolve) => {
225
- exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
226
- if (!err && fs.existsSync(fixedPath)) {
227
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
228
- fileDaCancellare.push(fixedPath);
229
- node.warn(`[STEP 3.2 - VIDEO] Convertito video con successo.`);
182
+ // LA TUA STRUTTURA XML INIZIALE PULITA E ALLINEATA AI MANUALI NVR
183
+ 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>30</maxResults><metadataList><metadataDescriptor>${descriptorTarget}</metadataDescriptor></metadataList></CMSearchDescription>`;
184
+
185
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
186
+
187
+ try {
188
+ const resSearch = await camAuth.request({
189
+ method: 'POST',
190
+ url: `${baseUrl}/search`,
191
+ data: searchXml,
192
+ headers: { "Content-Type": "application/xml" }
193
+ });
194
+
195
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 4.2] Risposta ricevuta da /search per traccia ${t.id}: Status ${resSearch.status}`);
196
+
197
+ let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
198
+ const uriMatch = xml.match(/<playbackURI>([^<]+)</);
199
+
200
+ if (uriMatch) {
201
+ const rawUri = uriMatch[1].replace(/&amp;/g, '&');
202
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 5] Trovato playbackURI per traccia ${t.id}: ${rawUri}`);
203
+
204
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 5.1] Invio richiesta GET /download per traccia ${t.id}`);
205
+ const resDown = await camAuth.request({
206
+ method: 'GET',
207
+ url: `${baseUrl}/download`,
208
+ data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
209
+ responseType: 'arraybuffer'
210
+ });
211
+
212
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 5.2] Scaricati ${resDown.data.byteLength} byte per traccia ${t.id}`);
213
+ let buffer = Buffer.from(resDown.data);
214
+
215
+ if (t.isFoto) {
216
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
217
+ fs.writeFileSync(fullImgPath, buffer);
218
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
219
+ fileDaCancellare.push(fullImgPath);
220
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 5.3] Immagine salvata e convertita in Base64.`);
230
221
  } else {
231
- output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
222
+ if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
223
+ const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
224
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
225
+ fs.writeFileSync(rawPath, buffer);
226
+
227
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 5.3] Video grezzo salvato. Avvio conversione FFMPEG...`);
228
+ await new Promise((resolve) => {
229
+ exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
230
+ if (!err && fs.existsSync(fixedPath)) {
231
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
232
+ fileDaCancellare.push(fixedPath);
233
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 5.4] Conversione FFMPEG completata con successo.`);
234
+ } else {
235
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 5.4] Errore FFMPEG (uso il video grezzo di backup): ${err ? err.message : 'File mancante'}`);
236
+ output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
237
+ }
238
+ fileDaCancellare.push(rawPath);
239
+ resolve();
240
+ });
241
+ });
232
242
  }
233
- fileDaCancellare.push(rawPath);
234
- resolve();
235
- });
236
- });
237
- } else {
238
- node.warn(`[STEP 3.3 - VIDEO] Nessun file video indicizzato per questa finestra temporale.`);
243
+ } else {
244
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 4.3] Nessun playbackURI trovato nel documento di risposta XML per traccia ${t.id}. XML di risposta:\n${resSearch.data}`);
245
+ }
246
+ } catch (searchError) {
247
+ let errorDetails = searchError.message;
248
+ if (searchError.response) {
249
+ errorDetails += ` | Status: ${searchError.response.status} | Dati Risposta: ${JSON.stringify(searchError.response.data)}`;
250
+ }
251
+ node.error(`[error] [hik-media-buffer:test] [CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
252
+ }
239
253
  }
240
- } catch (videoErr) {
241
- node.error(`[VIDEO ERR] Errore durante la ricerca JSON: ${videoErr.message}`);
242
- }
243
-
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>`;
248
254
 
249
- try {
250
- const resSearchFoto = await camAuth.request({
251
- method: 'POST',
252
- url: `${baseUrl}/search`,
253
- data: searchXmlFoto,
254
- headers: { "Content-Type": "application/xml" }
255
- });
256
-
257
- let xml = resSearchFoto.data.replace(/<(\/?)\w+:/g, "<$1");
258
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
259
-
260
- if (uriMatch) {
261
- const rawUriFoto = uriMatch[1].replace(/&amp;/g, '&');
262
- node.warn(`[STEP 4.1 - FOTO] Trovato URI foto: ${rawUriFoto}`);
255
+ if (output.foto_base64 || output.video_base64) {
256
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
257
+ node.send({ payload: output });
263
258
 
264
- const resDownFoto = await camAuth.request({
265
- method: 'GET',
266
- url: `${baseUrl}/download`,
267
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUriFoto.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
268
- responseType: 'arraybuffer'
269
- });
270
-
271
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
272
- fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
273
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
274
- fileDaCancellare.push(fullImgPath);
275
- node.warn(`[STEP 4.2 - FOTO] Foto salvata e convertita correttamente.`);
259
+ setTimeout(() => {
260
+ for (let file of fileDaCancellare) {
261
+ try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
262
+ }
263
+ }, 120000);
276
264
  } else {
277
- node.warn(`[STEP 4.3 - FOTO] Nessuna foto trovata per questa finestra temporale.`);
265
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
278
266
  }
279
- } catch (fotoErr) {
280
- node.error(`[FOTO ERR] Errore durante la ricerca XML: ${fotoErr.message}`);
281
- }
282
267
 
283
- // --- PARTE 3: SPEDIZIONE VERSO PYTHON ---
284
- if (output.foto_base64 || output.video_base64) {
285
- node.warn(`[STEP 5] Spedizione payload multimediale completata!`);
286
- node.send({ payload: output });
287
-
288
- setTimeout(() => {
289
- for (let file of fileDaCancellare) {
290
- try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
291
- }
292
- }, 120000);
293
- } else {
294
- node.warn(`[STEP 5] Nessun file multimediale scaricato, nessun pacchetto inviato.`);
268
+ } catch (e) {
269
+ node.error(`[error] [hik-media-buffer:test] [FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
295
270
  }
296
271
  updateNodeStatus();
297
272
  }
298
273
 
299
- // --- ALERT STREAM ---
274
+ // --- ALERT STREAM ORIGINALE ---
300
275
  function startAlertStream() {
301
276
  if (isClosing) return;
302
277
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
303
278
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
279
+
304
280
  nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
305
281
  .then(response => {
306
282
  streamRequest = response;
307
283
  if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
308
284
  updateNodeStatus();
285
+
309
286
  response.data.on('data', (chunk) => {
287
+ if (isClosing) return;
310
288
  const data = chunk.toString().toLowerCase();
311
289
  if (data.includes("active")) {
312
290
  const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
@@ -331,10 +309,11 @@ module.exports = function(RED) {
331
309
 
332
310
  startAlertStream();
333
311
  setTimeout(checkCameras, 2000);
312
+
334
313
  node.on('close', (done) => {
335
314
  isClosing = true;
336
315
  clearInterval(heartbeatInterval);
337
- if (streamRequest) streamRequest.data.destroy();
316
+ if (streamRequest && streamRequest.data) streamRequest.data.destroy();
338
317
  done();
339
318
  });
340
319
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.57",
3
+ "version": "1.1.59",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",
@@ -22,6 +22,7 @@
22
22
  "dependencies": {
23
23
  "@mhoc/axios-digest-auth": "^0.8.0",
24
24
  "axios": "^1.6.0",
25
+ "node-red-contrib-hik-media-buffer": "^1.1.58",
25
26
  "xml2js": "^0.6.2"
26
27
  }
27
28
  }