node-red-contrib-hik-media-buffer 1.1.81 → 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 +85 -109
  2. package/package.json +1 -1
@@ -1,11 +1,9 @@
1
1
  const axios = require('axios');
2
2
  const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
3
- const https = require('https');
4
3
  const fs = require('fs');
5
4
  const path = require('path');
6
5
  const os = require('os');
7
6
  const { exec } = require('child_process');
8
- const { channel } = require('diagnostics_channel');
9
7
 
10
8
  module.exports = function(RED) {
11
9
  function HikMediaBufferNode(config) {
@@ -18,8 +16,6 @@ module.exports = function(RED) {
18
16
  node.protocol = config.protocol || "http";
19
17
  node.user = config.user;
20
18
  node.pass = config.pass;
21
- node.camPass = config.camPass || config.pass;
22
- node.cameras = config.cameras || [];
23
19
 
24
20
  let streamRequest = null;
25
21
  let isClosing = false;
@@ -27,10 +23,7 @@ module.exports = function(RED) {
27
23
  let nvrOnline = true;
28
24
  let statoCamera = {};
29
25
 
30
- const httpsAgent = new https.Agent({ rejectUnauthorized: false });
31
26
  const EventList = ["FieldDetection", "LineDetection"];
32
-
33
- // CARTELLA TEMPORANEA
34
27
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
35
28
  if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
36
29
 
@@ -38,60 +31,59 @@ module.exports = function(RED) {
38
31
 
39
32
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
40
33
 
41
- // --- PRENDE IL NOME DELLA TELECAMERA ---
42
- async function getCameraName(cam) {
43
- const camAuth = new AxiosDigestAuth({
44
- username: node.user,
45
- password: node.camPass
46
- });
34
+ // Unica libreria di autenticazione usata per tutto il flusso
35
+ const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
47
36
 
37
+ // --- PRENDE IL NOME DELLA TELECAMERA DALL'NVR ---
38
+ async function getCameraName(channelID) {
48
39
  try {
49
- const res = await camAuth.request({
40
+ const res = await nvrAuthAxios.request({
50
41
  method: 'GET',
51
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
52
- timeout: 5000,
53
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
42
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
43
+ timeout: 5000
54
44
  });
55
-
56
- const data = res.data.toString();
57
- const match = data.match(/<name>([^<]+)<\/name>/i);
58
-
59
- if (match && match[1]) {
60
- return match[1].trim();
61
- } else {
62
- return `Canale_${cam.channel}`;
63
- }
64
-
45
+ const match = res.data.toString().match(/<name>([^<]+)<\/name>/i);
46
+ if (match && match[1]) return match[1].trim();
47
+ return `Canale_${channelID}`;
65
48
  } catch (e) {
66
- return `Camera_${cam.channel}`;
49
+ return `Camera_${channelID}`;
67
50
  }
68
51
  }
69
52
 
70
- // --- CONTROLLO STATUS CAMERE ---
53
+ // --- CONTROLLO STATUS CAMERE AUTOMATICO DALL'NVR ---
71
54
  async function checkCameras() {
72
- if (isClosing) return;
73
- for (let cam of node.cameras) {
74
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
75
- try {
76
- await camAuth.request({
77
- method: 'GET',
78
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
79
- timeout: 5000,
80
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
81
- });
82
- if (statoCamera[cam.ip] === false) {
83
- const nomeOnline = await getCameraName(cam);
84
- 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" } });
85
- statoCamera[cam.ip] = true;
86
- } else if (statoCamera[cam.ip] === undefined) {
87
- statoCamera[cam.ip] = true;
88
- }
89
- } catch (e) {
90
- if (statoCamera[cam.ip] !== false) {
91
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${cam.channel}`, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
92
- statoCamera[cam.ip] = false;
55
+ if (isClosing || !nvrOnline) return;
56
+ try {
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();
63
+ const statusBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/gi) || [];
64
+
65
+ for (let block of statusBlocks) {
66
+ const idMatch = block.match(/<id>(\d+)<\/id>/i);
67
+ const statusMatch = block.match(/<online>([^<]+)<\/online>/i);
68
+
69
+ if (idMatch && statusMatch) {
70
+ const ch = idMatch[1].trim();
71
+ const isOnlineNow = statusMatch[1].trim().toLowerCase() === 'true';
72
+
73
+ if (isOnlineNow && statoCamera[ch] === false) {
74
+ const nomeOnline = await getCameraName(ch);
75
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, channel: ch, msg: "Camera ripristinata" } });
76
+ statoCamera[ch] = true;
77
+ } else if (!isOnlineNow && statoCamera[ch] !== false && statoCamera[ch] !== undefined) {
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" } });
79
+ statoCamera[ch] = false;
80
+ } else if (statoCamera[ch] === undefined) {
81
+ statoCamera[ch] = isOnlineNow;
82
+ }
93
83
  }
94
84
  }
85
+ } catch (e) {
86
+ // Silenzioso
95
87
  }
96
88
  updateNodeStatus();
97
89
  }
@@ -109,21 +101,15 @@ module.exports = function(RED) {
109
101
 
110
102
  const heartbeatInterval = setInterval(checkCameras, 30000);
111
103
 
112
- // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
104
+ // --- DOWNLOAD MEDIA UTILIZZANDO SOLO AXIOS ---
113
105
  async function downloadMedia(evento, channelID) {
114
- const camera = node.cameras.find(c => c.channel == channelID);
115
- if (!camera) return;
116
-
117
- const nomeCamera = await getCameraName(camera);
118
-
106
+ const chStr = channelID.toString();
119
107
  const nowTime = Date.now();
120
- if(!lastTriggerTime[camera.ip]){
121
- lastTriggerTime[camera.ip] = 0;
122
- }
123
- if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
124
- lastTriggerTime[camera.ip] = nowTime;
108
+ if (!lastTriggerTime[chStr]) lastTriggerTime[chStr] = 0;
109
+ if (nowTime - lastTriggerTime[chStr] < 5000) return;
110
+ lastTriggerTime[chStr] = nowTime;
125
111
 
126
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
112
+ const nomeCamera = await getCameraName(channelID);
127
113
  const referenceTime = new Date();
128
114
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
129
115
  const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
@@ -132,40 +118,38 @@ module.exports = function(RED) {
132
118
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
133
119
  await new Promise(resolve => setTimeout(resolve, 6000));
134
120
 
135
- const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
136
-
137
- // struttura del payload per Python
138
121
  let output = {
139
- tipo_messaggio: "evento",
140
- nome_cliente: node.name,
141
- nome_telecamera: nomeCamera,
142
- ip_telecamera: camera.ip,
143
- tipo_evento: evento,
144
- timestamp_epoch: timestamp,
145
- stato_telecamera: "ONLINE",
146
- channel: channelID.toString(),
147
- foto_base64: null,
148
- 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
149
125
  };
150
126
 
151
-
152
127
  let fileDaCancellare = [];
153
128
 
154
129
  try {
155
- let searchXml = ``;
156
- const trackV = (channelID*100+1).toString();
157
- const trackI = (channelID*100+3).toString();
130
+ const trackV = (channelID * 100 + 1).toString();
131
+ const trackI = (channelID * 100 + 3).toString();
158
132
  const tracks = [{ id: trackV }, { id: trackI }];
133
+
159
134
  for (let t of tracks) {
160
- if(t.id===trackV){
135
+ let searchXml = ``;
136
+ if (t.id === trackV) {
161
137
  searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackList><trackID>${t.id}</trackID></trackList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>100</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList></CMSearchDescription>`;
162
-
163
- } else{
138
+ } else {
164
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>`;
165
140
  }
166
141
 
167
- const resSearch = await camAuth.request({
168
- method: 'POST', url: `${baseUrl}/search`, data: searchXml, 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
+ }
169
153
  });
170
154
 
171
155
  let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
@@ -173,32 +157,25 @@ module.exports = function(RED) {
173
157
 
174
158
  if (uriMatch) {
175
159
  const rawUri = uriMatch[1].replace(/&amp;/g, '&');
176
- const resDown = await camAuth.request({
160
+
161
+ const resDown = await nvrAuthAxios.request({
177
162
  method: 'GET',
178
- url: `${baseUrl}/download`,
179
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
163
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(rawUri)}`,
180
164
  responseType: 'arraybuffer'
181
165
  });
182
166
 
183
167
  let buffer = Buffer.from(resDown.data);
184
168
  if (t.id === trackI) {
185
- // SALVA FOTO IN LOCALE
186
169
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
187
170
  fs.writeFileSync(fullImgPath, buffer);
188
-
189
- // La convertiamo subito in testo per il payload
190
171
  output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
191
-
192
- // Registriamo il file per la distruzione futura
193
172
  fileDaCancellare.push(fullImgPath);
194
173
  } else {
195
- // SALVA VIDEO IN LOCALE-
196
174
  if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
197
175
  const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
198
176
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
199
177
  fs.writeFileSync(rawPath, buffer);
200
178
 
201
- // Eseguiamo ffmpeg localmente
202
179
  await new Promise((resolve) => {
203
180
  exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
204
181
  if (!err && fs.existsSync(fixedPath)) {
@@ -215,20 +192,11 @@ module.exports = function(RED) {
215
192
  }
216
193
  }
217
194
 
218
- // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
219
195
  if (output.foto_base64 || output.video_base64) {
220
196
  node.send({ payload: output });
221
-
222
- // TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
223
197
  setTimeout(() => {
224
198
  for (let file of fileDaCancellare) {
225
- try {
226
- if (fs.existsSync(file)) {
227
- fs.unlinkSync(file);
228
- }
229
- } catch (err) {
230
- node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
231
- }
199
+ try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
232
200
  }
233
201
  }, 120000);
234
202
  }
@@ -239,16 +207,20 @@ module.exports = function(RED) {
239
207
  updateNodeStatus();
240
208
  }
241
209
 
242
- // --- ALERT STREAM ---
210
+ // --- ALERT STREAM CON AXIOS ---
243
211
  function startAlertStream() {
244
212
  if (isClosing) return;
245
- const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
246
213
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
247
- nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
214
+
215
+ nvrAuthAxios.request({ method: 'GET', url: url, responseType: 'stream' })
248
216
  .then(response => {
249
217
  streamRequest = response;
250
- if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
218
+ if (!nvrOnline) {
219
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } });
220
+ nvrOnline = true;
221
+ }
251
222
  updateNodeStatus();
223
+
252
224
  response.data.on('data', (chunk) => {
253
225
  const data = chunk.toString().toLowerCase();
254
226
  if (data.includes("active")) {
@@ -266,13 +238,17 @@ module.exports = function(RED) {
266
238
  }
267
239
 
268
240
  function handleNvrError() {
269
- if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
241
+ if (nvrOnline) {
242
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } });
243
+ nvrOnline = false;
244
+ }
270
245
  updateNodeStatus();
271
246
  if (!isClosing) setTimeout(startAlertStream, 10000);
272
247
  }
273
248
 
274
249
  startAlertStream();
275
250
  setTimeout(checkCameras, 2000);
251
+
276
252
  node.on('close', (done) => {
277
253
  isClosing = true;
278
254
  clearInterval(heartbeatInterval);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.81",
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",