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

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