node-red-contrib-hik-media-buffer 1.1.108 → 1.1.110

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 +159 -193
  2. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  const axios = require('axios');
2
2
  const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
3
+ const https = require('https');
3
4
  const fs = require('fs');
4
5
  const path = require('path');
5
6
  const os = require('os');
@@ -16,6 +17,8 @@ module.exports = function(RED) {
16
17
  node.protocol = config.protocol || "http";
17
18
  node.user = config.user;
18
19
  node.pass = config.pass;
20
+ node.camPass = config.camPass || config.pass;
21
+ node.cameras = config.cameras || [];
19
22
 
20
23
  let streamRequest = null;
21
24
  let isClosing = false;
@@ -23,260 +26,227 @@ module.exports = function(RED) {
23
26
  let nvrOnline = true;
24
27
  let statoCamera = {};
25
28
 
29
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
30
+ const EventList = ["FieldDetection", "LineDetection"];
31
+
32
+ // CARTELLA TEMPORANEA
26
33
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
27
34
  if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
28
35
 
29
36
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
30
37
 
31
- const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
38
+ function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
32
39
 
33
- async function getCameraName(channelID) {
34
- try {
35
- const res = await nvrAuthAxios.request({
36
- method: 'GET',
37
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
38
- timeout: 5000
39
- });
40
- const match = res.data.toString().match(/<name>([^<]+)<\/name>/i);
41
- if (match && match[1]) return match[1].trim();
42
- return `Canale_${channelID}`;
43
- } catch (e) { return `Camera_${channelID}`; }
44
- }
40
+ // --- PRENDE IL NOME DELLA TELECAMERA ---
41
+ async function getCameraName(cam) {
42
+ const camAuth = new AxiosDigestAuth({
43
+ username: node.user,
44
+ password: node.camPass
45
+ });
45
46
 
46
- async function checkCameras() {
47
- if (isClosing || !nvrOnline) return;
48
47
  try {
49
- const res = await nvrAuthAxios.request({
48
+ const res = await camAuth.request({
50
49
  method: 'GET',
51
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
52
- timeout: 8000
50
+ url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
51
+ timeout: 5000,
52
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
53
53
  });
54
- const xml = res.data.toString();
55
- const statusBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/gi) || [];
56
54
 
57
- for (let block of statusBlocks) {
58
- const idMatch = block.match(/<id>(\d+)<\/id>/i);
59
- const statusMatch = block.match(/<online>([^<]+)<\/online>/i);
55
+ const data = res.data.toString();
56
+ 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
+ }
60
63
 
61
- if (idMatch && statusMatch) {
62
- const ch = idMatch[1].trim();
63
- const isOnlineNow = statusMatch[1].trim().toLowerCase() === 'true';
64
+ } catch (e) {
65
+ return `Camera_${cam.channel}`;
66
+ }
67
+ }
64
68
 
65
- if (isOnlineNow && statoCamera[ch] === false) {
66
- const nomeOnline = await getCameraName(ch);
67
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, channel: ch, msg: "Camera ripristinata" } });
68
- statoCamera[ch] = true;
69
- } else if (!isOnlineNow && statoCamera[ch] !== false && statoCamera[ch] !== undefined) {
70
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${ch}`, channel: ch, msg: "Camera scollegata dall'NVR" } });
71
- statoCamera[ch] = false;
72
- } else if (statoCamera[ch] === undefined) { statoCamera[ch] = isOnlineNow; }
69
+ // --- CONTROLLO STATUS CAMERE ---
70
+ async function checkCameras() {
71
+ if (isClosing) return;
72
+ for (let cam of node.cameras) {
73
+ const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
74
+ try {
75
+ await camAuth.request({
76
+ method: 'GET',
77
+ url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
78
+ timeout: 5000,
79
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
80
+ });
81
+ if (statoCamera[cam.ip] === false) {
82
+ 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" } });
84
+ statoCamera[cam.ip] = true;
85
+ } else if (statoCamera[cam.ip] === undefined) {
86
+ statoCamera[cam.ip] = true;
87
+ }
88
+ } catch (e) {
89
+ if (statoCamera[cam.ip] !== false) {
90
+ 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" } });
91
+ statoCamera[cam.ip] = false;
73
92
  }
74
93
  }
75
- } catch (e) {}
94
+ }
76
95
  updateNodeStatus();
77
96
  }
78
97
 
79
98
  function updateNodeStatus() {
80
99
  const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
81
- if (!nvrOnline) { node.status({fill:"red", shape:"ring", text:"NVR Offline"}); }
82
- else if (offlineCams > 0) { node.status({fill:"yellow", shape:"dot", text: `${offlineCams} Cam Offline`}); }
83
- else { node.status({fill:"green", shape:"ring", text:"In ascolto"}); }
100
+ if (!nvrOnline) {
101
+ node.status({fill:"red", shape:"ring", text:"NVR Offline"});
102
+ } else if (offlineCams > 0) {
103
+ node.status({fill:"yellow", shape:"dot", text: `${offlineCams} Cam Offline`});
104
+ } else {
105
+ node.status({fill:"green", shape:"ring", text:"In ascolto"});
106
+ }
84
107
  }
85
108
 
86
109
  const heartbeatInterval = setInterval(checkCameras, 30000);
87
110
 
88
- // --- NUOVA STRATEGIA INDISTRUTTIBILE: DIRETTA SU STREAMING RTSP STORICO ---
111
+ // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
89
112
  async function downloadMedia(evento, channelID) {
90
- const chStr = channelID.toString();
113
+ const camera = node.cameras.find(c => c.channel == channelID);
114
+ if (!camera) return;
115
+
116
+ const nomeCamera = await getCameraName(camera);
117
+
91
118
  const nowTime = Date.now();
92
- if (!lastTriggerTime[chStr]) lastTriggerTime[chStr] = 0;
93
- if (nowTime - lastTriggerTime[chStr] < 5000) return;
94
- lastTriggerTime[chStr] = nowTime;
119
+ if(!lastTriggerTime[camera.ip]){
120
+ lastTriggerTime[camera.ip] = 0;
121
+ }
122
+ if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
123
+ lastTriggerTime[camera.ip] = nowTime;
95
124
 
96
- const nomeCamera = await getCameraName(channelID);
125
+ const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
97
126
  const referenceTime = new Date();
98
127
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
128
+ const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
129
+ const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
99
130
 
100
- // Costruiamo le date giornaliere stile Chrome
101
- const pad = (num) => String(num).padStart(2, '0');
102
- const year = referenceTime.getFullYear();
103
- const month = pad(referenceTime.getMonth() + 1);
104
- const day = pad(referenceTime.getDate());
131
+ node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
132
+ await new Promise(resolve => setTimeout(resolve, 6000));
105
133
 
106
- const startTime = `${year}-${month}-${day}T00:00:00 01:00`;
107
- const endTime = `${year}-${month}-${day}T23:59:59 01:00`;
108
-
109
- node.status({fill:"yellow", shape:"dot", text:`Generazione Sessione...`});
134
+ const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
110
135
 
136
+ // struttura del payload per Python
111
137
  let output = {
112
- tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
113
- ip_telecamera: null, tipo_evento: evento, timestamp_epoch: timestamp,
114
- stato_telecamera: "ONLINE", channel: chStr, foto_base64: null, video_base64: null
138
+ tipo_messaggio: "evento",
139
+ nome_cliente: node.name,
140
+ nome_telecamera: nomeCamera,
141
+ ip_telecamera: camera.ip,
142
+ tipo_evento: evento,
143
+ timestamp_epoch: timestamp,
144
+ stato_telecamera: "ONLINE",
145
+ channel: channelID.toString(),
146
+ foto_base64: null,
147
+ video_base64: null
115
148
  };
116
149
 
150
+
117
151
  let fileDaCancellare = [];
118
152
 
119
153
  try {
120
- // --- STEP 1: CHIAMATA PRE-LOGIN PER ESTREMARE IL COOKIE WEBSESSION ---
121
- // Interroghiamo un endpoint ISAPI leggero in Digest per farci rilasciare il Cookie di sessione
122
- const loginRes = await nvrAuthAxios.request({
123
- method: 'GET',
124
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/Security/sessionLogin/capabilities?format=json`,
125
- timeout: 5000
126
- });
127
-
128
- // Estraiamo il cookie "WebSession" dagli header di risposta
129
- const responseHeaders = loginRes.headers['set-cookie'] || [];
130
- let webSessionValue = '';
131
- let cookieKeyName = 'WebSession';
132
-
133
- for (let cookie of responseHeaders) {
134
- if (cookie.includes('WebSession')) {
135
- const matchCookie = cookie.match(/(WebSession_[a-f0-9]+|WebSession)=([^;]+)/i);
136
- if (matchCookie) {
137
- cookieKeyName = matchCookie[1];
138
- webSessionValue = matchCookie[2];
139
- }
140
- }
141
- }
142
-
143
- // Se l'NVR non ha sputato il cookie, ne generiamo uno fittizio in MD5 basandoci sul timestamp
144
- // perché in molti firmware basta che l'header esista ed abbia lo stesso valore del cookie!
145
- if (!webSessionValue) {
146
- webSessionValue = require('crypto').createHash('md5').update(String(Date.now())).digest('hex');
147
- }
148
-
149
- node.warn(`[DEBUG SESSION] Clonazione Cookie -> ${cookieKeyName}=${webSessionValue}`);
150
-
151
- // --- STEP 2: COSTRUZIONE QUERY DIRETTA CON DATI DI CHROME ---
152
- const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
153
- var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
154
- return v.toString(16).toUpperCase();
155
- });
154
+ const tracks = [{ id: "203" }, { id: "201" }];
155
+ for (let t of tracks) {
156
+ const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${t.id}</trackID></trackIDList><timeSpanList><timeSpan><startTime>2026-06-25T00:00:00Z</startTime><endTime>2026-06-26T23:59:59Z</endTime></timeSpan></timeSpanList><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>recordType.meta.hikvision.com/LineDetection</metadataDescriptor></metadataList></CMSearchDescription>`;
156
157
 
157
- const searchPayload = {
158
- "EventSearchDescription": {
159
- "searchID": dynamicSearchId,
160
- "searchResultPosition": 0,
161
- "maxResults": 30,
162
- "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
163
- "type": "all",
164
- "channels": [ parseInt(channelID) ],
165
- "eventType": "behavior",
166
- "behavior": { "behaviorEventType": evento.toLowerCase() }
167
- }
168
- };
169
-
170
- const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
171
- fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
172
-
173
- // --- STEP 3: LANCE DIRETTO VIA CURL CON I TRE ASSI NELLA MANICA ---
174
- // 1. Content-Type form-urlencoded (anche se mandiamo un JSON, come fa Chrome!)
175
- // 2. Cookie WebSession inserito a mano
176
- // 3. sessiontag identico al valore del cookie
177
- const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
178
-
179
- const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" ` +
180
- `-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" ` +
181
- `-H "X-Requested-With: XMLHttpRequest" ` +
182
- `-H "sessiontag: ${webSessionValue}" ` +
183
- `-b "${cookieKeyName}=${webSessionValue}" ` +
184
- `-d "@${tempJsonPath}" "${targetUrl}"`;
185
-
186
- node.warn(`[DEBUG JSON] Invio query protetta con token di sessione...`);
187
- let jsonResponseRaw = await new Promise((resolve) => { exec(curlCommand, (err, stdout) => resolve(err ? null : stdout)); });
188
-
189
- try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
190
-
191
- if (!jsonResponseRaw) {
192
- node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
193
- updateNodeStatus();
194
- return;
195
- }
196
-
197
- const searchResult = JSON.parse(jsonResponseRaw);
198
- const matches = searchResult?.EventSearchResult?.matchList || [];
199
-
200
- if (matches.length === 0) {
201
- node.warn(`[DEBUG JSON] Zero match trovati. La sessione non è bastata o i parametri differiscono.`);
202
- updateNodeStatus();
203
- return;
204
- }
205
-
206
- // Se arriviamo qui, l'array si è sbloccato!
207
- const matchItem = matches[0];
208
- const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
209
-
210
- if (playbackUri) {
211
- node.warn(`[DEBUG JSON] Match sbloccato con successo! URL: ${playbackUri}`);
158
+ const resSearch = await camAuth.request({
159
+ method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
160
+ });
212
161
 
213
- // --- DOWNLOAD FOTO NATIVA REGISTRATA SU HDD ---
214
- let pictureUrlPath = null;
215
- const urlParamsMatch = playbackUri.match(/\?(.*)/);
216
- if (urlParamsMatch) {
217
- const trackI = (channelID * 100 + 3).toString();
218
- pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
219
- }
220
-
221
- if (pictureUrlPath) {
222
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
223
- const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" -b "${cookieKeyName}=${webSessionValue}" -H "sessiontag: ${webSessionValue}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
224
- await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
225
- if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
162
+ let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
163
+ const uriMatch = xml.match(/<playbackURI>([^<]+)</);
164
+
165
+ if (uriMatch) {
166
+ const rawUri = uriMatch[1].replace(/&amp;/g, '&');
167
+ const resDown = await camAuth.request({
168
+ method: 'GET',
169
+ url: `${baseUrl}/download`,
170
+ data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
171
+ responseType: 'arraybuffer'
172
+ });
173
+
174
+ let buffer = Buffer.from(resDown.data);
175
+ if (t.id === "203") {
176
+ // SALVA FOTO IN LOCALE
177
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
178
+ fs.writeFileSync(fullImgPath, buffer);
179
+
180
+ // La convertiamo subito in testo per il payload
226
181
  output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
182
+
183
+ // Registriamo il file per la distruzione futura
227
184
  fileDaCancellare.push(fullImgPath);
185
+ } else {
186
+ // SALVA VIDEO IN LOCALE-
187
+ if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
188
+ const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
189
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
190
+ fs.writeFileSync(rawPath, buffer);
191
+
192
+ // Eseguiamo ffmpeg localmente
193
+ await new Promise((resolve) => {
194
+ exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
195
+ if (!err && fs.existsSync(fixedPath)) {
196
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
197
+ fileDaCancellare.push(fixedPath);
198
+ } else {
199
+ output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
200
+ }
201
+ fileDaCancellare.push(rawPath);
202
+ resolve();
203
+ });
204
+ });
228
205
  }
229
206
  }
230
-
231
- // --- DOWNLOAD VIDEO NATIVO REGISTRATO SU HDD VIA FFMPEG ---
232
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
233
- const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
234
- const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
235
-
236
- await new Promise((resolve) => { exec(ffmpegRtspCmd, { timeout: 25000 }, () => resolve()); });
237
-
238
- if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
239
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
240
- fileDaCancellare.push(fixedPath);
241
- }
242
207
  }
