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

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 +33 -39
  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
+ // CLONE PERFETTO DEL FORMATO DATA DI CHROME (Es: 2026-06-23T17:40:00 02:00)
32
32
  function toJsonDate(d) {
33
33
  const pad = (num) => String(num).padStart(2, '0');
34
34
  const year = d.getFullYear();
@@ -39,17 +39,18 @@ module.exports = function(RED) {
39
39
  const seconds = pad(d.getSeconds());
40
40
 
41
41
  const offsetMinutes = d.getTimezoneOffset();
42
- const offsetSign = offsetMinutes <= 0 ? '+' : '-';
42
+ // Chiariamo l'offset senza inserire il '+' che fa incazzare l'NVR
43
+ const offsetSign = offsetMinutes <= 0 ? '' : '-';
43
44
  const absOffsetMinutes = Math.abs(offsetMinutes);
44
45
  const offsetHours = pad(Math.floor(absOffsetMinutes / 60));
45
46
  const offsetMins = pad(absOffsetMinutes % 60);
46
47
 
47
- return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}${offsetSign}${offsetHours}:${offsetMins}`;
48
+ // NOTA: C'è uno spazio intenzionale prima di offsetSign per replicare il log dell'NVR
49
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds} ${offsetSign}${offsetHours}:${offsetMins}`;
48
50
  }
49
51
 
50
52
  const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
51
53
 
