node-red-contrib-hik-media-buffer 1.1.82 → 1.1.83

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 +62 -86
  2. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- const got = require('got'); // Assicurati di aver installato got@11 nel container!
1
+ const axios = require('axios');
2
+ const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
2
3
  const fs = require('fs');
3
4
  const path = require('path');
4
5
  const os = require('os');
@@ -16,15 +17,13 @@ module.exports = function(RED) {
16
17
  node.user = config.user;
17
18
  node.pass = config.pass;
18
19
 
19
- let activeStream = null;
20
+ let streamRequest = null;
20
21
  let isClosing = false;
21
22
  let lastTriggerTime = {};
22
23
  let nvrOnline = true;
23
- let statoCamera = {}; // Indicizzato per ChannelID: true = online, false = offline
24
+ let statoCamera = {};
24
25
 
25
26
  const EventList = ["FieldDetection", "LineDetection"];
26
-
27
- // CARTELLA TEMPORANEA
28
27
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
29
28
  if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
30
29
 
@@ -32,24 +31,18 @@ module.exports = function(RED) {
32
31
 
33
32
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
34
33
 
35
- // Client base di GOT configurato con il Digest nativo per l'NVR
36
- const nvrClient = got.extend({
37
- prefixUrl: `${node.protocol}://${node.host}:${node.port}/ISAPI`,
38
- username: node.user,
39
- password: node.pass,
40
- authentication: 'digest',
41
- retry: 0,
42
- https: {
43
- rejectUnauthorized: false
44
- }
45
- });
34
+ // Unica libreria di autenticazione usata per tutto il flusso
35
+ const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
46
36
 
47
- // --- PRENDE IL NOME DELLA TELECAMERA DIRETTAMENTE DALL'NVR ---
37
+ // --- PRENDE IL NOME DELLA TELECAMERA DALL'NVR ---
48
38
  async function getCameraName(channelID) {
49
39
  try {
50
- const res = await nvrClient.get(`System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`, { timeout: 5000 });
51
- const data = res.body.toString();
52
- const match = data.match(/<name>([^<]+)<\/name>/i);
40
+ const res = await nvrAuthAxios.request({
41
+ method: 'GET',
42
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
43
+ timeout: 5000
44
+ });
45
+ const match = res.data.toString().match(/<name>([^<]+)<\/name>/i);
53
46
  if (match && match[1]) return match[1].trim();
54
47
  return `Canale_${channelID}`;
55
48
  } catch (e) {
@@ -61,11 +54,12 @@ module.exports = function(RED) {
61
54
  async function checkCameras() {
62
55
  if (isClosing || !nvrOnline) return;
63
56
  try {
64
- // Interroghiamo lo stato di tutti i canali IP proxy sull'NVR
65
- const res = await nvrClient.get('ContentMgmt/InputProxy/channels/status', { timeout: 8000 });
66
- const xml = res.body.toString();
67
-
68
- // Estraiamo tutti i blocchi <InputProxyChannelStatus> presenti nell'XML dell'NVR
57
+ const res = await nvrAuthAxios.request({
58
+ method: 'GET',
59
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
60
+ timeout: 8000
61
+ });
62
+ const xml = res.data.toString();
69
63
  const statusBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/gi) || [];
70
64
 
71
65
  for (let block of statusBlocks) {
@@ -77,13 +71,11 @@ module.exports = function(RED) {
77
71
  const isOnlineNow = statusMatch[1].trim().toLowerCase() === 'true';
78
72
 
79
73
  if (isOnlineNow && statoCamera[ch] === false) {
80
- // La telecamera è tornata online!
81
74
  const nomeOnline = await getCameraName(ch);
82
75
  node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, channel: ch, msg: "Camera ripristinata" } });
83
76
  statoCamera[ch] = true;
84
77
  } else if (!isOnlineNow && statoCamera[ch] !== false && statoCamera[ch] !== undefined) {
85
- // La telecamera si è scollegata! (Ignoriamo il primo avvio se è già scollegata per non intasare di alert)
86
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${ch}`, channel: ch, msg: "Camera scollegata o non raggiungibile dall'NVR" } });
78
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${ch}`, channel: ch, msg: "Camera scollegata dall'NVR" } });
87
79
  statoCamera[ch] = false;
88
80
  } else if (statoCamera[ch] === undefined) {
89
81
  statoCamera[ch] = isOnlineNow;
@@ -91,7 +83,7 @@ module.exports = function(RED) {
91
83
  }
92
84
  }
93
85
  } catch (e) {
94
- node.error(`[DEBUG STATUS] Errore lettura canali da NVR: ${e.message}`);
86
+ // Silenzioso
95
87
  }
96
88
  updateNodeStatus();
97
89
  }
