node-red-contrib-hik-media-buffer 1.1.16 → 1.1.19

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