52
- // --- RECUPERA IL NOME DELLA TELECAMERA ---
53
54
  async function getCameraName(channelID) {
54
55
  try {
55
56
  const res = await nvrAuthAxios.request({
@@ -63,7 +64,6 @@ module.exports = function(RED) {
63
64
  } catch (e) { return `Camera_${channelID}`; }
64
65
  }
65
66
 
66
- // --- CONTROLLO STATUS TELECAMERE CONNESSE ALL'NVR ---
67
67
  async function checkCameras() {
68
68
  if (isClosing || !nvrOnline) return;
69
69
  try {
@@ -106,7 +106,7 @@ module.exports = function(RED) {
106
106
 
107
107
  const heartbeatInterval = setInterval(checkCameras, 30000);
108
108
 
109
- // --- CORE RESCUE: RICERCA EVENTO JSON, CATTURA FOTO HTTP E VIDEO RTSP ---
109
+ // --- SISTEMA DI SCARICAMENTO SBLOCCATO CON DATA DI CHROME ---
110
110
  async function downloadMedia(evento, channelID) {
111
111
  const chStr = channelID.toString();
112
112
  const nowTime = Date.now();
@@ -118,22 +118,19 @@ module.exports = function(RED) {
118
118
  const referenceTime = new Date();
119
119
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
120
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
121
+ // Cerchiamo nell'ora corrente (da inizio ora a fine ora) per non fare tutto il giorno
122
+ const dStart = new Date(referenceTime);
123
+ dStart.setMinutes(0, 0, 0);
124
+ const dEnd = new Date(referenceTime);
125
+ dEnd.setMinutes(59, 59, 999);
127
126
 
128
127
  const startTime = toJsonDate(dStart);
129
128
  const endTime = toJsonDate(dEnd);
130
129
 
131
- node.warn(`[DEBUG TEMPO SANIFICATO] Cerco da: ${startTime} a: ${endTime}`);
132
-
133
130
  node.status({fill:"yellow", shape:"dot", text:`Ricerca Evento Cam ${channelID}...`});
134
131
 
135
- // Aspettiamo 3 secondi per dare tempo all'NVR di indicizzare l'evento sul log
136
- await new Promise(resolve => setTimeout(resolve, 6000));
132
+ // Aspettiamo 4 secondi per essere certi che l'NVR abbia scritto il log a database
133
+ await new Promise(resolve => setTimeout(resolve, 4000));
137
134
 
138
135
  let output = {
139
136
  tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
@@ -144,13 +141,13 @@ module.exports = function(RED) {
144
141
  let fileDaCancellare = [];
145
142
 
146
143
  try {
147
- // Ripristiniamo il SearchID dinamico
144
+ // Generazione dinamica di un SearchID UUID valido per la sessione
148
145
  const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
149
146
  var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
150
147
  return v.toString(16).toUpperCase();
151
148
  });
152
149
 
153
- // Ricostruiamo il JSON IDENTICO a quello intercettato su Chrome che funzionava
150
+ // Payload clonato identico a quello funzionante di Chrome
154
151
  const searchPayload = {
155
152
  "EventSearchDescription": {
156
153
  "searchID": dynamicSearchId,
@@ -158,17 +155,16 @@ module.exports = function(RED) {
158
155
  "maxResults": 30,
159
156
  "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
160
157
  "type": "all",
161
- "channels": [ parseInt(channelID) ], // Ritorniamo al canale puro (es. 2)
158
+ "channels": [ parseInt(channelID) ],
162
159
  "eventType": "behavior",
163
- "behavior": {
164
- "behaviorEventType": evento.toLowerCase() // Spara "linedetection" o "fielddetection"
165
- }
160
+ "behavior": { "behaviorEventType": evento.toLowerCase() }
166
161
  }
167
162
  };
163
+
168
164
  const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
169
165
  fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
170
166
 
171
- node.warn(`[DEBUG JSON] Interrogo il log eventi dell'NVR per canale ${channelID}...`);
167
+ node.warn(`[DEBUG JSON] Invio query con data stile Chrome...`);
172
168
  const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
173
169
  const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/json" -d "@${tempJsonPath}" "${targetUrl}"`;
174
170
 
@@ -179,7 +175,7 @@ module.exports = function(RED) {
179
175
  try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
180
176
 
181
177
  if (!jsonResponseRaw) {
182
- node.error("[DEBUG JSON] Nessuna risposta ricevuta dall'NVR.");
178
+ node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
183
179
  updateNodeStatus();
184
180
  return;
185
181
  }
@@ -188,27 +184,27 @@ module.exports = function(RED) {
188
184
  const matches = searchResult?.EventSearchResult?.matchList || [];
189
185
 
190
186
  if (matches.length === 0) {
191
- node.warn(`[DEBUG JSON] Nessun match trovato nel database per la Cam ${channelID} in questa finestra temporale.`);
187
+ node.warn(`[DEBUG JSON] Zero match trovati nell'intervallo orario con sintassi Chrome.`);
192
188
  updateNodeStatus();
193
189
  return;
194
190
  }
195
191
 
196
- // Estraiamo il record dell'evento
192
+ // Prendiamo il record più fresco (il primo restituito)
197
193
  const matchItem = matches[0];
198
194
  const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
199
195
 
200
- // --- 1. CATTURA DELLA FOTO ORIGINALE (URL /picture/ intercettato da Chrome) ---
196
+ // --- 1. DOWNLOAD FOTO ORIGINALE DAL PATH NATIVO GET ---
201
197
  let pictureUrlPath = null;
202
198
  if (playbackUri) {
203
199
  const urlParamsMatch = playbackUri.match(/\?(.*)/);
204
200
  if (urlParamsMatch) {
205
- const trackI = (channelID * 100 + 3).toString(); // Es: Canale 2 -> traccia foto 203
201
+ const trackI = (channelID * 100 + 3).toString(); // Traccia 203 per le foto
206
202
  pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
207
203
  }
208
204
  }
209
205
 
210
206
  if (pictureUrlPath) {
211
- node.warn(`[DEBUG FOTO] Scarico la foto nativa dell'evento via HTTP GET...`);
207
+ node.warn(`[DEBUG FOTO] Scarico la foto originale dell'evento via HTTP GET...`);
212
208
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
213
209
  const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
214
210
 
@@ -221,15 +217,15 @@ module.exports = function(RED) {
221
217
  }
222
218
  }
223
219
 
224
- // --- 2. CATTURA DELLO SPEZZONE VIDEO (Via RTSP con Ffmpeg per tagliare a 15 secondi) ---
220
+ // --- 2. DOWNLOAD VIDEO CHIRURGICO VIA RTSP (Taglio forzato a 15 secondi) ---
225
221
  if (playbackUri) {
226
- node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP dell'evento registrato...`);
222
+ node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP temporizzato dell'evento...`);
227
223
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
228
224
 
229
- // Iniettiamo le credenziali admin:pass all'interno dell'URL RTSP dell'NVR
225
+ // Integriamo utente e password nell'URL RTSP
230
226
  const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
231
227
 
232
- // -t 15 costringe Ffmpeg a registrare solo 15 secondi dall'orario di inizio dell'evento, poi chiude il rubinetto!
228
+ // -t 15 ferma Ffmpeg dopo 15 secondi esatti dall'inizio del record, evitando i file da 1GB
233
229
  const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
234
230
 
235
231
  await new Promise((resolve) => {
@@ -242,20 +238,18 @@ module.exports = function(RED) {
242
238
  if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
243
239
  output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
244
240
  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.`);
241
+ node.warn(`[DEBUG VIDEO] Video catturato con successo (${fs.statSync(fixedPath).size} byte).`);
248
242
  }
249
243
  }
250
244
 
251
- // --- INVIO PAYLOAD FINALE A NODE-RED ---
245
+ // --- SPEDIZIONE PAYLOAD ---
252
246
  if (output.foto_base64 || output.video_base64) {
253
247
  node.send({ payload: output });
254
248
  setTimeout(() => {
255
249
  for (let file of fileDaCancellare) {
256
250
  try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
257
251
  }
258
- }, 60000); // Pulisce i file locali dopo 1 minuto
252
+ }, 60000);
259
253
  }
260
254
 
261
255
  } catch (e) {
@@ -264,7 +258,7 @@ module.exports = function(RED) {
264
258
  updateNodeStatus();
265
259
  }
266
260
 
267
- // --- ASCOLTO ALLARMI IN LIVE STREAM (Axios Stream) ---
261
+ // --- APERTURA ALERT STREAM (Axios Stream continuo) ---
268
262
  function startAlertStream() {
269
263
  if (isClosing) return;
270
264
  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.100",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",