node-red-contrib-hik-media-buffer 1.1.70 → 1.1.71

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 +29 -107
  2. package/package.json +1 -1
@@ -32,6 +32,11 @@ module.exports = function(RED) {
32
32
 
33
33
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
34
34
 
35
+ function toHikDate(d) {
36
+ const pad = (num) => String(num).padStart(2, '0');
37
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
38
+ }
39
+
35
40
  async function getCameraName(channelID) {
36
41
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
37
42
  try {
@@ -105,11 +110,8 @@ module.exports = function(RED) {
105
110
 
106
111
  const heartbeatInterval = setInterval(checkCameras, 30000);
107
112
 
108
- // --- STRATEGIA DI DOWNLOAD MULTIMEDIALE 100% JSON + FFMPEG EXTRACTION ---
109
- // --- STRATEGIA DI DOWNLOAD MULTIMEDIALE 100% JSON + FFMPEG EXTRACTION ---
110
- // --- STRATEGIA DI DOWNLOAD MULTIMEDIALE CON DOPPIO FALLBACK ORARIO (LOCALE / UTC) ---
111
113
  async function downloadMedia(evento, channelID) {
112
- node.warn(`[warn] [hik-media-buffer:test] [TRIGGER] Ricevuto evento. Evento: ${evento}, Canale originario: ${channelID}`);
114
+ node.warn(`[TRIGGER] Ricevuto evento. Evento: ${evento}, Canale originario: ${channelID}`);
113
115
 
114
116
  const nowTime = Date.now();
115
117
  if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
@@ -120,36 +122,16 @@ module.exports = function(RED) {
120
122
  const referenceTime = new Date();
121
123
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
122
124
 
123
- // FUNZIONE 1: FORMATTO IN ORA LOCALE (Es. YYYY-MM-DDTHH:mm:ss)
124
- const formatLocaleDate = (d) => {
125
- const pad = (num) => String(num).padStart(2, '0');
126
- return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
127
- };
128
-
129
- // FUNZIONE 2: FORMATTO IN ORA UTC (Es. YYYY-MM-DDTHH:mm:ssZ)
130
- const formatUtcDate = (d) => {
131
- const pad = (num) => String(num).padStart(2, '0');
132
- return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
133
- };
125
+ const startTime = toHikDate(new Date(referenceTime.getTime() - (30 * 1000)));
126
+ const endTime = toHikDate(new Date(referenceTime.getTime() + (5 * 1000)));
134
127
 
135
128
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
136
129
  const nomeCamera = await getCameraName(channelID);
137
130
 
138
- // Aspettiamo 8 secondi per dare tempo all'Hard Disk di stabilizzare il file temporaneo
139
- node.warn(`[warn] [hik-media-buffer:test] Attesa scrittura indice Hard Disk (8s)...`);
140
131
  await new Promise(resolve => setTimeout(resolve, 8000));
141
132
 
142
133
  const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
143
- const evMinuscolo = evento.toLowerCase();
144
-
145
- // CREIAMO LE FINESTRE TEMPORALI: Dal passato fino a ORA (Senza sforare nel futuro!)
146
- const oraLocaleRichiesta = new Date(); // Questo è il momento esatto del termine degli 8s
147
- const startLocale = formatLocaleDate(new Date(oraLocaleRichiesta.getTime() - (90 * 1000))); // 90 secondi fa
148
- const endLocale = formatLocaleDate(oraLocaleRichiesta); // Esattamente ADESSO
149
-
150
- const oraUtcRichiesta = new Date();
151
- const startUtc = formatUtcDate(new Date(oraUtcRichiesta.getTime() - (90 * 1000))); // 90 secondi fa
152
- const endUtc = formatUtcDate(oraUtcRichiesta); // Esattamente ADESSO
134
+ let evMinuscolo = evento.toLowerCase();
153
135
 
154
136
  let output = {
155
137
  tipo_messaggio: "evento",
@@ -165,85 +147,37 @@ module.exports = function(RED) {
165
147
  };
166
148
 
167
149
  const parsedChannel = parseInt(channelID, 10);
168
- let fileDaCancellare = [];
169
- let videoUriMatch = null;
170
-
171
- // --- TENTATIVO 1: RICERCA IN ORA LOCALE (La più probabile per i dischi NVR) ---
172
- const startLocale = formatLocaleDate(new Date(referenceTime.getTime() - (120 * 1000)));
173
- const endLocale = formatLocaleDate(new Date(referenceTime.getTime() + (30 * 1000)));
174
150
 
175
- node.warn(`[warn] [hik-media-buffer:test] [TENTATIVO 1] Ricerca video in Ora Locale: ${startLocale} -> ${endLocale}`);
176
-
177
- const trackVideoId = (parsedChannel * 100) + 1;
178
-
179
- let payloadLocale = {
151
+ // Query JSON Pulita e Blindata (Ancorata al Canale Reale dell'NVR)
152
+ const searchJsonPayload = {
180
153
  "EventSearchDescription": {
181
154
  "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
182
155
  "searchResultPosition": 0,
183
156
  "maxResults": 10,
184
- "timeSpanList": [{"startTime": startLocale, "endTime": endLocale}],
157
+ "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
185
158
  "type": "all",
186
- "channels": [trackVideoId],
159
+ "channels": [parsedChannel],
187
160
  "eventType": "behavior",
188
- "behavior": {"behaviorEventType": evMinuscolo}
161
+ "behavior": { "behaviorEventType": "all" }
189
162
  }
190
163
  };
191
164
 
165
+ let fileDaCancellare = [];
166
+
192
167
  try {
193
- let resLocale = await camAuth.request({
168
+ const resSearchVideo = await camAuth.request({
194
169
  method: 'POST',
195
170
  url: `${baseUrl}/eventRecordSearch?format=json`,
196
- data: payloadLocale,
197
- headers: { "Content-Type": "application/json" },
198
- timeout: 5000
171
+ data: searchJsonPayload,
172
+ headers: { "Content-Type": "application/json" }
199
173
  });
200
- let dataStr = JSON.stringify(resLocale.data);
201
- videoUriMatch = dataStr.match(/rtsp:\/\/[^"]+/i) || dataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
202
- } catch (e) {
203
- node.warn(`[warn] [hik-media-buffer:test] Tentativo 1 fallito o timeout: ${e.message}`);
204
- }
205
174
 
206
- // --- TENTATIVO 2: FALLBACK IN ORA UTC (Se il primo tentativo è andato a vuoto) ---
207
- if (!videoUriMatch) {
208
- const startUtc = formatUtcDate(new Date(referenceTime.getTime() - (120 * 1000)));
209
- const endUtc = formatUtcDate(new Date(referenceTime.getTime() + (30 * 1000)));
210
-
211
- node.warn(`[warn] [hik-media-buffer:test] [TENTATIVO 2] Video locale non trovato. Provo Fallback in Ora UTC: ${startUtc} -> ${endUtc}`);
175
+ const resDataStr = JSON.stringify(resSearchVideo.data);
176
+ const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
212
177
 
213
- let payloadUtc = {
214
- "EventSearchDescription": {
215
- "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
216
- "searchResultPosition": 0,
217
- "maxResults": 10,
218
- "timeSpanList": [{"startTime": startUtc, "endTime": endUtc}],
219
- "type": "all",
220
- "channels": [trackVideoId],
221
- "eventType": "behavior",
222
- "behavior": {"behaviorEventType": evMinuscolo}
223
- }
224
- };
225
-
226
- try {
227
- let resUtc = await camAuth.request({
228
- method: 'POST',
229
- url: `${baseUrl}/eventRecordSearch?format=json`,
230
- data: payloadUtc,
231
- headers: { "Content-Type": "application/json" },
232
- timeout: 5000
233
- });
234
- let dataStr = JSON.stringify(resUtc.data);
235
- videoUriMatch = dataStr.match(/rtsp:\/\/[^"]+/i) || dataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
236
- } catch (e) {
237
- node.error(`[error] [hik-media-buffer:test] Errore critico anche nel Fallback UTC: ${e.message}`);
238
- }
239
- }
240
-
241
- // --- PROCESSO DI SCARICAMENTO SE UNA DELLE DUE QUERIES HA TROVATO IL VIDEO ---
242
- if (videoUriMatch) {
243
- let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
244
- node.warn(`[warn] [hik-media-buffer:test] [FOUND] Trovato playbackURI video: ${rawVideoUri}`);
245
-
246
- try {
178
+ if (videoUriMatch) {
179
+ let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
180
+
247
181
  const resDownVideo = await camAuth.request({
248
182
  method: 'GET',
249
183
  url: `${baseUrl}/download`,
@@ -261,51 +195,39 @@ module.exports = function(RED) {
261
195
  fs.writeFileSync(rawPath, videoBuffer);
262
196
  fileDaCancellare.push(rawPath);
263
197
 
264
- node.warn(`[warn] [hik-media-buffer:test] Esecuzione FFMPEG (Estrazione JPG + FastStart MP4)...`);
265
198
  await new Promise((resolve) => {
266
199
  exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
267
200
  if (!err) {
268
201
  if (fs.existsSync(extractedImgPath)) {
269
202
  output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
270
203
  fileDaCancellare.push(extractedImgPath);
271
- node.warn(`[warn] [hik-media-buffer:test] Snapshot FOTO estratto con successo.`);
272
204
  }
273
205
  if (fs.existsSync(fixedPath)) {
274
206
  output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
275
207
  fileDaCancellare.push(fixedPath);
276
- node.warn(`[warn] [hik-media-buffer:test] Video MP4 indicizzato con successo.`);
277
208
  }
278
209
  } else {
279
- node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, uso file grezzo: ${err.message}`);
280
210
  output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
281
211
  }
282
212
  resolve();
283
213
  });
284
214
  });
285
- } catch (downErr) {
286
- node.error(`[error] [hik-media-buffer:test] Errore durante il download fisico del file: ${downErr.message}`);
287
215
  }
288
- } else {
289
- node.warn(`[warn] [hik-media-buffer:test] [NOT FOUND] Il video non è stato trovato in nessuna delle due finestre temporali (Locale/UTC). Verificare se l'NVR sta effettivamente registrando l'evento.`);
216
+ } catch (videoErr) {
217
+ node.error(`[ERROR] Errore esecuzione: ${videoErr.message}`);
290
218
  }
291
219
 
292
- // --- SPEDIZIONE PAYLOAD VERSO PYTHON ---
293
220
  if (output.foto_base64 || output.video_base64) {
294
- node.warn(`[warn] [hik-media-buffer:test] Spedizione payload multimediale completata con successo!`);
295
221
  node.send({ payload: output });
296
-
297
222
  setTimeout(() => {
298
223
  for (let file of fileDaCancellare) {
299
224
  try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
300
225
  }
301
- }, 120000);
302
- } else {
303
- node.warn(`[warn] [hik-media-buffer:test] Nessun pacchetto inviato a Node-RED.`);
226
+ }, 60000);
304
227
  }
305
228
  updateNodeStatus();
306
229
  }
307
230
 
308
- // --- ALERT STREAM ---
309
231
  function startAlertStream() {
310
232
  if (isClosing) return;
311
233
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
@@ -314,7 +236,7 @@ module.exports = function(RED) {
314
236
  nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
315
237
  .then(response => {
316
238
  streamRequest = response;
317
- if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
239
+ if (!nvrOnline) { nvrOnline = true; }
318
240
  updateNodeStatus();
319
241
 
320
242
  response.data.on('data', (chunk) => {
@@ -336,7 +258,7 @@ module.exports = function(RED) {
336
258
  }
337
259
 
338
260
  function handleNvrError() {
339
- if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
261
+ nvrOnline = false;
340
262
  updateNodeStatus();
341
263
  if (!isClosing) setTimeout(startAlertStream, 10000);
342
264
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.70",
3
+ "version": "1.1.71",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",