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

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 +123 -123
  2. package/package.json +1 -1
@@ -1,11 +1,8 @@
1
- const axios = require('axios');
2
- const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
3
- const https = require('https');
1
+ const got = require('got'); // Assicurati di aver installato got@11 nel container!
4
2
  const fs = require('fs');
5
3
  const path = require('path');
6
4
  const os = require('os');
7
5
  const { exec } = require('child_process');
8
- const { channel } = require('diagnostics_channel');
9
6
 
10
7
  module.exports = function(RED) {
11
8
  function HikMediaBufferNode(config) {
@@ -18,16 +15,13 @@ module.exports = function(RED) {
18
15
  node.protocol = config.protocol || "http";
19
16
  node.user = config.user;
20
17
  node.pass = config.pass;
21
- node.camPass = config.camPass || config.pass;
22
- node.cameras = config.cameras || [];
23
18
 
24
- let streamRequest = null;
19
+ let activeStream = null;
25
20
  let isClosing = false;
26
21
  let lastTriggerTime = {};
27
22
  let nvrOnline = true;
28
- let statoCamera = {};
23
+ let statoCamera = {}; // Indicizzato per ChannelID: true = online, false = offline
29
24
 
30
- const httpsAgent = new https.Agent({ rejectUnauthorized: false });
31
25
  const EventList = ["FieldDetection", "LineDetection"];
32
26
 
33
27
  // CARTELLA TEMPORANEA
@@ -38,60 +32,66 @@ module.exports = function(RED) {
38
32
 
39
33
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
40
34
 
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
- });
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
+ });
47
46
 
47
+ // --- PRENDE IL NOME DELLA TELECAMERA DIRETTAMENTE DALL'NVR ---
48
+ async function getCameraName(channelID) {
48
49
  try {
49
- const res = await camAuth.request({
50
- 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
54
- });
55
-
56
- const data = res.data.toString();
50
+ const res = await nvrClient.get(`System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`, { timeout: 5000 });
51
+ const data = res.body.toString();
57
52
  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
-
53
+ if (match && match[1]) return match[1].trim();
54
+ return `Canale_${channelID}`;
65
55
  } catch (e) {
66
- return `Camera_${cam.channel}`;
56
+ return `Camera_${channelID}`;
67
57
  }
68
58
  }
69
59
 
70
- // --- CONTROLLO STATUS CAMERE ---
60
+ // --- CONTROLLO STATUS CAMERE AUTOMATICO DALL'NVR ---
71
61
  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;
62
+ if (isClosing || !nvrOnline) return;
63
+ 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
69
+ const statusBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/gi) || [];
70
+
71
+ for (let block of statusBlocks) {
72
+ const idMatch = block.match(/<id>(\d+)<\/id>/i);
73
+ const statusMatch = block.match(/<online>([^<]+)<\/online>/i);
74
+
75
+ if (idMatch && statusMatch) {
76
+ const ch = idMatch[1].trim();
77
+ const isOnlineNow = statusMatch[1].trim().toLowerCase() === 'true';
78
+
79
+ if (isOnlineNow && statoCamera[ch] === false) {
80
+ // La telecamera è tornata online!
81
+ const nomeOnline = await getCameraName(ch);
82
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, channel: ch, msg: "Camera ripristinata" } });
83
+ statoCamera[ch] = true;
84
+ } 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" } });
87
+ statoCamera[ch] = false;
88
+ } else if (statoCamera[ch] === undefined) {
89
+ statoCamera[ch] = isOnlineNow;
90
+ }
93
91
  }
94
92
  }
93
+ } catch (e) {
94
+ node.error(`[DEBUG STATUS] Errore lettura canali da NVR: ${e.message}`);
95
95
  }
96
96
  updateNodeStatus();
97
97
  }