243
208
 
209
+ // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
244
210
  if (output.foto_base64 || output.video_base64) {
245
211
  node.send({ payload: output });
212
+
213
+ // TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
246
214
  setTimeout(() => {
247
215
  for (let file of fileDaCancellare) {
248
- try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
216
+ try {
217
+ if (fs.existsSync(file)) {
218
+ fs.unlinkSync(file);
219
+ }
220
+ } catch (err) {
221
+ node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
222
+ }
249
223
  }
250
- }, 60000);
224
+ }, 120000);
251
225
  }
252
226
 
253
227
  } catch (e) {
254
- node.error(`Errore session-cloner: ${e.message}`);
228
+ node.error(`Errore Download Cam ${channelID}: ${e.message}`);
255
229
  }
256
230
  updateNodeStatus();
257
231
  }
258
232
 
259
- // --- ASCOLTO LIVE STREAM DEGLI ALLARMI ---
233
+ // --- ALERT STREAM ---
260
234
  function startAlertStream() {
261
235
  if (isClosing) return;
236
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
262
237
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
263
-
264
- nvrAuthAxios.request({ method: 'GET', url: url, responseType: 'stream' })
238
+ nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
265
239
  .then(response => {
266
240
  streamRequest = response;
267
- if (!nvrOnline) {
268
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } });
269
- nvrOnline = true;
270
- }
241
+ if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
271
242
  updateNodeStatus();
