node-red-contrib-hik-media-buffer 1.1.100 → 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 +53 -49
  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
- // CLONE PERFETTO DEL FORMATO DATA DI CHROME (Es: 2026-06-23T17:40: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,14 +39,12 @@ module.exports = function(RED) {
39
39
  const seconds = pad(d.getSeconds());
40
40
 
41
41
  const offsetMinutes = d.getTimezoneOffset();
42
- // Chiariamo l'offset senza inserire il '+' che fa incazzare l'NVR
43
42
  const offsetSign = offsetMinutes <= 0 ? '' : '-';
44
- const absOffsetMinutes = Math.abs(offsetMinutes);
45
- const offsetHours = pad(Math.floor(absOffsetMinutes / 60));
46
- const offsetMins = pad(absOffsetMinutes % 60);
43
+ const offsetHours = pad(Math.floor(Math.abs(offsetMinutes) / 60));
44
+ const offsetMins = pad(Math.abs(offsetMinutes) % 60);
45
+ const fusoOrario = `${offsetSign}${offsetHours}:${offsetMins}`;
47
46
 
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}`;
47
+ return `${year}-${month}-${day}T${hours}:${minutes}:${seconds} ${fusoOrario}`;
50
48
  }
51
49
 
52
50
  const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
@@ -106,7 +104,7 @@ module.exports = function(RED) {
106
104
 
107
105
  const heartbeatInterval = setInterval(checkCameras, 30000);
108
106
 
109
- // --- SISTEMA DI SCARICAMENTO SBLOCCATO CON DATA DI CHROME ---
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,19 +115,25 @@ 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
- // 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
118
 
127
- const startTime = toJsonDate(dStart);
128
- 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
+
131
+ const startTime = `${year}-${month}-${day}T00:00:00 ${fusoOrario}`;
132
+ const endTime = `${year}-${month}-${day}T23:59:59 ${fusoOrario}`;
129
133
 
130
- node.status({fill:"yellow", shape:"dot", text:`Ricerca Evento Cam ${channelID}...`});
134
+ node.status({fill:"yellow", shape:"dot", text:`Ricerca archivio Cam ${channelID}...`});
131
135
 
132
- // Aspettiamo 4 secondi per essere certi che l'NVR abbia scritto il log a database
136
+ // Aspettiamo 4 secondi per dare tempo all'NVR di indicizzare il log su disco
133
137
  await new Promise(resolve => setTimeout(resolve, 4000));
134
138
 
135
139
  let output = {
@@ -141,13 +145,13 @@ module.exports = function(RED) {
141
145
  let fileDaCancellare = [];
142
146
 
143
147
  try {
144
- // Generazione dinamica di un SearchID UUID valido per la sessione
148
+ // SearchID UUID Dinamico ad ogni sessione
145
149
  const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
146
150
  var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
147
151
  return v.toString(16).toUpperCase();
148
152
  });
149
153
 
150
- // Payload clonato identico a quello funzionante di Chrome
154
+ // Payload clonato dal Form Data di Chrome
151
155
  const searchPayload = {
152
156
  "EventSearchDescription": {
153
157
  "searchID": dynamicSearchId,
@@ -164,9 +168,11 @@ module.exports = function(RED) {
164
168
  const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
165
169
  fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
166
170
 
167
- node.warn(`[DEBUG JSON] Invio query con data stile Chrome...`);
171
+ node.warn(`[DEBUG JSON] Invio query giornaliera con data stile Chrome...`);
168
172
  const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
169
- 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}"`;
170
176
 
171
177
  let jsonResponseRaw = await new Promise((resolve) => {
172
178
  exec(curlCommand, (err, stdout) => { resolve(err ? null : stdout); });
@@ -184,48 +190,46 @@ module.exports = function(RED) {
184
190
  const matches = searchResult?.EventSearchResult?.matchList || [];
185
191
 
186
192
  if (matches.length === 0) {
187
- node.warn(`[DEBUG JSON] Zero match trovati nell'intervallo orario con sintassi Chrome.`);
193
+ node.warn(`[DEBUG JSON] Zero match trovati nell'archivio giornaliero.`);
188
194
  updateNodeStatus();
189
195
  return;
190
196
  }
191
197
 
192
- // Prendiamo il record più fresco (il primo restituito)
198
+ // Prendiamo il record d'archivio più fresco (l'ultimo registrato)
193
199
  const matchItem = matches[0];
194
200
  const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
195
201
 
196
- // --- 1. DOWNLOAD FOTO ORIGINALE DAL PATH NATIVO GET ---
197
- let pictureUrlPath = null;
198
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;
199
207
  const urlParamsMatch = playbackUri.match(/\?(.*)/);
200
208
  if (urlParamsMatch) {
201
- const trackI = (channelID * 100 + 3).toString(); // Traccia 203 per le foto
209
+ const trackI = (channelID * 100 + 3).toString();
202
210
  pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
203
211
  }
204
- }
205
212
 
206
- if (pictureUrlPath) {
207
- node.warn(`[DEBUG FOTO] Scarico la foto originale dell'evento via HTTP GET...`);
208
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
209
- const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
210
-
211
- await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
212
-
213
- if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
214
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
215
- fileDaCancellare.push(fullImgPath);
216
- 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
+ }
217
225
  }
218
- }
219
226
 
220
- // --- 2. DOWNLOAD VIDEO CHIRURGICO VIA RTSP (Taglio forzato a 15 secondi) ---
221
- if (playbackUri) {
222
- node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP temporizzato dell'evento...`);
227
+ // --- 2. DOWNLOAD VIDEO HISTORIC VIA RTSP (Taglio a 15 secondi) ---
228
+ node.warn(`[DEBUG VIDEO] Recupero lo spezzone video registrato via RTSP...`);
223
229
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
224
-
225
- // Integriamo utente e password nell'URL RTSP
226
230
  const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
227
231
 
228
- // -t 15 ferma Ffmpeg dopo 15 secondi esatti dall'inizio del record, evitando i file da 1GB
232
+ // Connessione all'RTSP storico e salvataggio locale dei primi 15 secondi dell'evento
229
233
  const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
230
234
 
231
235
  await new Promise((resolve) => {
@@ -238,11 +242,11 @@ module.exports = function(RED) {
238
242
  if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
239
243
  output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
240
244
  fileDaCancellare.push(fixedPath);
241
- node.warn(`[DEBUG VIDEO] Video catturato con successo (${fs.statSync(fixedPath).size} byte).`);
245
+ node.warn(`[DEBUG VIDEO] Spezzone video generato con successo.`);
242
246
  }
243
247
  }
244
248
 
245
- // --- SPEDIZIONE PAYLOAD ---
249
+ // --- SPEDIZIONE MESSAGGIO FINALE ---
246
250
  if (output.foto_base64 || output.video_base64) {
247
251
  node.send({ payload: output });
248
252
  setTimeout(() => {
@@ -258,7 +262,7 @@ module.exports = function(RED) {
258
262
  updateNodeStatus();
259
263
  }
260
264
 
261
- // --- APERTURA ALERT STREAM (Axios Stream continuo) ---
265
+ // --- ASCOLTO SULL'ALERT STREAM DELL'NVR ---
262
266
  function startAlertStream() {
263
267
  if (isClosing) return;
264
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.100",
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",