@@ -109,21 +109,15 @@ module.exports = function(RED) {
109
109
 
110
110
  const heartbeatInterval = setInterval(checkCameras, 30000);
111
111
 
112
- // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
112
+ // --- DOWNLOAD MEDIA AUTOMATICO DA EVENTO NVR ---
113
113
  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
-
114
+ const chStr = channelID.toString();
119
115
  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;
116
+ if (!lastTriggerTime[chStr]) lastTriggerTime[chStr] = 0;
117
+ if (nowTime - lastTriggerTime[chStr] < 5000) return;
118
+ lastTriggerTime[chStr] = nowTime;
125
119
 
126
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
120
+ const nomeCamera = await getCameraName(channelID);
127
121
  const referenceTime = new Date();
128
122
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
129
123
  const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
@@ -132,73 +126,63 @@ module.exports = function(RED) {
132
126
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
133
127
  await new Promise(resolve => setTimeout(resolve, 6000));
134
128
 
135
- const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
136
-
137
- // struttura del payload per Python
138
129
  let output = {
139
130
  tipo_messaggio: "evento",
140
131
  nome_cliente: node.name,
141
132
  nome_telecamera: nomeCamera,
142
- ip_telecamera: camera.ip,
133
+ ip_telecamera: null,
143
134
  tipo_evento: evento,
144
135
  timestamp_epoch: timestamp,
145
136
  stato_telecamera: "ONLINE",
146
- channel: channelID.toString(),
137
+ channel: chStr,
147
138
  foto_base64: null,
148
139
  video_base64: null
149
140
  };
150
141
 
151
-
152
142
  let fileDaCancellare = [];
153
143
 
154
144
  try {
155
- let searchXml = ``;
156
- const trackV = (channelID*100+1).toString();
157
- const trackI = (channelID*100+3).toString();
145
+ const trackV = (channelID * 100 + 1).toString();
146
+ const trackI = (channelID * 100 + 3).toString();
158
147
  const tracks = [{ id: trackV }, { id: trackI }];
148
+
159
149
  for (let t of tracks) {
160
- if(t.id===trackV){
150
+ let searchXml = ``;
151
+ if (t.id === trackV) {
161
152
  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{
153
+ } else {
164
154
  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
155
  }
166
156
 
167
- const resSearch = await camAuth.request({
168
- method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
157
+ const resSearch = await nvrClient.post('ContentMgmt/search', {
158
+ body: searchXml,
159
+ headers: { "Content-Type": "application/xml" }
169
160
  });
170
161
 
171
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
162
+ let xml = resSearch.body.replace(/<(\/?)\w+:/g, "<$1");
172
163
  const uriMatch = xml.match(/<playbackURI>([^<]+)</);
173
164
 
174
165
  if (uriMatch) {
175
166
  const rawUri = uriMatch[1].replace(/&amp;/g, '&');
176
- const resDown = await camAuth.request({
177
- method: 'GET',
178
- url: `${baseUrl}/download`,
179
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
180
- responseType: 'arraybuffer'
167
+
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" }
181
172
  });
182
173
 
183
- let buffer = Buffer.from(resDown.data);
174
+ let buffer = resDown.body;
184
175
  if (t.id === trackI) {
185
- // SALVA FOTO IN LOCALE
186
176
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
187
177
  fs.writeFileSync(fullImgPath, buffer);
188
-
189
- // La convertiamo subito in testo per il payload
190
178
  output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
191
-
192
- // Registriamo il file per la distruzione futura
193
179
  fileDaCancellare.push(fullImgPath);
194
180
  } else {
195
- // SALVA VIDEO IN LOCALE-
196
181
  if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
197
182
  const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
198
183
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
199
184
  fs.writeFileSync(rawPath, buffer);
200
185
 
201
- // Eseguiamo ffmpeg localmente
202
186
  await new Promise((resolve) => {
203
187
  exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
204
188
  if (!err && fs.existsSync(fixedPath)) {
@@ -215,20 +199,12 @@ module.exports = function(RED) {
215
199
  }
216
200
  }
217
201
 
218
- // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
219
202
  if (output.foto_base64 || output.video_base64) {
220
203
  node.send({ payload: output });
221
204
 
222
- // TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
223
205
  setTimeout(() => {
224
206
  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
- }
207
+ try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
232
208
  }
233
209
  }, 120000);
234
210
  }
@@ -242,41 +218,65 @@ module.exports = function(RED) {
242
218
  // --- ALERT STREAM ---
243
219
  function startAlertStream() {
244
220
  if (isClosing) return;
245
- const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
246
- 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 })
248
- .then(response => {
249
- 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; }
221
+
222
+ const streamUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
223
+
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', () => {
234
+ if (!nvrOnline) {
235
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } });
236
+ nvrOnline = true;
237
+ }
251
238
  updateNodeStatus();
252
- response.data.on('data', (chunk) => {
253
- const data = chunk.toString().toLowerCase();
254
- if (data.includes("active")) {
255
- const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
256
- if (chMatch) {
257
- let ev = "Unknown";
258
- for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
259
- downloadMedia(ev, chMatch[1]);
260
- }
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]);
261
249
  }
262
- });
263
- response.data.on('error', () => handleNvrError());
264
- response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
265
- }).catch(() => handleNvrError());
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
+ });
266
260
  }
267
261
 
268
262
  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; }
263
+ if (nvrOnline) {
264
+ node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } });
265
+ nvrOnline = false;
266
+ }
270
267
  updateNodeStatus();
271
268
  if (!isClosing) setTimeout(startAlertStream, 10000);
272
269
  }
273
270
 
274
271
  startAlertStream();
275
272
  setTimeout(checkCameras, 2000);
273
+
276
274
  node.on('close', (done) => {
277
275
  isClosing = true;
278
276
  clearInterval(heartbeatInterval);
279
- if (streamRequest) streamRequest.data.destroy();
277
+ if (activeStream) {
278
+ try { activeStream.destroy(); } catch(e){}
279
+ }
280
280
  done();
281
281
  });
282
282
  }
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.82",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",