node-red-contrib-hik-media-buffer 1.1.50 → 1.1.51

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 +84 -65
  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; // IP dell'NVR centrale
15
+ node.host = config.host;
16
16
  node.port = config.port || "80";
17
17
  node.protocol = config.protocol || "http";
18
18
  node.user = config.user;
@@ -25,11 +25,7 @@ module.exports = function(RED) {
25
25
  let statoCanale = {};
26
26
 
27
27
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
28
-
29
- // La tua EventList originale per l'alertStream
30
28
  const EventList = ["FieldDetection", "LineDetection"];
31
-
32
- // CARTELLA TEMPORANEA
33
29
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
34
30
  if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
35
31
 
@@ -37,7 +33,6 @@ module.exports = function(RED) {
37
33
 
38
34
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
39
35
 
40
- // --- PRENDE IL NOME DEL CANALE DIRETTAMENTE DALL'NVR ---
41
36
  async function getCameraName(channelID) {
42
37
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
43
38
  try {
@@ -47,7 +42,6 @@ module.exports = function(RED) {
47
42
  timeout: 5000,
48
43
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
49
44
  });
50
-
51
45
  const data = res.data.toString();
52
46
  const match = data.match(/<name>([^<]+)<\/name>/i);
53
47
  return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
@@ -56,11 +50,9 @@ module.exports = function(RED) {
56
50
  }
57
51
  }
58
52
 
59
- // --- CONTROLLO STATUS DINAMICO DEI CANALI DIGITALI DALL'NVR ---
60
53
  async function checkCameras() {
61
54
  if (isClosing) return;
62
55
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
63
-
64
56
  try {
65
57
  const res = await nvrAuth.request({
66
58
  method: 'GET',
@@ -68,23 +60,19 @@ module.exports = function(RED) {
68
60
  timeout: 5000,
69
61
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
70
62
  });
71
-
72
63
  nvrOnline = true;
73
64
  const xmlData = res.data.toString();
74
-
75
65
  const bloccoRegex = /<InputProxyChannelStatus[\s\\S]*?>([\s\S]*?)<\/InputProxyChannelStatus>/gi;
76
66
  let matchBlocco;
77
67
 
78
68
  while ((matchBlocco = bloccoRegex.exec(xmlData)) !== null) {
79
69
  const bloccoContenuto = matchBlocco[1];
80
-
81
70
  const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
82
71
  const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i);
83
72
 
84
73
  if (idMatch && statusMatch) {
85
74
  const ch = idMatch[1];
86
75
  const statusStr = statusMatch[1].toLowerCase();
87
-
88
76
  const isOnline = statusStr.includes("online") || statusStr.includes("recording");
89
77
 
90
78
  if (isOnline && statoCanale[ch] === false) {
@@ -118,25 +106,32 @@ module.exports = function(RED) {
118
106
 
119
107
  const heartbeatInterval = setInterval(checkCameras, 30000);
120
108
 
121
- // --- DOWNLOAD MULTIMEDIALE CON DIFFERENZIAZIONE XML VIDEO/FOTO ---
109
+ // --- DOWNLOAD CON DEBUG COPIOSO ---
122
110
  async function downloadMedia(evento, channelID) {
111
+ node.warn(`[STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
112
+
123
113
  const nowTime = Date.now();
124
114
  if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
125
- if (nowTime - lastTriggerTime[channelID] < 5000) return;
115
+ if (nowTime - lastTriggerTime[channelID] < 5000) {
116
+ node.warn(`[STEP 1] Richiesta bloccata dall'antirimbalzo (meno di 5s dall'ultimo trigger)`);
117
+ return;
118
+ }
126
119
  lastTriggerTime[channelID] = nowTime;
127
120
 
128
121
  const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
129
-
130
122
  const referenceTime = new Date();
131
123
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
132
124
 
133
125
  const startTime = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
134
126
  const endTime = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
127
+ node.warn(`[STEP 2] Finestra temporale impostata: ${startTime} -> ${endTime}`);
135
128
 
136
129
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
137
130
  const nomeCamera = await getCameraName(channelID);
138
131
 
132
+ node.warn(`[STEP 3] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
139
133
  await new Promise(resolve => setTimeout(resolve, 6000));
134
+
140
135
  const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
141
136
 
142
137
  let eventoNVR = "FieldDetection";
@@ -161,72 +156,95 @@ module.exports = function(RED) {
161
156
  };
162
157
 
163
158
  const parsedChannel = parseInt(channelID, 10);
164
- const trackVideo = (parsedChannel * 100) + 1; // Canale 2 -> 201
165
- const trackFoto = (parsedChannel * 100) + 3; // Canale 2 -> 203
159
+ const trackVideo = (parsedChannel * 100) + 1;
160
+ const trackFoto = (parsedChannel * 100) + 3;
166
161
 
167
162
  const tracks = [
168
- { id: trackVideo.toString(), isFoto: false },
169
- { id: trackFoto.toString(), isFoto: true }
163
+ { id: trackVideo.toString(), isFoto: false, label: "VIDEO" },
164
+ { id: trackFoto.toString(), isFoto: true, label: "FOTO" }
170
165
  ];
171
166
  let fileDaCancellare = [];
172
167
 
173
168
  try {
174
169
  for (let t of tracks) {
175
- let searchXml = "";
170
+ node.warn(`[STEP 4] Elaborazione traccia ${t.label} (ID: ${t.id})`);
176
171
 
177
- if (t.isFoto) {
178
- // XML CLONATO DALL'ISPEZIONA IMMAGINI: Per cercare le foto (metadata)
179
- 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><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
180
- } else {
181
- // XML ADATTATO PER I VIDEO: Cambiato il contentType in video per non mandare l'NVR in errore 400
182
- 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><contentTypeList><contentType>video</contentType></contentTypeList><maxResults>30</maxResults><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
183
- }
172
+ // Ripristiniamo l'XML puro clonato dal tuo ispeziona immagini per traccia 203
173
+ 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><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
184
174
 
185
- const resSearch = await camAuth.request({
186
- method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
187
- });
188
-
189
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
190
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
175
+ node.warn(`[STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
191
176
 
192
- if (uriMatch) {
193
- const rawUri = uriMatch[1].replace(/&amp;/g, '&');
194
- const resDown = await camAuth.request({
195
- method: 'GET',
196
- url: `${baseUrl}/download`,
197
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
198
- responseType: 'arraybuffer'
177
+ try {
178
+ const resSearch = await camAuth.request({
179
+ method: 'POST',
180
+ url: `${baseUrl}/search`,
181
+ data: searchXml,
182
+ headers: { "Content-Type": "application/xml" }
199
183
  });
184
+
185
+ node.warn(`[STEP 4.2] Risposta ricevuta da /search per traccia ${t.id}: Riposta Ricevuta (Status ${resSearch.status})`);
186
+
187
+ let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
188
+ const uriMatch = xml.match(/<playbackURI>([^<]+)</);
200
189
 
201
- let buffer = Buffer.from(resDown.data);
202
- if (t.isFoto) {
203
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
204
- fs.writeFileSync(fullImgPath, buffer);
205
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
206
- fileDaCancellare.push(fullImgPath);
207
- } else {
208
- if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
209
- const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
210
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
211
- fs.writeFileSync(rawPath, buffer);
190
+ if (uriMatch) {
191
+ const rawUri = uriMatch[1].replace(/&amp;/g, '&');
192
+ node.warn(`[STEP 5] Trovato playbackURI per traccia ${t.id}: ${rawUri}`);
212
193
 
213
- await new Promise((resolve) => {
214
- exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
215
- if (!err && fs.existsSync(fixedPath)) {
216
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
217
- fileDaCancellare.push(fixedPath);
218
- } else {
219
- output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
220
- }
221
- fileDaCancellare.push(rawPath);
222
- resolve();
223
- });
194
+ node.warn(`[STEP 5.1] Invio richiesta GET /download per traccia ${t.id}`);
195
+ const resDown = await camAuth.request({
196
+ method: 'GET',
197
+ url: `${baseUrl}/download`,
198
+ data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
199
+ responseType: 'arraybuffer'
224
200
  });
201
+
202
+ node.warn(`[STEP 5.2] Scaricati ${resDown.data.byteLength} byte per traccia ${t.id}`);
203
+ let buffer = Buffer.from(resDown.data);
204
+
205
+ if (t.isFoto) {
206
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
207
+ fs.writeFileSync(fullImgPath, buffer);
208
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
209
+ fileDaCancellare.push(fullImgPath);
210
+ node.warn(`[STEP 5.3] Immagine salvata e convertita in Base64.`);
211
+ } else {
212
+ if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
213
+ const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
214
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
215
+ fs.writeFileSync(rawPath, buffer);
216
+
217
+ node.warn(`[STEP 5.3] Video grezzo salvato. Avvio conversione FFMPEG...`);
218
+ await new Promise((resolve) => {
219
+ exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
220
+ if (!err && fs.existsSync(fixedPath)) {
221
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
222
+ fileDaCancellare.push(fixedPath);
223
+ node.warn(`[STEP 5.4] Conversione FFMPEG completata con successo.`);
224
+ } else {
225
+ node.warn(`[STEP 5.4] Errore FFMPEG (uso il video grezzo di backup): ${err ? err.message : 'File mancante'}`);
226
+ output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
227
+ }
228
+ fileDaCancellare.push(rawPath);
229
+ resolve();
230
+ });
231
+ });
232
+ }
233
+ } else {
234
+ node.warn(`[STEP 4.3] Nessun playbackURI trovato nel documento di risposta XML per traccia ${t.id}. XML di risposta:\n${resSearch.data}`);
235
+ }
236
+ } catch (searchError) {
237
+ // CATTURA DETTAGLIATA DELL'ERRORE 400
238
+ let errorDetails = searchError.message;
239
+ if (searchError.response) {
240
+ errorDetails += ` | Status: ${searchError.response.status} | Dati Risposta: ${JSON.stringify(searchError.response.data)}`;
225
241
  }
242
+ node.error(`[CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
226
243
  }
227
244
  }
228
245
 
229
246
  if (output.foto_base64 || output.video_base64) {
247
+ node.warn(`[STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
230
248
  node.send({ payload: output });
231
249
 
232
250
  setTimeout(() => {
@@ -234,15 +252,16 @@ module.exports = function(RED) {
234
252
  try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
235
253
  }
236
254
  }, 120000);
255
+ } else {
256
+ node.warn(`[STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
237
257
  }
238
258
 
239
259
  } catch (e) {
240
- node.error(`Errore Download Cam ${channelID}: ${e.message}`);
260
+ node.error(`[FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
241
261
  }
242
262
  updateNodeStatus();
243
263
  }
244
264
 
245
- // --- ALERT STREAM ---
246
265
  function startAlertStream() {
247
266
  if (isClosing) return;
248
267
  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.50",
3
+ "version": "1.1.51",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",