node-red-contrib-hik-media-buffer 1.1.98 → 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 +36 -34
  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,14 +118,19 @@ module.exports = function(RED) {
118
118
  const referenceTime = new Date();
119
119
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
120
120
 
121
- // Nuova finestra temporale larga: cerca da 10 minuti fa a 2 minuti nel futuro per compensare i disallineamenti di orario
122
- const startTime = toJsonDate(new Date(referenceTime.getTime() - (10 * 1000)));
123
- const endTime = toJsonDate(new Date(referenceTime.getTime() + (10 * 1000)));
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);
126
+
127
+ const startTime = toJsonDate(dStart);
128
+ const endTime = toJsonDate(dEnd);
124
129
 
125
130
  node.status({fill:"yellow", shape:"dot", text:`Ricerca Evento Cam ${channelID}...`});
126
131
 
127
- // Aspettiamo 3 secondi per dare tempo all'NVR di indicizzare l'evento sul log
128
- 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));
129
134
 
130
135
  let output = {
131
136
  tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
@@ -136,13 +141,13 @@ module.exports = function(RED) {
136
141
  let fileDaCancellare = [];
137
142
 
138
143
  try {
139
- // Ripristiniamo il SearchID dinamico
144
+ // Generazione dinamica di un SearchID UUID valido per la sessione
140
145
  const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
141
146
  var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
142
147
  return v.toString(16).toUpperCase();
143
148
  });
144
149
 
145
- // Ricostruiamo il JSON IDENTICO a quello intercettato su Chrome che funzionava
150
+ // Payload clonato identico a quello funzionante di Chrome
146
151
  const searchPayload = {
147
152
  "EventSearchDescription": {
148
153
  "searchID": dynamicSearchId,
@@ -150,17 +155,16 @@ module.exports = function(RED) {
150
155
  "maxResults": 30,
151
156
  "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
152
157
  "type": "all",
153
- "channels": [ parseInt(channelID) ], // Ritorniamo al canale puro (es. 2)
158
+ "channels": [ parseInt(channelID) ],
154
159
  "eventType": "behavior",
155
- "behavior": {
156
- "behaviorEventType": evento.toLowerCase() // Spara "linedetection" o "fielddetection"
157
- }
160
+ "behavior": { "behaviorEventType": evento.toLowerCase() }
158
161
  }
159
162
  };
163
+
160
164
  const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
161
165
  fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
162
166
 
163
- node.warn(`[DEBUG JSON] Interrogo il log eventi dell'NVR per canale ${channelID}...`);
167
+ node.warn(`[DEBUG JSON] Invio query con data stile Chrome...`);
164
168
  const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
165
169
  const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/json" -d "@${tempJsonPath}" "${targetUrl}"`;
166
170
 
@@ -171,7 +175,7 @@ module.exports = function(RED) {
171
175
  try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
172
176
 
173
177
  if (!jsonResponseRaw) {
174
- node.error("[DEBUG JSON] Nessuna risposta ricevuta dall'NVR.");
178
+ node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
175
179
  updateNodeStatus();
176
180
  return;
177
181
  }
@@ -180,27 +184,27 @@ module.exports = function(RED) {
180
184
  const matches = searchResult?.EventSearchResult?.matchList || [];
181
185
 
182
186
  if (matches.length === 0) {
183
- 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.`);
184
188
  updateNodeStatus();
185
189
  return;
186
190
  }
187
191
 
188
- // Estraiamo il record dell'evento
192
+ // Prendiamo il record più fresco (il primo restituito)
189
193
  const matchItem = matches[0];
190
194
  const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
191
195
 
192
- // --- 1. CATTURA DELLA FOTO ORIGINALE (URL /picture/ intercettato da Chrome) ---
196
+ // --- 1. DOWNLOAD FOTO ORIGINALE DAL PATH NATIVO GET ---
193
197
  let pictureUrlPath = null;
194
198
  if (playbackUri) {
195
199
  const urlParamsMatch = playbackUri.match(/\?(.*)/);
196
200
  if (urlParamsMatch) {
197
- const trackI = (channelID * 100 + 3).toString(); // Es: Canale 2 -> traccia foto 203
201
+ const trackI = (channelID * 100 + 3).toString(); // Traccia 203 per le foto
198
202
  pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
199
203
  }
200
204
  }
201
205
 
202
206
  if (pictureUrlPath) {
203
- 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...`);
204
208
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
205
209
  const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
206
210
 
@@ -213,15 +217,15 @@ module.exports = function(RED) {
213
217
  }
214
218
  }
215
219
 
216
- // --- 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) ---
217
221
  if (playbackUri) {
218
- node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP dell'evento registrato...`);
222
+ node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP temporizzato dell'evento...`);
219
223
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
220
224
 
221
- // Iniettiamo le credenziali admin:pass all'interno dell'URL RTSP dell'NVR
225
+ // Integriamo utente e password nell'URL RTSP
222
226
  const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
223
227
 
224
- // -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
225
229
  const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
226
230
 
227
231
  await new Promise((resolve) => {
@@ -234,20 +238,18 @@ module.exports = function(RED) {
234
238
  if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
235
239
  output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
236
240
  fileDaCancellare.push(fixedPath);
237
- node.warn(`[DEBUG VIDEO] Spezzone video catturato con successo (${fs.statSync(fixedPath).size} byte).`);
238
- } else {
239
- 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).`);
240
242
  }
241
243
  }
242
244
 
243
- // --- INVIO PAYLOAD FINALE A NODE-RED ---
245
+ // --- SPEDIZIONE PAYLOAD ---
244
246
  if (output.foto_base64 || output.video_base64) {
245
247
  node.send({ payload: output });
246
248
  setTimeout(() => {
247
249
  for (let file of fileDaCancellare) {
248
250
  try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
249
251
  }
250
- }, 60000); // Pulisce i file locali dopo 1 minuto
252
+ }, 60000);
251
253
  }
252
254
 
253
255
  } catch (e) {
@@ -256,7 +258,7 @@ module.exports = function(RED) {
256
258
  updateNodeStatus();
257
259
  }
258
260
 
259
- // --- ASCOLTO ALLARMI IN LIVE STREAM (Axios Stream) ---
261
+ // --- APERTURA ALERT STREAM (Axios Stream continuo) ---
260
262
  function startAlertStream() {
261
263
  if (isClosing) return;
262
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.98",
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",