node-red-contrib-hik-media-buffer 1.1.65 → 1.1.67

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 +98 -148
  2. package/package.json +1 -1
@@ -12,7 +12,7 @@ module.exports = function(RED) {
12
12
  const node = this;
13
13
 
14
14
  node.name = config.name || "TEST";
15
- node.host = config.host; // IP dell'NVR Centrale
15
+ node.host = config.host;
16
16
  node.port = config.port || "80";
17
17
  node.protocol = config.protocol || "http";
18
18
  node.user = config.user;
@@ -32,12 +32,6 @@ module.exports = function(RED) {
32
32
 
33
33
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
34
34
 
35
- function toHikDate(d) {
36
- const pad = (num) => String(num).padStart(2, '0');
37
- // Genera esattamente: YYYY-MM-DDTHH:mm:ssZ
38
- return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
39
- }
40
-
41
35
  async function getCameraName(channelID) {
42
36
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
43
37
  try {
@@ -111,49 +105,46 @@ module.exports = function(RED) {
111
105
 
112
106
  const heartbeatInterval = setInterval(checkCameras, 30000);
113
107
 
114
- // --- IL TUO DOWNLOAD MEDIA INIZIALE CORRETTO (100% XML SEPARATO) ---
108
+ // --- STRATEGIA DI DOWNLOAD MULTIMEDIALE 100% JSON + FFMPEG EXTRACTION ---
109
+ // --- STRATEGIA DI DOWNLOAD MULTIMEDIALE 100% JSON + FFMPEG EXTRACTION ---
115
110
  async function downloadMedia(evento, channelID) {
116
- node.warn(`[warn] [hik-media-buffer:test] [STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
111
+ node.warn(`[warn] [hik-media-buffer:test] [TRIGGER] Ricevuto evento. Evento: ${evento}, Canale originario: ${channelID}`);
117
112
 
118
113
  const nowTime = Date.now();
119
114
  if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
120
- if (nowTime - lastTriggerTime[channelID] < 5000) {
121
- node.warn(`[warn] [hik-media-buffer:test] Antirimbalzo bloccato.`);
122
- return;
123
- }
115
+ if (nowTime - lastTriggerTime[channelID] < 5000) return;
124
116
  lastTriggerTime[channelID] = nowTime;
125
117
 
126
118
  const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
127
119
  const referenceTime = new Date();
128
120
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
129
121
 
130
- // Generazione finestre temporali pulite per il database dei dischi dell'NVR
131
- const startTime = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
132
- const endTime = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
133
- node.warn(`[warn] [hik-media-buffer:test] [STEP 2] Finestra temporale impostata: ${startTime} -> ${endTime}`);
122
+ // FORMATTAZIONE DATA UTC PULITA PER IL JSON DELL'NVR
123
+ const formatJsonDate = (d) => {
124
+ const pad = (num) => String(num).padStart(2, '0');
125
+ return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
126
+ };
127
+
128
+ // ALLARGHIAMO LA FINESTRA: Cerca da 2 minuti prima dell'evento a 30 secondi dopo
129
+ const startTimeJson = formatJsonDate(new Date(referenceTime.getTime() - (120 * 1000)));
130
+ const endTimeJson = formatJsonDate(new Date(referenceTime.getTime() + (30 * 1000)));
134
131
 
135
132
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
136
133
  const nomeCamera = await getCameraName(channelID);
137
134
 
138
- node.warn(`[warn] [hik-media-buffer:test] [STEP 3] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
139
- await new Promise(resolve => setTimeout(resolve, 6000));
135
+ // PORTIAMO L'ATTESA A 10 SECONDI PER L'HARD DISK
136
+ node.warn(`[warn] [hik-media-buffer:test] Attesa scrittura completa su Hard Disk (10s)...`);
137
+ await new Promise(resolve => setTimeout(resolve, 10000));
140
138
 
141
139
  const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
142
-
143
- let eventoNVR = "FieldDetection";
144
- for (let e of EventList) {
145
- if (evento.toLowerCase() === e.toLowerCase()) {
146
- eventoNVR = e;
147
- break;
148
- }
149
- }
140
+ const evMinuscolo = evento.toLowerCase();
150
141
 
151
142
  let output = {
152
143
  tipo_messaggio: "evento",
153
144
  nome_cliente: node.name,
154
145
  nome_telecamera: nomeCamera,
155
146
  ip_telecamera: node.host,
156
- tipo_evento: eventoNVR,
147
+ tipo_evento: evMinuscolo.includes("linedetection") ? "LineDetection" : "FieldDetection",
157
148
  timestamp_epoch: timestamp,
158
149
  stato_telecamera: "ONLINE",
159
150
  channel: channelID.toString(),
@@ -162,150 +153,109 @@ module.exports = function(RED) {
162
153
  };
163
154
 
164
155
  const parsedChannel = parseInt(channelID, 10);
165
- const trackVideo = (parsedChannel * 100) + 1;
166
- const trackFoto = (parsedChannel * 100) + 3;
167
-
168
- // Le due tracce originali del tuo ciclo
169
- const tracks = [
170
- { id: trackVideo.toString(), isFoto: false, label: "VIDEO" },
171
- { id: trackFoto.toString(), isFoto: true, label: "FOTO" }
172
- ];
173
156
  let fileDaCancellare = [];
174
157
 
175
- try {
176
- for (let t of tracks) {
177
- node.warn(`[warn] [hik-media-buffer:test] [STEP 4] Elaborazione traccia ${t.label} (ID: ${t.id})`);
178
-
179
- const evMinuscolo = eventoNVR.toLowerCase();
180
-
181
- if (t.isFoto) {
182
- // --- SE È LA FOTO: USA IL TUO XML ORIGINALE IDENTICO ---
183
- const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${t.id}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>100</maxResults><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${evMinuscolo}</metadataDescriptor></metadataList></CMSearchDescription>`;
184
-
185
- node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST XML /search per FOTO. XML:\n${searchXml}`);
186
-
187
- try {
188
- const resSearch = await camAuth.request({
189
- method: 'POST',
190
- url: `${baseUrl}/search`,
191
- data: searchXml,
192
- headers: { "Content-Type": "application/xml" }
193
- });
194
-
195
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
196
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
197
- if (uriMatch) {
198
- const rawUri = uriMatch[1].replace(/&amp;/g, '&');
199
- await scaricaIlFile(rawUri, t.isFoto);
200
- }
201
- } catch (err) {
202
- node.error(`[error] [hik-media-buffer:test] Errore FOTO XML: ${err.message}`);
203
- }
204
-
205
- } else {
206
- // --- SE È IL VIDEO: USA IL JSON DELL'ISPEZIONA NVR CHE ANDAVA ---
207
- // Adattiamo le date al formato del JSON dell'NVR (senza Z, con spazio offset se necessario, o UTC pulito)
208
- const startTimeJson = startTime.replace('Z', ' 02:00'); // adatta al fuso orario del tuo NVR se serve, o lascialo Z
209
- const endTimeJson = endTime.replace('Z', ' 02:00');
210
-
211
- const searchJsonPayload = {
212
- "EventSearchDescription": {
213
- "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
214
- "searchResultPosition": 0,
215
- "maxResults": 30,
216
- "timeSpanList": [{
217
- "startTime": startTime.replace('Z', ''), // Prova formato pulito senza Z richiesto dal JSON
218
- "endTime": endTime.replace('Z', '')
219
- }],
220
- "type": "all",
221
- "channels": [parsedChannel],
222
- "eventType": "behavior",
223
- "behavior": {
224
- "behaviorEventType": evMinuscolo
225
- }
226
- }
227
- };
228
-
229
- node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST JSON /eventRecordSearch per VIDEO...`);
230
-
231
- try {
232
- const resSearchVideo = await camAuth.request({
233
- method: 'POST',
234
- url: `${baseUrl}/eventRecordSearch?format=json`,
235
- data: searchJsonPayload,
236
- headers: { "Content-Type": "application/json" }
237
- });
238
-
239
- const resDataStr = JSON.stringify(resSearchVideo.data);
240
- const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
241
-
242
- if (videoUriMatch) {
243
- let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
244
- await scaricaIlFile(rawVideoUri, t.isFoto);
245
- }
246
- } catch (err) {
247
- node.error(`[error] [hik-media-buffer:test] Errore VIDEO JSON: ${err.message}`);
248
- }
158
+ // --- STRUTTURA JSON DALL'ISPEZIONA NVR NATIVO ---
159
+ const searchJsonPayload = {
160
+ "EventSearchDescription": {
161
+ "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
162
+ "searchResultPosition": 0,
163
+ "maxResults": 30,
164
+ "timeSpanList": [{
165
+ "startTime": startTimeJson,
166
+ "endTime": endTimeJson
167
+ }],
168
+ "type": "all",
169
+ "channels": [parsedChannel],
170
+ "eventType": "behavior",
171
+ "behavior": {
172
+ "behaviorEventType": evMinuscolo
249
173
  }
250
174
  }
175
+ };
251
176
 
252
- // Funzione interna di supporto per evitare duplicazione di codice di download
253
- async function scaricaIlFile(playbackUri, isFoto) {
254
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5] Scarico traccia. URI: ${playbackUri}`);
255
- const resDown = await camAuth.request({
256
- method: 'GET',
257
- url: `${baseUrl}/download`,
258
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${playbackUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
259
- responseType: 'arraybuffer'
177
+ node.warn(`[warn] [hik-media-buffer:test] Invio query JSON a eventRecordSearch per canale ${parsedChannel}...`);
178
+
179
+ try {
180
+ const resSearchVideo = await camAuth.request({
181
+ method: 'POST',
182
+ url: `${baseUrl}/eventRecordSearch?format=json`,
183
+ data: searchJsonPayload,
184
+ headers: { "Content-Type": "application/json" }
260
185
  });
261
186
 
262
- let buffer = Buffer.from(resDown.data);
263
- if (isFoto) {
264
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
265
- fs.writeFileSync(fullImgPath, buffer);
266
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
267
- fileDaCancellare.push(fullImgPath);
268
- } else {
269
- if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
187
+ const resDataStr = JSON.stringify(resSearchVideo.data);
188
+ // Estrae l'RTSP playbackURI o l'endpoint di download dal JSON di risposta
189
+ const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
190
+
191
+ if (videoUriMatch) {
192
+ let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
193
+ node.warn(`[warn] [hik-media-buffer:test] Trovato playbackURI video nel JSON: ${rawVideoUri}`);
194
+
195
+ // Scarichiamo il filmato dall'NVR
196
+ const resDownVideo = await camAuth.request({
197
+ method: 'GET',
198
+ url: `${baseUrl}/download`,
199
+ data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawVideoUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
200
+ responseType: 'arraybuffer'
201
+ });
202
+
203
+ let videoBuffer = Buffer.from(resDownVideo.data);
204
+ if (videoBuffer.slice(0, 4).toString() === 'IMKH') videoBuffer = videoBuffer.slice(40);
205
+
270
206
  const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
271
207
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
272
- fs.writeFileSync(rawPath, buffer);
208
+ const extractedImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
273
209
 
210
+ fs.writeFileSync(rawPath, videoBuffer);
211
+ fileDaCancellare.push(rawPath);
212
+
213
+ node.warn(`[warn] [hik-media-buffer:test] Esecuzione FFMPEG simultanea (Estrazione frame JPG + conversione video MP4)...`);
274
214
  await new Promise((resolve) => {
275
- exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
276
- if (!err && fs.existsSync(fixedPath)) {
277
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
278
- fileDaCancellare.push(fixedPath);
215
+ // FFMPEG estrae il primo secondo in JPG e indicizza il video in FastStart in un colpo solo
216
+ exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
217
+ if (!err) {
218
+ if (fs.existsSync(extractedImgPath)) {
219
+ output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
220
+ fileDaCancellare.push(extractedImgPath);
221
+ node.warn(`[warn] [hik-media-buffer:test] Foto estratta dal video correttamente.`);
222
+ }
223
+ if (fs.existsSync(fixedPath)) {
224
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
225
+ fileDaCancellare.push(fixedPath);
226
+ node.warn(`[warn] [hik-media-buffer:test] Video ottimizzato convertito correttamente.`);
227
+ }
279
228
  } else {
229
+ node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, invio video originale grezzo: ${err.message}`);
280
230
  output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
281
231
  }
282
- fileDaCancellare.push(rawPath);
283
232
  resolve();
284
233
  });
285
234
  });
286
- }
287
- }
288
-
289
- if (output.foto_base64 || output.video_base64) {
290
- node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
291
- node.send({ payload: output });
292
-
293
- setTimeout(() => {
294
- for (let file of fileDaCancellare) {
295
- try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
296
- }
297
- }, 120000);
298
235
  } else {
299
- node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
236
+ node.warn(`[warn] [hik-media-buffer:test] Nessun file video trovato nel database JSON dell'NVR.`);
300
237
  }
238
+ } catch (videoErr) {
239
+ node.error(`[error] [hik-media-buffer:test] Errore critico durante la ricerca JSON: ${videoErr.message}`);
240
+ }
301
241
 
302
- } catch (e) {
303
- node.error(`[error] [hik-media-buffer:test] [FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
242
+ // --- SPEDIZIONE VERSO PYTHON ---
243
+ if (output.foto_base64 || output.video_base64) {
244
+ node.warn(`[warn] [hik-media-buffer:test] Spedizione payload multimediale completata!`);
245
+ node.send({ payload: output });
246
+
247
+ setTimeout(() => {
248
+ for (let file of fileDaCancellare) {
249
+ try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
250
+ }
251
+ }, 120000);
252
+ } else {
253
+ node.warn(`[warn] [hik-media-buffer:test] Nessun file multimediale generato.`);
304
254
  }
305
255
  updateNodeStatus();
306
256
  }
307
257
 
308
- // --- ALERT STREAM ORIGINALE ---
258
+ // --- ALERT STREAM ---
309
259
  function startAlertStream() {
310
260
  if (isClosing) return;
311
261
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.65",
3
+ "version": "1.1.67",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",