node-red-contrib-hik-media-buffer 1.1.99 → 1.1.101

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 +59 -61
  2. package/package.json +1 -1
@@ -28,7 +28,7 @@ module.exports = function(RED) {
28
28
 
29
29
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
30
30
 
31
- // Formatta la data nel formato ISO locale richiesto dal JSON dell'NVR (Es: 2026-06-22T14:41:00+02:00)
31
+ // Formatta la data simulando il fuso orario senza il segno '+'
32
32
  function toJsonDate(d) {
33
33
  const pad = (num) => String(num).padStart(2, '0');
34
34
  const year = d.getFullYear();
@@ -39,17 +39,16 @@ module.exports = function(RED) {
39
39
  const seconds = pad(d.getSeconds());
40
40
 
41
41
  const offsetMinutes = d.getTimezoneOffset();
42
- const offsetSign = offsetMinutes <= 0 ? '+' : '-';
43
- const absOffsetMinutes = Math.abs(offsetMinutes);
44
- const offsetHours = pad(Math.floor(absOffsetMinutes / 60));
45
- const offsetMins = pad(absOffsetMinutes % 60);
42
+ const offsetSign = offsetMinutes <= 0 ? '' : '-';
43
+ const offsetHours = pad(Math.floor(Math.abs(offsetMinutes) / 60));
44
+ const offsetMins = pad(Math.abs(offsetMinutes) % 60);
45
+ const fusoOrario = `${offsetSign}${offsetHours}:${offsetMins}`;
46
46
 
47
- return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMins}`;
47
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds} ${fusoOrario}`;
48
48
  }
49
49
 
50
50
  const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
51
51
 
52
- // --- RECUPERA IL NOME DELLA TELECAMERA ---
53
52
  async function getCameraName(channelID) {
54
53
  try {
55
54
  const res = await nvrAuthAxios.request({
@@ -63,7 +62,6 @@ module.exports = function(RED) {
63
62
  } catch (e) { return `Camera_${channelID}`; }
64
63
  }
65
64
 
66
- // --- CONTROLLO STATUS TELECAMERE CONNESSE ALL'NVR ---
67
65
  async function checkCameras() {
68
66
  if (isClosing || !nvrOnline) return;
69
67
  try {
@@ -106,7 +104,7 @@ module.exports = function(RED) {
106
104
 
107
105
  const heartbeatInterval = setInterval(checkCameras, 30000);
108
106
 
109
- // --- CORE RESCUE: RICERCA EVENTO JSON, CATTURA FOTO HTTP E VIDEO RTSP ---
107
+ // --- CORE: RICERCA ARCHIVIO GIORNALIERO SIMULANDO CHROME ---
110
108
  async function downloadMedia(evento, channelID) {
111
109
  const chStr = channelID.toString();
112
110
  const nowTime = Date.now();
@@ -117,23 +115,26 @@ module.exports = function(RED) {
117
115
  const nomeCamera = await getCameraName(channelID);
118
116
  const referenceTime = new Date();
119
117
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
120
-
121
- // Forziamo l'azzeramento totale dei secondi (:00 e :59) come vuole il database Hikvision
122
- const dStart = new Date(referenceTime.getTime() - (5 * 60 * 1000)); // 5 minuti fa
123
- dStart.setSeconds(0, 0); // Forza :00 secondi e 0 millisecondi
124
-
125
- const dEnd = new Date(referenceTime.getTime() + (2 * 60 * 1000)); // 2 minuti nel futuro
126
- dEnd.setSeconds(59, 999); // Forza :59 secondi
127
118
 
128
- const startTime = toJsonDate(dStart);
129
- const endTime = toJsonDate(dEnd);
119
+ // Generazione dell'intervallo per l'intera giornata corrente
120
+ const pad = (num) => String(num).padStart(2, '0');
121
+ const year = referenceTime.getFullYear();
122
+ const month = pad(referenceTime.getMonth() + 1);
123
+ const day = pad(referenceTime.getDate());
124
+
125
+ const offsetMinutes = referenceTime.getTimezoneOffset();
126
+ const offsetSign = offsetMinutes <= 0 ? '' : '-';
127
+ const offsetHours = pad(Math.floor(Math.abs(offsetMinutes) / 60));
128
+ const offsetMins = pad(Math.abs(offsetMinutes) % 60);
129
+ const fusoOrario = `${offsetSign}${offsetHours}:${offsetMins}`;
130
130
 
131
- node.warn(`[DEBUG TEMPO SANIFICATO] Cerco da: ${startTime} a: ${endTime}`);
131
+ const startTime = `${year}-${month}-${day}T00:00:00 ${fusoOrario}`;
132
+ const endTime = `${year}-${month}-${day}T23:59:59 ${fusoOrario}`;
132
133
 
133
- node.status({fill:"yellow", shape:"dot", text:`Ricerca Evento Cam ${channelID}...`});
134
+ node.status({fill:"yellow", shape:"dot", text:`Ricerca archivio Cam ${channelID}...`});
134
135
 
135
- // Aspettiamo 3 secondi per dare tempo all'NVR di indicizzare l'evento sul log
136
- await new Promise(resolve => setTimeout(resolve, 6000));
136
+ // Aspettiamo 4 secondi per dare tempo all'NVR di indicizzare il log su disco
137
+ await new Promise(resolve => setTimeout(resolve, 4000));
137
138
 
138
139
  let output = {
139
140
  tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
@@ -144,13 +145,13 @@ module.exports = function(RED) {
144
145
  let fileDaCancellare = [];
145
146
 
146
147
  try {
147
- // Ripristiniamo il SearchID dinamico
148
+ // SearchID UUID Dinamico ad ogni sessione
148
149
  const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
149
150
  var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
150
151
  return v.toString(16).toUpperCase();
151
152
  });
152
153
 
153
- // Ricostruiamo il JSON IDENTICO a quello intercettato su Chrome che funzionava
154
+ // Payload clonato dal Form Data di Chrome
154
155
  const searchPayload = {
155
156
  "EventSearchDescription": {
156
157
  "searchID": dynamicSearchId,
@@ -158,19 +159,20 @@ module.exports = function(RED) {
158
159
  "maxResults": 30,
159
160
  "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
160
161
  "type": "all",
161
- "channels": [ parseInt(channelID) ], // Ritorniamo al canale puro (es. 2)
162
+ "channels": [ parseInt(channelID) ],
162
163
  "eventType": "behavior",
163
- "behavior": {
164
- "behaviorEventType": evento.toLowerCase() // Spara "linedetection" o "fielddetection"
165
- }
164
+ "behavior": { "behaviorEventType": evento.toLowerCase() }
166
165
  }
167
166
  };
167
+
168
168
  const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
169
169
  fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
170
170
 
171
- node.warn(`[DEBUG JSON] Interrogo il log eventi dell'NVR per canale ${channelID}...`);
171
+ node.warn(`[DEBUG JSON] Invio query giornaliera con data stile Chrome...`);
172
172
  const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
173
- const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/json" -d "@${tempJsonPath}" "${targetUrl}"`;
173
+
174
+ // Forziamo gli header completi ricavati dall'analisi del client web
175
+ const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/json; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d "@${tempJsonPath}" "${targetUrl}"`;
174
176
 
175
177
  let jsonResponseRaw = await new Promise((resolve) => {
176
178
  exec(curlCommand, (err, stdout) => { resolve(err ? null : stdout); });
@@ -179,7 +181,7 @@ module.exports = function(RED) {
179
181
  try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
180
182
 
181
183
  if (!jsonResponseRaw) {
182
- node.error("[DEBUG JSON] Nessuna risposta ricevuta dall'NVR.");
184
+ node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
183
185
  updateNodeStatus();
184
186
  return;
185
187
  }
@@ -188,48 +190,46 @@ module.exports = function(RED) {
188
190
  const matches = searchResult?.EventSearchResult?.matchList || [];
189
191
 
190
192
  if (matches.length === 0) {
191
- node.warn(`[DEBUG JSON] Nessun match trovato nel database per la Cam ${channelID} in questa finestra temporale.`);
193
+ node.warn(`[DEBUG JSON] Zero match trovati nell'archivio giornaliero.`);
192
194
  updateNodeStatus();
193
195
  return;
194
196
  }
195
197
 
196
- // Estraiamo il record dell'evento
198
+ // Prendiamo il record d'archivio più fresco (l'ultimo registrato)
197
199
  const matchItem = matches[0];
198
200
  const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
199
201
 
200
- // --- 1. CATTURA DELLA FOTO ORIGINALE (URL /picture/ intercettato da Chrome) ---
201
- let pictureUrlPath = null;
202
202
  if (playbackUri) {
203
+ node.warn(`[DEBUG JSON] Trovato PlaybackURI: ${playbackUri}`);
204
+
205
+ // --- 1. DOWNLOAD FOTO HISTORIC (GET diretta sulla traccia foto 203) ---
206
+ let pictureUrlPath = null;
203
207
  const urlParamsMatch = playbackUri.match(/\?(.*)/);
204
208
  if (urlParamsMatch) {
205
- const trackI = (channelID * 100 + 3).toString(); // Es: Canale 2 -> traccia foto 203
209
+ const trackI = (channelID * 100 + 3).toString();
206
210
  pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
207
211
  }
208
- }
209
212
 
210
- if (pictureUrlPath) {
211
- node.warn(`[DEBUG FOTO] Scarico la foto nativa dell'evento via HTTP GET...`);
212
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
213
- const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
214
-
215
- await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
216
-
217
- if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
218
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
219
- fileDaCancellare.push(fullImgPath);
220
- node.warn(`[DEBUG FOTO] Foto catturata con successo (${fs.statSync(fullImgPath).size} byte).`);
213
+ if (pictureUrlPath) {
214
+ node.warn(`[DEBUG FOTO] Scarico lo snapshot nativo d'archivio via GET...`);
215
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
216
+ const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
217
+
218
+ await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
219
+
220
+ if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
221
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
222
+ fileDaCancellare.push(fullImgPath);
223
+ node.warn(`[DEBUG FOTO] Foto scaricata con successo.`);
224
+ }
221
225
  }
222
- }
223
226
 
224
- // --- 2. CATTURA DELLO SPEZZONE VIDEO (Via RTSP con Ffmpeg per tagliare a 15 secondi) ---
225
- if (playbackUri) {
226
- node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP dell'evento registrato...`);
227
+ // --- 2. DOWNLOAD VIDEO HISTORIC VIA RTSP (Taglio a 15 secondi) ---
228
+ node.warn(`[DEBUG VIDEO] Recupero lo spezzone video registrato via RTSP...`);
227
229
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
228
-
229
- // Iniettiamo le credenziali admin:pass all'interno dell'URL RTSP dell'NVR
230
230
  const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
231
231
 
232
- // -t 15 costringe Ffmpeg a registrare solo 15 secondi dall'orario di inizio dell'evento, poi chiude il rubinetto!
232
+ // Connessione all'RTSP storico e salvataggio locale dei primi 15 secondi dell'evento
233
233
  const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
234
234
 
235
235
  await new Promise((resolve) => {
@@ -242,20 +242,18 @@ module.exports = function(RED) {
242
242
  if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
243
243
  output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
244
244
  fileDaCancellare.push(fixedPath);
245
- node.warn(`[DEBUG VIDEO] Spezzone video catturato con successo (${fs.statSync(fixedPath).size} byte).`);
246
- } else {
247
- node.warn(`[DEBUG VIDEO] Flusso RTSP non catturato o spezzone non pronto.`);
245
+ node.warn(`[DEBUG VIDEO] Spezzone video generato con successo.`);
248
246
  }
249
247
  }
250
248
 
251
- // --- INVIO PAYLOAD FINALE A NODE-RED ---
249
+ // --- SPEDIZIONE MESSAGGIO FINALE ---
252
250
  if (output.foto_base64 || output.video_base64) {
253
251
  node.send({ payload: output });
254
252
  setTimeout(() => {
255
253
  for (let file of fileDaCancellare) {
256
254
  try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
257
255
  }
258
- }, 60000); // Pulisce i file locali dopo 1 minuto
256
+ }, 60000);
259
257
  }
260
258
 
261
259
  } catch (e) {
@@ -264,7 +262,7 @@ module.exports = function(RED) {
264
262
  updateNodeStatus();
265
263
  }
266
264
 
267
- // --- ASCOLTO ALLARMI IN LIVE STREAM (Axios Stream) ---
265
+ // --- ASCOLTO SULL'ALERT STREAM DELL'NVR ---
268
266
  function startAlertStream() {
269
267
  if (isClosing) return;
270
268
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.99",
3
+ "version": "1.1.101",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",