node-red-contrib-hik-media-buffer 1.1.20 → 1.1.22

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 +57 -96
  2. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  const axios = require('axios');
2
+ const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
2
3
  const https = require('https');
3
4
  const fs = require('fs');
4
5
  const path = require('path');
@@ -27,69 +28,59 @@ module.exports = function(RED) {
27
28
 
28
29
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
29
30
  const EventList = ["FieldDetection", "LineDetection"];
31
+
32
+ // --- CARTELLA TEMPORANEA SUL PC DEL CLIENTE ---
30
33
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
31
-
32
34
  if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
33
35
 
34
36
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
35
37
 
36
38
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
37
39
 
38
- // --- HELPER CENTRALIZZATO PER GLI HEADERS DI AUTENTICAZIONE ---
39
- // È QUI che si concentra la flessibilità richiesta dal tuo capo!
40
- function getAuthHeaders() {
41
- // SCENARIO ATTUALE: Basic Auth standard (o se integrerai un calcolatore Digest custom)
42
- // Axios gestisce nativamente la Basic Auth codificandola in Base64 nell'header
43
- const tokenBase64 = Buffer.from(`${node.user}:${node.camPass}`).toString('base64');
44
-
45
- return {
46
- "Authorization": `Basic ${tokenBase64}`, // <-- OGGI: Usiamo Basic/Digest
47
- // "Authorization": `Bearer ${node.futuroToken}`, // <-- DOMANI: Basterà scommentare questo!
48
- "Content-Type": "application/xml"
49
- };
50
- }
51
-
52
40
  // --- PRENDE IL NOME DELLA TELECAMERA ---
53
41
  async function getCameraName(cam) {
42
+ const camAuth = new AxiosDigestAuth({
43
+ username: node.user,
44
+ password: node.camPass
45
+ });
46
+
54
47
  try {
55
- const res = await axios({
48
+ const res = await camAuth.request({
56
49
  method: 'GET',
57
50
  url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
58
51
  timeout: 5000,
59
- headers: getAuthHeaders(),
60
52
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
61
53
  });
54
+
62
55
  const data = res.data.toString();
63
56
  const match = data.match(/<name>([^<]+)<\/name>/i);
64
- return match && match[1] ? match[1].trim() : `Canale_${cam.channel}`;
65
- } catch (e) { return `Camera_${cam.ip}`; }
57
+
58
+ if (match && match[1]) {
59
+ return match[1].trim();
60
+ } else {
61
+ return `Canale_${cam.channel}`;
62
+ }
63
+
64
+ } catch (e) {
65
+ return `Camera_${cam.ip}`;
66
+ }
66
67
  }
67
68
 
68
69
  // --- CONTROLLO STATUS CAMERE ---
69
70
  async function checkCameras() {
70
71
  if (isClosing) return;
71
72
  for (let cam of node.cameras) {
73
+ const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
72
74
  try {
73
- await axios({
75
+ await camAuth.request({
74
76
  method: 'GET',
75
77
  url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
76
78
  timeout: 5000,
77
- headers: getAuthHeaders(),
78
79
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
79
80
  });
80
81
  if (statoCamera[cam.ip] === false) {
81
82
  const nomeOnline = await getCameraName(cam);
82
-
83
- node.send({
84
- payload: {
85
- tipo_messaggio: "status",
86
- nome_cliente: node.name,
87
- nome_telecamera: nomeOnline,
88
- ip_telecamera: cam.ip,
89
- channel: cam.channel,
90
- stato_telecamera: "ONLINE",
91
- }
92
- });
83
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
93
84
  statoCamera[cam.ip] = true;
94
85
  } else if (statoCamera[cam.ip] === undefined) {
95
86
  statoCamera[cam.ip] = true;
@@ -97,17 +88,7 @@ module.exports = function(RED) {
97
88
  } catch (e) {
98
89
  if (statoCamera[cam.ip] !== false) {
99
90
  const nomeOffline = await getCameraName(cam);
100
-
101
- node.send({
102
- payload: {
103
- tipo_messaggio: "status",
104
- nome_cliente: node.name,
105
- nome_telecamera: nomeOffline,
106
- ip_telecamera: cam.ip,
107
- channel: cam.channel,
108
- stato_telecamera: "OFFLINE",
109
- }
110
- });
91
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: nomeOffline, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
111
92
  statoCamera[cam.ip] = false;
112
93
  }
113
94
  }
@@ -128,7 +109,7 @@ module.exports = function(RED) {
128
109
 
129
110
  const heartbeatInterval = setInterval(checkCameras, 30000);
130
111
 
131
- // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E PULIZIA ---
112
+ // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
132
113
  async function downloadMedia(evento, channelID) {
133
114
  const camera = node.cameras.find(c => c.channel == channelID);
134
115
  const nomeCamera = await getCameraName(camera);
@@ -138,6 +119,7 @@ module.exports = function(RED) {
138
119
  if (nowTime - lastTriggerTime < 10000) return;
139
120
  lastTriggerTime = nowTime;
140
121
 
122
+ const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
141
123
  const referenceTime = new Date();
142
124
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
143
125
  const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
@@ -148,6 +130,7 @@ module.exports = function(RED) {
148
130
 
149
131
  const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
150
132
 
133
+ // Prepariamo la struttura del payload per Python
151
134
  let output = {
152
135
  tipo_messaggio: "evento",
153
136
  nome_cliente: node.name,
@@ -161,6 +144,7 @@ module.exports = function(RED) {
161
144
  video_base64: null
162
145
  };
163
146
 
147
+ // Teniamo traccia dei percorsi per poterli cancellare tra 2 minuti
164
148
  let fileDaCancellare = [];
165
149
 
166
150
  try {
@@ -168,13 +152,8 @@ module.exports = function(RED) {
168
152
  for (let t of tracks) {
169
153
  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.std-cgi.com/${evento}</metadataDescriptor></metadataList></CMSearchDescription>`;
170
154
 
171
- // Usiamo Axios puro con gli header dinamici
172
- const resSearch = await axios({
173
- method: 'POST',
174
- url: `${baseUrl}/search`,
175
- data: searchXml,
176
- headers: getAuthHeaders(),
177
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
155
+ const resSearch = await camAuth.request({
156
+ method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
178
157
  });
179
158
 
180
159
  let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
@@ -182,28 +161,32 @@ module.exports = function(RED) {
182
161
 
183
162
  if (uriMatch) {
184
163
  const rawUri = uriMatch[1].replace(/&amp;/g, '&');
185
-
186
- const resDown = await axios({
164
+ const resDown = await camAuth.request({
187
165
  method: 'GET',
188
166
  url: `${baseUrl}/download`,
189
167
  data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
190
- responseType: 'arraybuffer',
191
- headers: getAuthHeaders(),
192
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
168
+ responseType: 'arraybuffer'
193
169
  });
194
170
 
195
171
  let buffer = Buffer.from(resDown.data);
196
172
  if (t.id === "203") {
173
+ // --- SALVA FOTO IN LOCALE COME PRIMA ---
197
174
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
198
175
  fs.writeFileSync(fullImgPath, buffer);
176
+
177
+ // La convertiamo subito in testo per il payload
199
178
  output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
179
+
180
+ // Registriamo il file per la distruzione futura
200
181
  fileDaCancellare.push(fullImgPath);
201
182
  } else {
183
+ // --- SALVA VIDEO IN LOCALE COME PRIMA ---
202
184
  if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
203
185
  const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
204
186
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
205
187
  fs.writeFileSync(rawPath, buffer);
206
188
 
189
+ // Eseguiamo ffmpeg localmente sul PC del cliente
207
190
  await new Promise((resolve) => {
208
191
  exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
209
192
  if (!err && fs.existsSync(fixedPath)) {
@@ -220,19 +203,26 @@ module.exports = function(RED) {
220
203
  }
221
204
  }
222
205
 
223
- node.send({ payload: output });
224
-
225
- if (fileDaCancellare.length > 0) {
206
+ // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
207
+ if (output.foto_base64 || output.video_base64) {
208
+ node.send({ payload: output });
209
+
210
+ // --- TIMER RIGIDO A 2 MINUTI PER LA PULIZIA DEL DISCO ---
226
211
  setTimeout(() => {
227
212
  for (let file of fileDaCancellare) {
228
- try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
213
+ try {
214
+ if (fs.existsSync(file)) {
215
+ fs.unlinkSync(file);
216
+ }
217
+ } catch (err) {
218
+ node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
219
+ }
229
220
  }
230
- }, 120000);
221
+ }, 120000); // 120.000 ms = 2 minuti esatti
231
222
  }
232
223
 
233
224
  } catch (e) {
234
- node.error(`Errore Download Cam ${channelID} (${e.message}). Invio allarme base.`);
235
- node.send({ payload: output });
225
+ node.error(`Errore Download Cam ${channelID}: ${e.message}`);
236
226
  }
237
227
  updateNodeStatus();
238
228
  }
@@ -240,30 +230,12 @@ module.exports = function(RED) {
240
230
  // --- ALERT STREAM ---
241
231
  function startAlertStream() {
242
232
  if (isClosing) return;
233
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
243
234
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
244
-
245
- axios({
246
- method: 'GET',
247
- url: url,
248
- responseType: 'stream',
249
- headers: getAuthHeaders(),
250
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
251
- })
235
+ nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
252
236
  .then(response => {
253
237
  streamRequest = response;
254
- if (!nvrOnline) {
255
- const timestamp = Math.floor(Date.now() / 1000);
256
- node.send({
257
- payload: {
258
- tipo_messaggio: "status",
259
- nome_cliente: node.name,
260
- ip: node.host,
261
- tipo_evento: "CAMBIO STATO",
262
- stato_telecamera: "ONLINE",
263
- }
264
- });
265
- nvrOnline = true;
266
- }
238
+ if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
267
239
  updateNodeStatus();
268
240
  response.data.on('data', (chunk) => {
269
241
  const data = chunk.toString().toLowerCase();
@@ -282,18 +254,7 @@ module.exports = function(RED) {
282
254
  }
283
255
 
284
256
  function handleNvrError() {
285
- if (nvrOnline) {
286
- node.send({
287
- payload: {
288
- tipo_messaggio: "status",
289
- nome_cliente: node.name,
290
- ip: node.host,
291
- tipo_evento: "CAMBIO STATO",
292
- stato_telecamera: "OFFLINE",
293
- }
294
- });
295
- nvrOnline = false;
296
- }
257
+ if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
297
258
  updateNodeStatus();
298
259
  if (!isClosing) setTimeout(startAlertStream, 10000);
299
260
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",