node-red-contrib-hik-media-buffer 1.1.100 → 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 +48 -123
  2. package/package.json +1 -1
@@ -28,27 +28,6 @@ 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)
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
- // Chiariamo l'offset senza inserire il '+' che fa incazzare l'NVR
43
- const offsetSign = offsetMinutes <= 0 ? '' : '-';
44
- const absOffsetMinutes = Math.abs(offsetMinutes);
45
- const offsetHours = pad(Math.floor(absOffsetMinutes / 60));
46
- const offsetMins = pad(absOffsetMinutes % 60);
47
-
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}`;
50
- }
51
-
52
31
  const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
53
32
 
54
33
  async function getCameraName(channelID) {
@@ -106,7 +85,7 @@ module.exports = function(RED) {
106
85
 
107
86
  const heartbeatInterval = setInterval(checkCameras, 30000);
108
87
 
109
- // --- SISTEMA DI SCARICAMENTO SBLOCCATO CON DATA DI CHROME ---
88
+ // --- NUOVA STRATEGIA INDISTRUTTIBILE: DIRETTA SU STREAMING RTSP STORICO ---
110
89
  async function downloadMedia(evento, channelID) {
111
90
  const chStr = channelID.toString();
112
91
  const nowTime = Date.now();
@@ -117,21 +96,27 @@ module.exports = function(RED) {
117
96
  const nomeCamera = await getCameraName(channelID);
118
97
  const referenceTime = new Date();
119
98
  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
99
 
127
- const startTime = toJsonDate(dStart);
128
- const endTime = toJsonDate(dEnd);
129
-
130
- node.status({fill:"yellow", shape:"dot", text:`Ricerca Evento Cam ${channelID}...`});
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
103
+ const pad = (num) => String(num).padStart(2, '0');
104
+ const timeGoBack = new Date(referenceTime.getTime() - (10 * 1000)); // iniziamo 10 secondi prima dello scatto
131
105
 
132
- // Aspettiamo 4 secondi per essere certi che l'NVR abbia scritto il log a database
133
- 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());
134
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
+
135
120
  let output = {
136
121
  tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
137
122
  ip_telecamera: null, tipo_evento: evento, timestamp_epoch: timestamp,
@@ -141,105 +126,45 @@ module.exports = function(RED) {
141
126
  let fileDaCancellare = [];
142
127
 
143
128
  try {
144
- // Generazione dinamica di un SearchID UUID valido per la sessione
145
- const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
146
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
147
- return v.toString(16).toUpperCase();
148
- });
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`);
149
132
 
150
- // Payload clonato identico a quello funzionante di Chrome
151
- const searchPayload = {
152
- "EventSearchDescription": {
153
- "searchID": dynamicSearchId,
154
- "searchResultPosition": 0,
155
- "maxResults": 30,
156
- "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
157
- "type": "all",
158
- "channels": [ parseInt(channelID) ],
159
- "eventType": "behavior",
160
- "behavior": { "behaviorEventType": evento.toLowerCase() }
161
- }
162
- };
163
-
164
- const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
165
- 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}`);
166
136
 
167
- node.warn(`[DEBUG JSON] Invio query con data stile Chrome...`);
168
- 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}"`;
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}"`;
170
139
 
171
- let jsonResponseRaw = await new Promise((resolve) => {
172
- 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
+ });
173
145
  });
174
146
 
175
- try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
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).`);
176
151
 
177
- if (!jsonResponseRaw) {
178
- node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
179
- updateNodeStatus();
180
- return;
181
- }
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}"`;
182
156
 
183
- const searchResult = JSON.parse(jsonResponseRaw);
184
- const matches = searchResult?.EventSearchResult?.matchList || [];
185
-
186
- if (matches.length === 0) {
187
- node.warn(`[DEBUG JSON] Zero match trovati nell'intervallo orario con sintassi Chrome.`);
188
- updateNodeStatus();
189
- return;
190
- }
191
-
192
- // Prendiamo il record più fresco (il primo restituito)
193
- const matchItem = matches[0];
194
- const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
195
-
196
- // --- 1. DOWNLOAD FOTO ORIGINALE DAL PATH NATIVO GET ---
197
- let pictureUrlPath = null;
198
- if (playbackUri) {
199
- const urlParamsMatch = playbackUri.match(/\?(.*)/);
200
- if (urlParamsMatch) {
201
- const trackI = (channelID * 100 + 3).toString(); // Traccia 203 per le foto
202
- pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
203
- }
204
- }
157
+ await new Promise((resolve) => {
158
+ exec(ffmpegImgCmd, () => resolve());
159
+ });
205
160
 
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
161
  if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
214
162
  output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
215
163
  fileDaCancellare.push(fullImgPath);
216
- node.warn(`[DEBUG FOTO] Foto catturata con successo (${fs.statSync(fullImgPath).size} byte).`);
217
- }
218
- }
219
-
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...`);
223
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
224
-
225
- // Integriamo utente e password nell'URL RTSP
226
- const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
227
-
228
- // -t 15 ferma Ffmpeg dopo 15 secondi esatti dall'inizio del record, evitando i file da 1GB
229
- const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
230
-
231
- await new Promise((resolve) => {
232
- exec(ffmpegRtspCmd, { timeout: 25000 }, (err) => {
233
- if (err) node.error(`[DEBUG FFMPEG ERRORE]: ${err.message}`);
234
- resolve();
235
- });
236
- });
237
-
238
- if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
239
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
240
- fileDaCancellare.push(fixedPath);
241
- node.warn(`[DEBUG VIDEO] Video catturato con successo (${fs.statSync(fixedPath).size} byte).`);
164
+ node.warn(`[DEBUG RTSP] Foto estratta con successo dal video dell'evento.`);
242
165
  }
166
+ } else {
167
+ node.warn(`[DEBUG RTSP] Impossibile recuperare lo stream video dall'archivio.`);
243
168
  }
244
169
 
245
170
  // --- SPEDIZIONE PAYLOAD ---
@@ -253,12 +178,12 @@ module.exports = function(RED) {
253
178
  }
254
179
 
255
180
  } catch (e) {
256
- node.error(`Errore Critico nel Download Cam ${channelID}: ${e.message}`);
181
+ node.error(`Errore Critico nel recupero RTSP diretto: ${e.message}`);
257
182
  }
258
183
  updateNodeStatus();
259
184
  }
260
185
 
261
- // --- APERTURA ALERT STREAM (Axios Stream continuo) ---
186
+ // --- ASCOLTO LIVE STREAM DEGLI ALLARMI ---
262
187
  function startAlertStream() {
263
188
  if (isClosing) return;
264
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.100",
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",