node-red-contrib-hik-media-buffer 1.1.69 → 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 -98
  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,27 +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. 2026-06-09T14:30:00)
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. 2026-06-09T12:30:00Z)
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 scrivere l'indice
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();
134
+ let evMinuscolo = evento.toLowerCase();
144
135
 
145
136
  let output = {
146
137
  tipo_messaggio: "evento",
@@ -156,85 +147,37 @@ module.exports = function(RED) {
156
147
  };
157
148
 
158
149
  const parsedChannel = parseInt(channelID, 10);
159
- let fileDaCancellare = [];
160
- let videoUriMatch = null;
161
-
162
- // --- TENTATIVO 1: RICERCA IN ORA LOCALE (La più probabile per i dischi NVR) ---
163
- const startLocale = formatLocaleDate(new Date(referenceTime.getTime() - (120 * 1000)));
164
- const endLocale = formatLocaleDate(new Date(referenceTime.getTime() + (30 * 1000)));
165
150
 
166
- node.warn(`[warn] [hik-media-buffer:test] [TENTATIVO 1] Ricerca video in Ora Locale: ${startLocale} -> ${endLocale}`);
167
-
168
- const trackVideoId = (parsedChannel * 100) + 1;
169
-
170
- let payloadLocale = {
151
+ // Query JSON Pulita e Blindata (Ancorata al Canale Reale dell'NVR)
152
+ const searchJsonPayload = {
171
153
  "EventSearchDescription": {
172
154
  "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
173
155
  "searchResultPosition": 0,
174
156
  "maxResults": 10,
175
- "timeSpanList": [{"startTime": startLocale, "endTime": endLocale}],
157
+ "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
176
158
  "type": "all",
177
- "channels": [trackVideoId],
159
+ "channels": [parsedChannel],
178
160
  "eventType": "behavior",
179
- "behavior": {"behaviorEventType": evMinuscolo}
161
+ "behavior": { "behaviorEventType": "all" }
180
162
  }
181
163
  };
182
164
 
165
+ let fileDaCancellare = [];
166
+
183
167
  try {
184
- let resLocale = await camAuth.request({
168
+ const resSearchVideo = await camAuth.request({
185
169
  method: 'POST',
186
170
  url: `${baseUrl}/eventRecordSearch?format=json`,
187
- data: payloadLocale,
188
- headers: { "Content-Type": "application/json" },
189
- timeout: 5000
171
+ data: searchJsonPayload,
172
+ headers: { "Content-Type": "application/json" }
190
173
  });
191
- let dataStr = JSON.stringify(resLocale.data);
192
- videoUriMatch = dataStr.match(/rtsp:\/\/[^"]+/i) || dataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
193
- } catch (e) {
194
- node.warn(`[warn] [hik-media-buffer:test] Tentativo 1 fallito o timeout: ${e.message}`);
195
- }
196
174
 
197
- // --- TENTATIVO 2: FALLBACK IN ORA UTC (Se il primo tentativo è andato a vuoto) ---
198
- if (!videoUriMatch) {
199
- const startUtc = formatUtcDate(new Date(referenceTime.getTime() - (120 * 1000)));
200
- const endUtc = formatUtcDate(new Date(referenceTime.getTime() + (30 * 1000)));
175
+ const resDataStr = JSON.stringify(resSearchVideo.data);
176
+ const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
201
177
 
202
- node.warn(`[warn] [hik-media-buffer:test] [TENTATIVO 2] Video locale non trovato. Provo Fallback in Ora UTC: ${startUtc} -> ${endUtc}`);
203
-
204
- let payloadUtc = {
205
- "EventSearchDescription": {
206
- "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
207
- "searchResultPosition": 0,
208
- "maxResults": 10,
209
- "timeSpanList": [{"startTime": startUtc, "endTime": endUtc}],
210
- "type": "all",
211
- "channels": [trackVideoId],
212
- "eventType": "behavior",
213
- "behavior": {"behaviorEventType": evMinuscolo}
214
- }
215
- };
216
-
217
- try {
218
- let resUtc = await camAuth.request({
219
- method: 'POST',
220
- url: `${baseUrl}/eventRecordSearch?format=json`,
221
- data: payloadUtc,
222
- headers: { "Content-Type": "application/json" },
223
- timeout: 5000
224
- });
225
- let dataStr = JSON.stringify(resUtc.data);
226
- videoUriMatch = dataStr.match(/rtsp:\/\/[^"]+/i) || dataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
227
- } catch (e) {
228
- node.error(`[error] [hik-media-buffer:test] Errore critico anche nel Fallback UTC: ${e.message}`);
229
- }
230
- }
231
-
232
- // --- PROCESSO DI SCARICAMENTO SE UNA DELLE DUE QUERIES HA TROVATO IL VIDEO ---
233
- if (videoUriMatch) {
234
- let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
235
- node.warn(`[warn] [hik-media-buffer:test] [FOUND] Trovato playbackURI video: ${rawVideoUri}`);
236
-
237
- try {
178
+ if (videoUriMatch) {
179
+ let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
180
+
238
181
  const resDownVideo = await camAuth.request({
239
182
  method: 'GET',
240
183
  url: `${baseUrl}/download`,
@@ -252,51 +195,39 @@ module.exports = function(RED) {
252
195
  fs.writeFileSync(rawPath, videoBuffer);
253
196
  fileDaCancellare.push(rawPath);
254
197
 
255
- node.warn(`[warn] [hik-media-buffer:test] Esecuzione FFMPEG (Estrazione JPG + FastStart MP4)...`);
256
198
  await new Promise((resolve) => {
257
199
  exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
258
200
  if (!err) {
259
201
  if (fs.existsSync(extractedImgPath)) {
260
202
  output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
261
203
  fileDaCancellare.push(extractedImgPath);
262
- node.warn(`[warn] [hik-media-buffer:test] Snapshot FOTO estratto con successo.`);
263
204
  }
264
205
  if (fs.existsSync(fixedPath)) {
265
206
  output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
266
207
  fileDaCancellare.push(fixedPath);
267
- node.warn(`[warn] [hik-media-buffer:test] Video MP4 indicizzato con successo.`);
268
208
  }
269
209
  } else {
270
- node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, uso file grezzo: ${err.message}`);
271
210
  output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
272
211
  }
273
212
  resolve();
274
213
  });
275
214
  });
276
- } catch (downErr) {
277
- node.error(`[error] [hik-media-buffer:test] Errore durante il download fisico del file: ${downErr.message}`);
278
215
  }
279
- } else {
280
- 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}`);
281
218
  }
282
219
 
283
- // --- SPEDIZIONE PAYLOAD VERSO PYTHON ---
284
220
  if (output.foto_base64 || output.video_base64) {
285
- node.warn(`[warn] [hik-media-buffer:test] Spedizione payload multimediale completata con successo!`);
286
221
  node.send({ payload: output });
287
-
288
222
  setTimeout(() => {
289
223
  for (let file of fileDaCancellare) {
290
224
  try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
291
225
  }
292
- }, 120000);
293
- } else {
294
- node.warn(`[warn] [hik-media-buffer:test] Nessun pacchetto inviato a Node-RED.`);
226
+ }, 60000);
295
227
  }
296
228
  updateNodeStatus();
297
229
  }
298
230
 
299
- // --- ALERT STREAM ---
300
231
  function startAlertStream() {
301
232
  if (isClosing) return;
302
233
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
@@ -305,7 +236,7 @@ module.exports = function(RED) {
305
236
  nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
306
237
  .then(response => {
307
238
  streamRequest = response;
308
- 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; }
309
240
  updateNodeStatus();
310
241
 
311
242
  response.data.on('data', (chunk) => {
@@ -327,7 +258,7 @@ module.exports = function(RED) {
327
258
  }
328
259
 
329
260
  function handleNvrError() {
330
- 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;
331
262
  updateNodeStatus();
332
263
  if (!isClosing) setTimeout(startAlertStream, 10000);
333
264
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.69",
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",