node-red-contrib-hik-media-buffer 1.1.49 → 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 -59
  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,24 +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
- // FISSO: Corretto il nome della variabile bloccoContenuto (prima c'era un typo che rompeva il nodo)
82
70
  const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
83
71
  const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i);
84
72
 
85
73
  if (idMatch && statusMatch) {
86
74
  const ch = idMatch[1];
87
75
  const statusStr = statusMatch[1].toLowerCase();
88
-
89
76
  const isOnline = statusStr.includes("online") || statusStr.includes("recording");
90
77
 
91
78
  if (isOnline && statoCanale[ch] === false) {
@@ -119,25 +106,32 @@ module.exports = function(RED) {
119
106
 
120
107
  const heartbeatInterval = setInterval(checkCameras, 30000);
121
108
 
122
- // --- DOWNLOAD MULTIMEDIALE BASATO SULL'ISPEZIONA REALE DELL'NVR ---
109
+ // --- DOWNLOAD CON DEBUG COPIOSO ---
123
110
  async function downloadMedia(evento, channelID) {
111
+ node.warn(`[STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
112
+
124
113
  const nowTime = Date.now();
125
114
  if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
126
- 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
+ }
127
119
  lastTriggerTime[channelID] = nowTime;
128
120
 
129
121
  const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
130
-
131
122
  const referenceTime = new Date();
132
123
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
133
124
 
134
125
  const startTime = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
135
126
  const endTime = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
127
+ node.warn(`[STEP 2] Finestra temporale impostata: ${startTime} -> ${endTime}`);
136
128
 
137
129
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
138
130
  const nomeCamera = await getCameraName(channelID);
139
131
 
132
+ node.warn(`[STEP 3] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
140
133
  await new Promise(resolve => setTimeout(resolve, 6000));
134
+
141
135
  const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
142
136
 
143
137
  let eventoNVR = "FieldDetection";
@@ -162,65 +156,95 @@ module.exports = function(RED) {
162
156
  };
163
157
 
164
158
  const parsedChannel = parseInt(channelID, 10);
165
- const trackVideo = (parsedChannel * 100) + 1; // Canale 2 -> 201
166
- const trackFoto = (parsedChannel * 100) + 3; // Canale 2 -> 203
159
+ const trackVideo = (parsedChannel * 100) + 1;
160
+ const trackFoto = (parsedChannel * 100) + 3;
167
161
 
168
162
  const tracks = [
169
- { id: trackVideo.toString(), isFoto: false },
170
- { id: trackFoto.toString(), isFoto: true }
163
+ { id: trackVideo.toString(), isFoto: false, label: "VIDEO" },
164
+ { id: trackFoto.toString(), isFoto: true, label: "FOTO" }
171
165
  ];
172
166
  let fileDaCancellare = [];
173
167
 
174
168
  try {
175
169
  for (let t of tracks) {
176
- // FISSO: Logica XML clonata al 100% dal tuo ispeziona NVR reale
170
+ node.warn(`[STEP 4] Elaborazione traccia ${t.label} (ID: ${t.id})`);
171
+
172
+ // Ripristiniamo l'XML puro clonato dal tuo ispeziona immagini per traccia 203
177
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>`;
178
174
 
179
- const resSearch = await camAuth.request({
180
- method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
181
- });
182
-
183
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
184
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
175
+ node.warn(`[STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
185
176
 
186
- if (uriMatch) {
187
- const rawUri = uriMatch[1].replace(/&amp;/g, '&');
188
- const resDown = await camAuth.request({
189
- method: 'GET',
190
- url: `${baseUrl}/download`,
191
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
192
- 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" }
193
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>([^<]+)</);
194
189
 
195
- let buffer = Buffer.from(resDown.data);
196
- if (t.isFoto) {
197
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
198
- fs.writeFileSync(fullImgPath, buffer);
199
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
200
- fileDaCancellare.push(fullImgPath);
201
- } else {
202
- if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
203
- const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
204
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
205
- 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}`);
206
193
 
207
- await new Promise((resolve) => {
208
- exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
209
- if (!err && fs.existsSync(fixedPath)) {
210
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
211
- fileDaCancellare.push(fixedPath);
212
- } else {
213
- output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
214
- }
215
- fileDaCancellare.push(rawPath);
216
- resolve();
217
- });
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'
218
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)}`;
219
241
  }
242
+ node.error(`[CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
220
243
  }
221
244
  }
222
245
 
223
246
  if (output.foto_base64 || output.video_base64) {
247
+ node.warn(`[STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
224
248
  node.send({ payload: output });
225
249
 
226
250
  setTimeout(() => {
@@ -228,15 +252,16 @@ module.exports = function(RED) {
228
252
  try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
229
253
  }
230
254
  }, 120000);
255
+ } else {
256
+ node.warn(`[STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
231
257
  }
232
258
 
233
259
  } catch (e) {
234
- node.error(`Errore Download Cam ${channelID}: ${e.message}`);
260
+ node.error(`[FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
235
261
  }
236
262
  updateNodeStatus();
237
263
  }
238
264
 
239
- // --- ALERT STREAM ---
240
265
  function startAlertStream() {
241
266
  if (isClosing) return;
242
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.49",
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",