node-red-contrib-hik-media-buffer 1.1.64 → 1.1.66

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 +102 -119
  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,42 @@ 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 ---
115
109
  async function downloadMedia(evento, channelID) {
116
- node.warn(`[warn] [hik-media-buffer:test] [STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
110
+ node.warn(`[warn] [hik-media-buffer:test] [TRIGGER] Ricevuto evento. Evento: ${evento}, Canale originario: ${channelID}`);
117
111
 
118
112
  const nowTime = Date.now();
119
113
  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
- }
114
+ if (nowTime - lastTriggerTime[channelID] < 5000) return;
124
115
  lastTriggerTime[channelID] = nowTime;
125
116
 
126
117
  const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
127
118
  const referenceTime = new Date();
128
119
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
129
120
 
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}`);
121
+ // Orario formattato per il database JSON dell'NVR (Ora locale con indicazione fuso es. 02:00)
122
+ const formatJsonDate = (d) => {
123
+ const pad = (num) => String(num).padStart(2, '0');
124
+ return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} 02:00`;
125
+ };
126
+ const startTimeJson = formatJsonDate(new Date(referenceTime.getTime() - (15 * 1000)));
127
+ const endTimeJson = formatJsonDate(new Date(referenceTime.getTime() + (15 * 1000)));
134
128
 
135
129
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
136
130
  const nomeCamera = await getCameraName(channelID);
137
131
 
138
- node.warn(`[warn] [hik-media-buffer:test] [STEP 3] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
132
+ node.warn(`[warn] [hik-media-buffer:test] Attesa scrittura file su Hard Disk (6s)...`);
139
133
  await new Promise(resolve => setTimeout(resolve, 6000));
140
134
 
141
135
  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
- }
136
+ const evMinuscolo = evento.toLowerCase();
150
137
 