272
-
273
243
  response.data.on('data', (chunk) => {
274
- const data = chunk.toString();
275
- if (data.toLowerCase().includes("active")) {
276
- const chMatch = data.match(/<channelID>(\d+)<\/channelID>/i);
244
+ const data = chunk.toString().toLowerCase();
245
+ if (data.includes("active")) {
246
+ const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
277
247
  if (chMatch) {
278
- let ev = "LineDetection";
279
- if (data.toLowerCase().includes("fielddetection")) ev = "FieldDetection";
248
+ let ev = "Unknown";
249
+ for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
280
250
  downloadMedia(ev, chMatch[1]);
281
251
  }
282
252
  }
@@ -287,17 +257,13 @@ module.exports = function(RED) {
287
257
  }
288
258
 
289
259
  function handleNvrError() {
290
- if (nvrOnline) {
291
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } });
292
- nvrOnline = false;
293
- }
260
+ if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
294
261
  updateNodeStatus();
295
262
  if (!isClosing) setTimeout(startAlertStream, 10000);
296
263
  }
297
264
 
298
265
  startAlertStream();
299
266
  setTimeout(checkCameras, 2000);
300
-
301
267
  node.on('close', (done) => {
302
268
  isClosing = true;
303
269
  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.108",
3
+ "version": "1.1.110",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",