node-red-contrib-hik-media-buffer 1.1.54 → 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 +143 -109
  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,42 +118,42 @@ 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
 
113
125
  const nowTime = Date.now();
114
126
  if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
115
- if (nowTime - lastTriggerTime[channelID] < 5000) {
116
- node.warn(`[STEP 1] Richiesta bloccata dall'antirimbalzo (meno di 5s dall'ultimo trigger)`);
117
- return;
118
- }
127
+ if (nowTime - lastTriggerTime[channelID] < 5000) return;
119
128
  lastTriggerTime[channelID] = nowTime;
120
129
 
121
130
  const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
122
131
  const referenceTime = new Date();
123
132
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
124
133
 
125
- // Ripristinato toHikDate identico a prima (con la Z / UTC che andava)
126
- const startTime = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
127
- const endTime = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
128
- node.warn(`[STEP 2] Finestra temporale impostata: ${startTime} -> ${endTime}`);
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)));
129
145
 
130
146
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
131
147
  const nomeCamera = await getCameraName(channelID);
132
148
 
133
- 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...`);
134
150
  await new Promise(resolve => setTimeout(resolve, 6000));
135
151
 
136
152
  const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
137
153
 
138
- let eventoNVR = "FieldDetection";
139
- for (let e of EventList) {
140
- if (evento.toLowerCase() === e.toLowerCase()) {
141
- eventoNVR = e;
142
- break;
143
- }
144
- }
154
+ const eventoMinuscolo = evento.toLowerCase();
155
+ let eventoNVR = "FieldDetection";
156
+ if (eventoMinuscolo.includes("linedetection")) eventoNVR = "LineDetection";
145
157
 
146
158
  let output = {
147
159
  tipo_messaggio: "evento",
@@ -157,112 +169,134 @@ module.exports = function(RED) {
157
169
  };
158
170
 
159
171
  const parsedChannel = parseInt(channelID, 10);
160
- const trackVideo = (parsedChannel * 100) + 1;
161
- const trackFoto = (parsedChannel * 100) + 3;
162
-
163
- const tracks = [
164
- { id: trackVideo.toString(), isFoto: false, label: "VIDEO" },
165
- { id: trackFoto.toString(), isFoto: true, label: "FOTO" }
166
- ];
172
+ const trackFoto = (parsedChannel * 100) + 3; // Es. Canale 2 -> 203
167
173
  let fileDaCancellare = [];
168
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
+
169
195
  try {
170
- for (let t of tracks) {
171
- node.warn(`[STEP 4] Elaborazione traccia ${t.label} (ID: ${t.id})`);
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}`);
172
209
 
173
- // RIPRISTINATO: XML identico riga per riga a quello vecchio della telecamera,
174
- // ma usando la stringa di metadato corretta dell'NVR (recordType.meta.hikvision.com)
175
- 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.hikvision.com/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
176
-
177
- node.warn(`[STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
178
-
179
- try {
180
- const resSearch = await camAuth.request({
181
- method: 'POST',
182
- url: `${baseUrl}/search`,
183
- data: searchXml,
184
- headers: { "Content-Type": "application/xml" }
185
- });
186
-
187
- node.warn(`[STEP 4.2] Risposta ricevuta da /search per traccia ${t.id}: Status ${resSearch.status}`);
188
-
189
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
190
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
191
-
192
- if (uriMatch) {
193
- const rawUri = uriMatch[1].replace(/&amp;/g, '&');
194
- node.warn(`[STEP 5] Trovato playbackURI per traccia ${t.id}: ${rawUri}`);
195
-
196
- node.warn(`[STEP 5.1] Invio richiesta GET /download per traccia ${t.id}`);
197
- const resDown = await camAuth.request({
198
- method: 'GET',
199
- url: `${baseUrl}/download`,
200
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
201
- responseType: 'arraybuffer'
202
- });
203
-
204
- node.warn(`[STEP 5.2] Scaricati ${resDown.data.byteLength} byte per traccia ${t.id}`);
205
- let buffer = Buffer.from(resDown.data);
206
-
207
- if (t.isFoto) {
208
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
209
- fs.writeFileSync(fullImgPath, buffer);
210
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
211
- fileDaCancellare.push(fullImgPath);
212
- node.warn(`[STEP 5.3] Immagine salvata e convertita in Base64.`);
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
+ });
216
+
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.`);
213
230
  } else {
214
- if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
215
- const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
216
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
217
- fs.writeFileSync(rawPath, buffer);
218
-
219
- node.warn(`[STEP 5.3] Video grezzo salvato. Avvio conversione FFMPEG...`);
220
- await new Promise((resolve) => {
221
- exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
222
- if (!err && fs.existsSync(fixedPath)) {
223
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
224
- fileDaCancellare.push(fixedPath);
225
- node.warn(`[STEP 5.4] Conversione FFMPEG completata con successo.`);
226
- } else {
227
- node.warn(`[STEP 5.4] Errore FFMPEG (uso il video grezzo di backup): ${err ? err.message : 'File mancante'}`);
228
- output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
229
- }
230
- fileDaCancellare.push(rawPath);
231
- resolve();
232
- });
233
- });
231
+ output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
234
232
  }
235
- } else {
236
- node.warn(`[STEP 4.3] Nessun playbackURI trovato nel documento di risposta XML per traccia ${t.id}. XML di risposta:\n${resSearch.data}`);
237
- }
238
- } catch (searchError) {
239
- let errorDetails = searchError.message;
240
- if (searchError.response) {
241
- errorDetails += ` | Status: ${searchError.response.status} | Dati Risposta: ${JSON.stringify(searchError.response.data)}`;
242
- }
243
- node.error(`[CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
244
- }
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.`);
245
239
  }
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>`;
246
248
 
247
- if (output.foto_base64 || output.video_base64) {
248
- node.warn(`[STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
249
- node.send({ payload: output });
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}`);
250
263
 
251
- setTimeout(() => {
252
- for (let file of fileDaCancellare) {
253
- try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
254
- }
255
- }, 120000);
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.`);
256
276
  } else {
257
- node.warn(`[STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
277
+ node.warn(`[STEP 4.3 - FOTO] Nessuna foto trovata per questa finestra temporale.`);
258
278
  }
279
+ } catch (fotoErr) {
280
+ node.error(`[FOTO ERR] Errore durante la ricerca XML: ${fotoErr.message}`);
281
+ }
259
282
 
260
- } catch (e) {
261
- node.error(`[FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
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.`);
262
295
  }
263
- node.status({fill:"green", shape:"ring", text:"In ascolto"});
296
+ updateNodeStatus();
264
297
  }
265
298
 
299
+ // --- ALERT STREAM ---
266
300
  function startAlertStream() {
267
301
  if (isClosing) return;
268
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.54",
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",