151
138
  let output = {
152
139
  tipo_messaggio: "evento",
153
140
  nome_cliente: node.name,
154
141
  nome_telecamera: nomeCamera,
155
142
  ip_telecamera: node.host,
156
- tipo_evento: eventoNVR,
143
+ tipo_evento: evMinuscolo.includes("linedetection") ? "LineDetection" : "FieldDetection",
157
144
  timestamp_epoch: timestamp,
158
145
  stato_telecamera: "ONLINE",
159
146
  channel: channelID.toString(),
@@ -162,113 +149,109 @@ module.exports = function(RED) {
162
149
  };
163
150
 
164
151
  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
152
  let fileDaCancellare = [];
174
153
 
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})`);
154
+ // --- STRUTTURA JSON DALL'ISPEZIONA NVR NATIVO ---
155
+ const searchJsonPayload = {
156
+ "EventSearchDescription": {
157
+ "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
158
+ "searchResultPosition": 0,
159
+ "maxResults": 30,
160
+ "timeSpanList": [{
161
+ "startTime": startTimeJson,
162
+ "endTime": endTimeJson
163
+ }],
164
+ "type": "all",
165
+ "channels": [parsedChannel],
166
+ "eventType": "behavior",
167
+ "behavior": {
168
+ "behaviorEventType": evMinuscolo
169
+ }
170
+ }
171
+ };
178
172
 
179
- // Questo è il TUO XML ORIGINALE, non è stato toccato un singolo tag.
180
- // Cambia solo il contenuto testuale del descriptor per puntare all'NVR in minuscolo e senza i doppi slash che facevano scattare il "two root tags"
181
- 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/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
173
+ node.warn(`[warn] [hik-media-buffer:test] Invio query JSON a eventRecordSearch per canale ${parsedChannel}...`);
182
174
 
183
- node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
184
- try {
185
- const resSearch = await camAuth.request({
186
- method: 'POST',
187
- url: `${baseUrl}/search`,
188
- data: searchXml,
189
- headers: { "Content-Type": "application/xml" }
190
- });
191
-
192
- node.warn(`[warn] [hik-media-buffer:test] [STEP 4.2] Risposta ricevuta da /search per traccia ${t.id}: Status ${resSearch.status}`);
193
-
194
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
195
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
175
+ try {
176
+ const resSearchVideo = await camAuth.request({
177
+ method: 'POST',
178
+ url: `${baseUrl}/eventRecordSearch?format=json`,
179
+ data: searchJsonPayload,
180
+ headers: { "Content-Type": "application/json" }
181
+ });
196
182
 
197
- if (uriMatch) {
198
- const rawUri = uriMatch[1].replace(/&amp;/g, '&');
199
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5] Trovato playbackURI per traccia ${t.id}: ${rawUri}`);
200
-
201
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.1] Invio richiesta GET /download per traccia ${t.id}`);
202
- const resDown = await camAuth.request({
203
- method: 'GET',
204
- url: `${baseUrl}/download`,
205
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
206
- responseType: 'arraybuffer'
207
- });
183
+ const resDataStr = JSON.stringify(resSearchVideo.data);
184
+ // Estrae l'RTSP playbackURI o l'endpoint di download dal JSON di risposta
185
+ const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
186
+
187
+ if (videoUriMatch) {
188
+ let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
189
+ node.warn(`[warn] [hik-media-buffer:test] Trovato playbackURI video nel JSON: ${rawVideoUri}`);
190
+
191
+ // Scarichiamo il filmato dall'NVR
192
+ const resDownVideo = await camAuth.request({
193
+ method: 'GET',
194
+ url: `${baseUrl}/download`,
195
+ data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawVideoUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
196
+ responseType: 'arraybuffer'
197
+ });
208
198
 
209
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.2] Scaricati ${resDown.data.byteLength} byte per traccia ${t.id}`);
210
- let buffer = Buffer.from(resDown.data);
211
-
212
- if (t.isFoto) {
213
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
214
- fs.writeFileSync(fullImgPath, buffer);
215
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
216
- fileDaCancellare.push(fullImgPath);
217
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.3] Immagine salvata e convertita in Base64.`);
199
+ let videoBuffer = Buffer.from(resDownVideo.data);
200
+ if (videoBuffer.slice(0, 4).toString() === 'IMKH') videoBuffer = videoBuffer.slice(40);
201
+
202
+ const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
203
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
204
+ const extractedImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
205
+
206
+ fs.writeFileSync(rawPath, videoBuffer);
207
+ fileDaCancellare.push(rawPath);
208
+
209
+ node.warn(`[warn] [hik-media-buffer:test] Esecuzione FFMPEG simultanea (Estrazione frame JPG + conversione video MP4)...`);
210
+ await new Promise((resolve) => {
211
+ // FFMPEG estrae il primo secondo in JPG e indicizza il video in FastStart in un colpo solo
212
+ exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
213
+ if (!err) {
214
+ if (fs.existsSync(extractedImgPath)) {
215
+ output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
216
+ fileDaCancellare.push(extractedImgPath);
217
+ node.warn(`[warn] [hik-media-buffer:test] Foto estratta dal video correttamente.`);
218
+ }
219
+ if (fs.existsSync(fixedPath)) {
220
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
221
+ fileDaCancellare.push(fixedPath);
222
+ node.warn(`[warn] [hik-media-buffer:test] Video ottimizzato convertito correttamente.`);
223
+ }
218
224
  } else {
219
- if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
220
- const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
221
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
222
- fs.writeFileSync(rawPath, buffer);
223
-
224
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.3] Video grezzo salvato. Avvio conversione FFMPEG...`);
225
- await new Promise((resolve) => {
226
- exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
227
- if (!err && fs.existsSync(fixedPath)) {
228
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
229
- fileDaCancellare.push(fixedPath);
230
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.4] Conversione FFMPEG completata con successo.`);
231
- } else {
232
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.4] Errore FFMPEG (uso il video grezzo di backup): ${err ? err.message : 'File mancante'}`);
233
- output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
234
- }
235
- fileDaCancellare.push(rawPath);
236
- resolve();
237
- });
238
- });
225
+ node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, invio video originale grezzo: ${err.message}`);
226
+ output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
239
227
  }
240
- } else {
241
- node.warn(`[warn] [hik-media-buffer:test] [STEP 4.3] Nessun playbackURI trovato nel documento di risposta XML per traccia ${t.id}. XML di risposta:\n${resSearch.data}`);
242
- }
243
- } catch (searchError) {
244
- let errorDetails = searchError.message;
245
- if (searchError.response) {
246
- errorDetails += ` | Status: ${searchError.response.status} | Dati Risposta: ${JSON.stringify(searchError.response.data)}`;
247
- }
248
- node.error(`[error] [hik-media-buffer:test] [CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
249
- }
250
- }
251
-
252
- if (output.foto_base64 || output.video_base64) {
253
- node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
254
- node.send({ payload: output });
255
-
256
- setTimeout(() => {
257
- for (let file of fileDaCancellare) {
258
- try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
259
- }
260
- }, 120000);
228
+ resolve();
229
+ });
230
+ });
261
231
  } else {
262
- node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
232
+ node.warn(`[warn] [hik-media-buffer:test] Nessun file video trovato nel database JSON dell'NVR.`);
263
233
  }
234
+ } catch (videoErr) {
235
+ node.error(`[error] [hik-media-buffer:test] Errore critico durante la ricerca JSON: ${videoErr.message}`);
236
+ }
264
237
 
265
- } catch (e) {
266
- node.error(`[error] [hik-media-buffer:test] [FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
238
+ // --- SPEDIZIONE VERSO PYTHON ---
239
+ if (output.foto_base64 || output.video_base64) {
240
+ node.warn(`[warn] [hik-media-buffer:test] Spedizione payload multimediale completata!`);
241
+ node.send({ payload: output });
242
+
243
+ setTimeout(() => {
244
+ for (let file of fileDaCancellare) {
245
+ try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
246
+ }
247
+ }, 120000);
248
+ } else {
249
+ node.warn(`[warn] [hik-media-buffer:test] Nessun file multimediale generato.`);
267
250
  }
268
251
  updateNodeStatus();
269
252
  }
270
253
 
271
- // --- ALERT STREAM ORIGINALE ---
254
+ // --- ALERT STREAM ---
272
255
  function startAlertStream() {
273
256
  if (isClosing) return;
274
257
  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.64",
3
+ "version": "1.1.66",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",