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

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 +49 -128
  2. package/package.json +1 -1
@@ -28,25 +28,6 @@ module.exports = function(RED) {
28
28
 
29
29
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
30
30
 
31
- // Formatta la data simulando il fuso orario senza il segno '+'
32
- function toJsonDate(d) {
33
- const pad = (num) => String(num).padStart(2, '0');
34
- const year = d.getFullYear();
35
- const month = pad(d.getMonth() + 1);
36
- const day = pad(d.getDate());
37
- const hours = pad(d.getHours());
38
- const minutes = pad(d.getMinutes());
39
- const seconds = pad(d.getSeconds());
40
-
41
- const offsetMinutes = d.getTimezoneOffset();
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
-
47
- return `${year}-${month}-${day}T${hours}:${minutes}:${seconds} ${fusoOrario}`;
48
- }
49
-
50
31
  const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
51
32
 
52
33
  async function getCameraName(channelID) {
@@ -104,7 +85,7 @@ module.exports = function(RED) {
104
85
 
105
86
  const heartbeatInterval = setInterval(checkCameras, 30000);
106
87
 
107
- // --- CORE: RICERCA ARCHIVIO GIORNALIERO SIMULANDO CHROME ---
88
+ // --- NUOVA STRATEGIA INDISTRUTTIBILE: DIRETTA SU STREAMING RTSP STORICO ---
108
89
  async function downloadMedia(evento, channelID) {
109
90
  const chStr = channelID.toString();
110
91
  const nowTime = Date.now();
@@ -116,26 +97,26 @@ module.exports = function(RED) {
116
97
  const referenceTime = new Date();
117
98
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
118
99
 
119
- // Generazione dell'intervallo per l'intera giornata corrente
100
+ // Costruiamo la stringa temporale RTSP Hikvision calcolando l'orario di inizio (10 secondi prima di ADESSO)
101
+ // Il formato richiesto dall'RTSP Hikvision è: AAAAMMGGtHHMMSSz (tutto minuscolo)
102
+ // Usiamo l'ora UTC per compatibilità nativa con i file system dei flussi video
120
103
  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}`;
133
-
134
- node.status({fill:"yellow", shape:"dot", text:`Ricerca archivio Cam ${channelID}...`});
104
+ const timeGoBack = new Date(referenceTime.getTime() - (10 * 1000)); // iniziamo 10 secondi prima dello scatto
135
105
 
136
- // Aspettiamo 4 secondi per dare tempo all'NVR di indicizzare il log su disco
137
- await new Promise(resolve => setTimeout(resolve, 4000));
106
+ const year = timeGoBack.getUTCFullYear();
107
+ const month = pad(timeGoBack.getUTCMonth() + 1);
108
+ const day = pad(timeGoBack.getUTCDate());
109
+ const hours = pad(timeGoBack.getUTCHours());
110
+ const minutes = pad(timeGoBack.getUTCMinutes());
111
+ const seconds = pad(timeGoBack.getUTCSeconds());
138
112
 
113
+ const rtspTimestamp = `${year}${month}${day}t${hours}${minutes}${seconds}z`;
114
+
115
+ node.status({fill:"yellow", shape:"dot", text:`Estrazione media Cam ${channelID}...`});
116
+
117
+ // Aspettiamo 5 secondi per dare all'NVR il tempo di scrivere i frame correnti sull'hard disk
118
+ await new Promise(resolve => setTimeout(resolve, 5000));
119
+
139
120
  let output = {
140
121
  tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
141
122
  ip_telecamera: null, tipo_evento: evento, timestamp_epoch: timestamp,
@@ -145,108 +126,48 @@ module.exports = function(RED) {
145
126
  let fileDaCancellare = [];
146
127
 
147
128
  try {
148
- // SearchID UUID Dinamico ad ogni sessione
149
- const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
150
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
151
- return v.toString(16).toUpperCase();
152
- });
129
+ const trackId = parseInt(channelID) * 100 + 1; // Es: Canale 2 -> traccia 201
130
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
131
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
153
132
 
154
- // Payload clonato dal Form Data di Chrome
155
- const searchPayload = {
156
- "EventSearchDescription": {
157
- "searchID": dynamicSearchId,
158
- "searchResultPosition": 0,
159
- "maxResults": 30,
160
- "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
161
- "type": "all",
162
- "channels": [ parseInt(channelID) ],
163
- "eventType": "behavior",
164
- "behavior": { "behaviorEventType": evento.toLowerCase() }
165
- }
166
- };
167
-
168
- const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
169
- fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
133
+ // Costruiamo l'URL RTSP di Playback diretto inserendo le credenziali e il timestamp di inizio
134
+ const archiveRtspUrl = `rtsp://${node.user}:${node.pass}@${node.host}:554/Streaming/tracks/${trackId}/?starttime=${rtspTimestamp}`;
135
+ node.warn(`[DEBUG RTSP] Mi collego all'archivio su traccia ${trackId} dall'orario: ${rtspTimestamp}`);
170
136
 
171
- node.warn(`[DEBUG JSON] Invio query giornaliera con data stile Chrome...`);
172
- const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
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}"`;
137
+ // --- 1. CATTURA VIDEO DIRECT (Registriamo 15 secondi di archivio) ---
138
+ const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${archiveRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
176
139
 
177
- let jsonResponseRaw = await new Promise((resolve) => {
178
- exec(curlCommand, (err, stdout) => { resolve(err ? null : stdout); });
140
+ await new Promise((resolve) => {
141
+ exec(ffmpegRtspCmd, { timeout: 25000 }, (err) => {
142
+ if (err) node.error(`[DEBUG FFMPEG VIDEO ERRORE]: ${err.message}`);
143
+ resolve();
144
+ });
179
145
  });
180
146
 
181
- try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
182
-
183
- if (!jsonResponseRaw) {
184
- node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
185
- updateNodeStatus();
186
- return;
187
- }
188
-
189
- const searchResult = JSON.parse(jsonResponseRaw);
190
- const matches = searchResult?.EventSearchResult?.matchList || [];
191
-
192
- if (matches.length === 0) {
193
- node.warn(`[DEBUG JSON] Zero match trovati nell'archivio giornaliero.`);
194
- updateNodeStatus();
195
- return;
196
- }
197
-
198
- // Prendiamo il record d'archivio più fresco (l'ultimo registrato)
199
- const matchItem = matches[0];
200
- const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
201
-
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;
207
- const urlParamsMatch = playbackUri.match(/\?(.*)/);
208
- if (urlParamsMatch) {
209
- const trackI = (channelID * 100 + 3).toString();
210
- pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
211
- }
212
-
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
- }
225
- }
226
-
227
- // --- 2. DOWNLOAD VIDEO HISTORIC VIA RTSP (Taglio a 15 secondi) ---
228
- node.warn(`[DEBUG VIDEO] Recupero lo spezzone video registrato via RTSP...`);
229
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
230
- const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
147
+ if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
148
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
149
+ fileDaCancellare.push(fixedPath);
150
+ node.warn(`[DEBUG RTSP] Spezzone video d'archivio estratto con successo (${fs.statSync(fixedPath).size} byte).`);
231
151
 
232
- // Connessione all'RTSP storico e salvataggio locale dei primi 15 secondi dell'evento
233
- const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
152
+ // --- 2. GENERAZIONE FOTO DAL VIDEO APPENA SCARICATO ---
153
+ // Estraiamo il primo fotogramma utile del video reale registrato su disco dell'evento. Infallibile!
154
+ node.warn(`[DEBUG RTSP] Estraggo il fotogramma dell'evento dal video registrato...`);
155
+ const ffmpegImgCmd = `ffmpeg -y -i "${fixedPath}" -vframes 1 -q:v 2 "${fullImgPath}"`;
234
156
 
235
157
  await new Promise((resolve) => {
236
- exec(ffmpegRtspCmd, { timeout: 25000 }, (err) => {
237
- if (err) node.error(`[DEBUG FFMPEG ERRORE]: ${err.message}`);
238
- resolve();
239
- });
158
+ exec(ffmpegImgCmd, () => resolve());
240
159
  });
241
160
 
242
- if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
243
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
244
- fileDaCancellare.push(fixedPath);
245
- node.warn(`[DEBUG VIDEO] Spezzone video generato con successo.`);
161
+ if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
162
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
163
+ fileDaCancellare.push(fullImgPath);
164
+ node.warn(`[DEBUG RTSP] Foto estratta con successo dal video dell'evento.`);
246
165
  }
166
+ } else {
167
+ node.warn(`[DEBUG RTSP] Impossibile recuperare lo stream video dall'archivio.`);
247
168
  }
248
169
 
249
- // --- SPEDIZIONE MESSAGGIO FINALE ---
170
+ // --- SPEDIZIONE PAYLOAD ---
250
171
  if (output.foto_base64 || output.video_base64) {
251
172
  node.send({ payload: output });
252
173
  setTimeout(() => {
@@ -257,12 +178,12 @@ module.exports = function(RED) {
257
178
  }
258
179
 
259
180
  } catch (e) {
260
- node.error(`Errore Critico nel Download Cam ${channelID}: ${e.message}`);
181
+ node.error(`Errore Critico nel recupero RTSP diretto: ${e.message}`);
261
182
  }
262
183
  updateNodeStatus();
263
184
  }
264
185
 
265
- // --- ASCOLTO SULL'ALERT STREAM DELL'NVR ---
186
+ // --- ASCOLTO LIVE STREAM DEGLI ALLARMI ---
266
187
  function startAlertStream() {
267
188
  if (isClosing) return;
268
189
  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.101",
3
+ "version": "1.1.102",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",