@@ -109,7 +101,7 @@ module.exports = function(RED) {
109
101
 
110
102
  const heartbeatInterval = setInterval(checkCameras, 30000);
111
103
 
112
- // --- DOWNLOAD MEDIA AUTOMATICO DA EVENTO NVR ---
104
+ // --- DOWNLOAD MEDIA UTILIZZANDO SOLO AXIOS ---
113
105
  async function downloadMedia(evento, channelID) {
114
106
  const chStr = channelID.toString();
115
107
  const nowTime = Date.now();
@@ -127,16 +119,9 @@ module.exports = function(RED) {
127
119
  await new Promise(resolve => setTimeout(resolve, 6000));
128
120
 
129
121
  let output = {
130
- tipo_messaggio: "evento",
131
- nome_cliente: node.name,
132
- nome_telecamera: nomeCamera,
133
- ip_telecamera: null,
134
- tipo_evento: evento,
135
- timestamp_epoch: timestamp,
136
- stato_telecamera: "ONLINE",
137
- channel: chStr,
138
- foto_base64: null,
139
- video_base64: null
122
+ tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
123
+ ip_telecamera: null, tipo_evento: evento, timestamp_epoch: timestamp,
124
+ stato_telecamera: "ONLINE", channel: chStr, foto_base64: null, video_base64: null
140
125
  };
141
126
 
142
127
  let fileDaCancellare = [];
@@ -154,24 +139,32 @@ module.exports = function(RED) {
154
139
  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><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${evento}</metadataDescriptor></metadataList></CMSearchDescription>`;
155
140
  }
156
141
 
157
- const resSearch = await nvrClient.post('ContentMgmt/search', {
158
- body: searchXml,
159
- headers: { "Content-Type": "application/xml" }
142
+ // Forza l'XML in un buffer per evitare che Axios sballi la lunghezza della stringa HTTP
143
+ const xmlBuffer = Buffer.from(searchXml, 'utf-8');
144
+
145
+ const resSearch = await nvrAuthAxios.request({
146
+ method: 'POST',
147
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
148
+ data: xmlBuffer,
149
+ headers: {
150
+ "Content-Type": "application/xml; charset=UTF-8",
151
+ "Content-Length": xmlBuffer.length
152
+ }
160
153
  });
161
154
 
162
- let xml = resSearch.body.replace(/<(\/?)\w+:/g, "<$1");
155
+ let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
163
156
  const uriMatch = xml.match(/<playbackURI>([^<]+)</);
164
157
 
165
158
  if (uriMatch) {
166
159
  const rawUri = uriMatch[1].replace(/&amp;/g, '&');
167
160
 
168
- const resDown = await nvrClient.post('ContentMgmt/download', {
169
- body: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
170
- responseType: 'buffer',
171
- headers: { "Content-Type": "application/xml" }
161
+ const resDown = await nvrAuthAxios.request({
162
+ method: 'GET',
163
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(rawUri)}`,
164
+ responseType: 'arraybuffer'
172
165
  });
173
166
 
174
- let buffer = resDown.body;
167
+ let buffer = Buffer.from(resDown.data);
175
168
  if (t.id === trackI) {
176
169
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
177
170
  fs.writeFileSync(fullImgPath, buffer);
@@ -201,7 +194,6 @@ module.exports = function(RED) {
201
194
 
202
195
  if (output.foto_base64 || output.video_base64) {
203
196
  node.send({ payload: output });
204
-
205
197
  setTimeout(() => {
206
198
  for (let file of fileDaCancellare) {
207
199
  try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
@@ -215,48 +207,34 @@ module.exports = function(RED) {
215
207
  updateNodeStatus();
216
208
  }
217
209
 
218
- // --- ALERT STREAM ---
210
+ // --- ALERT STREAM CON AXIOS ---
219
211
  function startAlertStream() {
220
212
  if (isClosing) return;
221
-
222
- const streamUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
213
+ const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
223
214
 
224
- const downloadStream = got.stream(streamUrl, {
225
- username: node.user,
226
- password: node.pass,
227
- authentication: 'digest',
228
- https: { rejectUnauthorized: false }
229
- });
230
-
231
- activeStream = downloadStream;
232
-
233
- downloadStream.on('response', () => {
215
+ nvrAuthAxios.request({ method: 'GET', url: url, responseType: 'stream' })
216
+ .then(response => {
217
+ streamRequest = response;
234
218
  if (!nvrOnline) {
235
219
  node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } });
236
220
  nvrOnline = true;
237
221
  }
238
222
  updateNodeStatus();
239
- });
240
-
241
- downloadStream.on('data', (chunk) => {
242
- const data = chunk.toString().toLowerCase();
243
- if (data.includes("active")) {
244
- const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
245
- if (chMatch) {
246
- let ev = "Unknown";
247
- for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
248
- downloadMedia(ev, chMatch[1]);
223
+
224
+ response.data.on('data', (chunk) => {
225
+ const data = chunk.toString().toLowerCase();
226
+ if (data.includes("active")) {
227
+ const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
228
+ if (chMatch) {
229
+ let ev = "Unknown";
230
+ for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
231
+ downloadMedia(ev, chMatch[1]);
232
+ }
249
233
  }
250
- }
251
- });
252
-
253
- downloadStream.on('error', (err) => {
254
- if (!isClosing) handleNvrError();
255
- });
256
-
257
- downloadStream.on('end', () => {
258
- if (!isClosing) setTimeout(startAlertStream, 5000);
259
- });
234
+ });
235
+ response.data.on('error', () => handleNvrError());
236
+ response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
237
+ }).catch(() => handleNvrError());
260
238
  }
261
239
 
262
240
  function handleNvrError() {
@@ -274,9 +252,7 @@ module.exports = function(RED) {
274
252
  node.on('close', (done) => {
275
253
  isClosing = true;
276
254
  clearInterval(heartbeatInterval);
277
- if (activeStream) {
278
- try { activeStream.destroy(); } catch(e){}
279
- }
255
+ if (streamRequest) streamRequest.data.destroy();
280
256
  done();
281
257
  });
282
258
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.82",
3
+ "version": "1.1.83",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",