node-red-contrib-hik-media-buffer 1.1.71 → 1.1.72

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 +138 -137
  2. package/package.json +1 -1
@@ -5,6 +5,7 @@ const fs = require('fs');
5
5
  const path = require('path');
6
6
  const os = require('os');
7
7
  const { exec } = require('child_process');
8
+ const { channel } = require('diagnostics_channel');
8
9
 
9
10
  module.exports = function(RED) {
10
11
  function HikMediaBufferNode(config) {
@@ -12,93 +13,91 @@ module.exports = function(RED) {
12
13
  const node = this;
13
14
 
14
15
  node.name = config.name || "TEST";
15
- node.host = config.host;
16
+ node.host = config.host;
16
17
  node.port = config.port || "80";
17
18
  node.protocol = config.protocol || "http";
18
19
  node.user = config.user;
19
20
  node.pass = config.pass;
21
+ node.camPass = config.camPass || config.pass;
22
+ node.cameras = config.cameras || [];
20
23
 
21
24
  let streamRequest = null;
22
25
  let isClosing = false;
23
26
  let lastTriggerTime = {};
24
27
  let nvrOnline = true;
25
- let statoCanale = {};
28
+ let statoCamera = {};
26
29
 
27
30
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
28
31
  const EventList = ["FieldDetection", "LineDetection"];
29
32
 
33
+ // CARTELLA TEMPORANEA
30
34
  const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
31
35
  if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
32
36
 
33
37
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
34
38
 
35
- function toHikDate(d) {
36
- const pad = (num) => String(num).padStart(2, '0');
37
- return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
38
- }
39
+ function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
40
+
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
+ });
39
47
 
40
- async function getCameraName(channelID) {
41
- const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
42
48
  try {
43
- const res = await nvrAuth.request({
49
+ const res = await camAuth.request({
44
50
  method: 'GET',
45
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
51
+ url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
46
52
  timeout: 5000,
47
53
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
48
54
  });
55
+
49
56
  const data = res.data.toString();
50
57
  const match = data.match(/<name>([^<]+)<\/name>/i);
51
- return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
58
+
59
+ if (match && match[1]) {
60
+ return match[1].trim();
61
+ } else {
62
+ return `Canale_${cam.channel}`;
63
+ }
64
+
52
65
  } catch (e) {
53
- return `Canale_${channelID}`;
66
+ return `Camera_${cam.channel}`;
54
67
  }
55
68
  }
56
69
 
70
+ // --- CONTROLLO STATUS CAMERE ---
57
71
  async function checkCameras() {
58
72
  if (isClosing) return;
59
- const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
60
- try {
61
- const res = await nvrAuth.request({
62
- method: 'GET',
63
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
64
- timeout: 5000,
65
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
66
- });
67
- nvrOnline = true;
68
- const xmlData = res.data.toString();
69
- const bloccoRegex = /<InputProxyChannelStatus[\s\\S]*?>([\s\S]*?)<\/InputProxyChannelStatus>/gi;
70
- let matchBlocco;
71
-
72
- while ((matchBlocco = bloccoRegex.exec(xmlData)) !== null) {
73
- const bloccoContenuto = matchBlocco[1];
74
- const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
75
- const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i);
76
-
77
- if (idMatch && statusMatch) {
78
- const ch = idMatch[1];
79
- const statusStr = statusMatch[1].toLowerCase();
80
- const isOnline = statusStr.includes("online") || statusStr.includes("recording");
81
-
82
- if (isOnline && statoCanale[ch] === false) {
83
- const nomeOnline = await getCameraName(ch);
84
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_nvr: node.host, channel: ch.toString(), msg: "Camera ripristinata su NVR" } });
85
- statoCanale[ch] = true;
86
- } else if (!isOnline && statoCanale[ch] !== false && statoCanale[ch] !== undefined) {
87
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Canale_${ch}`, ip_nvr: node.host, channel: ch.toString(), msg: "Camera scollegata o offline su NVR" } });
88
- statoCanale[ch] = false;
89
- } else if (statoCanale[ch] === undefined) {
90
- statoCanale[ch] = isOnline;
91
- }
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;
92
93
  }
93
94
  }
94
- } catch (e) {
95
- nvrOnline = false;
96
95
  }
97
96
  updateNodeStatus();
98
97
  }
99
98
 
100
99
  function updateNodeStatus() {
101
- const offlineCams = Object.values(statoCanale).filter(v => v === false).length;
100
+ const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
102
101
  if (!nvrOnline) {
103
102
  node.status({fill:"red", shape:"ring", text:"NVR Offline"});
104
103
  } else if (offlineCams > 0) {
@@ -110,35 +109,38 @@ module.exports = function(RED) {
110
109
 
111
110
  const heartbeatInterval = setInterval(checkCameras, 30000);
112
111
 
112
+ // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
113
113
  async function downloadMedia(evento, channelID) {
114
- node.warn(`[TRIGGER] Ricevuto evento. Evento: ${evento}, Canale originario: ${channelID}`);
114
+ const camera = node.cameras.find(c => c.channel == channelID);
115
+ if (!camera) return;
116
+
117
+ const nomeCamera = await getCameraName(camera);
115
118
 
116
119
  const nowTime = Date.now();
117
- if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
118
- if (nowTime - lastTriggerTime[channelID] < 5000) return;
119
- lastTriggerTime[channelID] = nowTime;
120
+ if(!lastTriggerTime[camera.ip]){
121
+ lastTriggerTime[camera.ip] = 0;
122
+ }
123
+ if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
124
+ lastTriggerTime[camera.ip] = nowTime;
120
125
 
121
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
126
+ const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
122
127
  const referenceTime = new Date();
123
128
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
124
-
125
- const startTime = toHikDate(new Date(referenceTime.getTime() - (30 * 1000)));
126
- const endTime = toHikDate(new Date(referenceTime.getTime() + (5 * 1000)));
129
+ const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
130
+ const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
127
131
 
128
132
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
129
- const nomeCamera = await getCameraName(channelID);
133
+ await new Promise(resolve => setTimeout(resolve, 6000));
130
134
 
131
- await new Promise(resolve => setTimeout(resolve, 8000));
135
+ const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
132
136
 
133
- const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
134
- let evMinuscolo = evento.toLowerCase();
135
-
137
+ // struttura del payload per Python
136
138
  let output = {
137
139
  tipo_messaggio: "evento",
138
140
  nome_cliente: node.name,
139
141
  nome_telecamera: nomeCamera,
140
- ip_telecamera: node.host,
141
- tipo_evento: evMinuscolo.includes("linedetection") ? "LineDetection" : "FieldDetection",
142
+ ip_telecamera: camera.ip,
143
+ tipo_evento: evento,
142
144
  timestamp_epoch: timestamp,
143
145
  stato_telecamera: "ONLINE",
144
146
  channel: channelID.toString(),
@@ -146,108 +148,108 @@ module.exports = function(RED) {
146
148
  video_base64: null
147
149
  };
148
150
 
149
- const parsedChannel = parseInt(channelID, 10);
150
151
 
151
- // Query JSON Pulita e Blindata (Ancorata al Canale Reale dell'NVR)
152
- const searchJsonPayload = {
153
- "EventSearchDescription": {
154
- "searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
155
- "searchResultPosition": 0,
156
- "maxResults": 10,
157
- "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
158
- "type": "all",
159
- "channels": [parsedChannel],
160
- "eventType": "behavior",
161
- "behavior": { "behaviorEventType": "all" }
162
- }
163
- };
164
-
165
152
  let fileDaCancellare = [];
166
153
 
167
154
  try {
168
- const resSearchVideo = await camAuth.request({
169
- method: 'POST',
170
- url: `${baseUrl}/eventRecordSearch?format=json`,
171
- data: searchJsonPayload,
172
- headers: { "Content-Type": "application/json" }
173
- });
155
+ const trackV = (channelID*100+1).toString();
156
+ const trackI = (channelID*100+3).toString();
157
+ const tracks = [{ id: trackV }, { id: trackI }];
158
+ for (let t of tracks) {
159
+ 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>`;
174
160
 
175
- const resDataStr = JSON.stringify(resSearchVideo.data);
176
- const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
177
-
178
- if (videoUriMatch) {
179
- let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
180
-
181
- const resDownVideo = await camAuth.request({
182
- method: 'GET',
183
- url: `${baseUrl}/download`,
184
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawVideoUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
185
- responseType: 'arraybuffer'
161
+ const resSearch = await camAuth.request({
162
+ method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
186
163
  });
187
-
188
- let videoBuffer = Buffer.from(resDownVideo.data);
189
- if (videoBuffer.slice(0, 4).toString() === 'IMKH') videoBuffer = videoBuffer.slice(40);
190
164
 
191
- const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
192
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
193
- const extractedImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
194
-
195
- fs.writeFileSync(rawPath, videoBuffer);
196
- fileDaCancellare.push(rawPath);
165
+ let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
166
+ const uriMatch = xml.match(/<playbackURI>([^<]+)</);
167
+
168
+ if (uriMatch) {
169
+ const rawUri = uriMatch[1].replace(/&amp;/g, '&');
170
+ const resDown = await camAuth.request({
171
+ method: 'GET',
172
+ url: `${baseUrl}/download`,
173
+ data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
174
+ responseType: 'arraybuffer'
175
+ });
176
+
177
+ let buffer = Buffer.from(resDown.data);
178
+ if (t.id === "203") {
179
+ // SALVA FOTO IN LOCALE
180
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
181
+ fs.writeFileSync(fullImgPath, buffer);
182
+
183
+ // La convertiamo subito in testo per il payload
184
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
185
+
186
+ // Registriamo il file per la distruzione futura
187
+ fileDaCancellare.push(fullImgPath);
188
+ } else {
189
+ // SALVA VIDEO IN LOCALE-
190
+ if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
191
+ const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
192
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
193
+ fs.writeFileSync(rawPath, buffer);
194
+
195
+ // Eseguiamo ffmpeg localmente
196
+ await new Promise((resolve) => {
197
+ exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
198
+ if (!err && fs.existsSync(fixedPath)) {
199
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
200
+ fileDaCancellare.push(fixedPath);
201
+ } else {
202
+ output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
203
+ }
204
+ fileDaCancellare.push(rawPath);
205
+ resolve();
206
+ });
207
+ });
208
+ }
209
+ }
210
+ }
211
+
212
+ // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
213
+ if (output.foto_base64 || output.video_base64) {
214
+ node.send({ payload: output });
197
215
 
198
- await new Promise((resolve) => {
199
- exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
200
- if (!err) {
201
- if (fs.existsSync(extractedImgPath)) {
202
- output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
203
- fileDaCancellare.push(extractedImgPath);
204
- }
205
- if (fs.existsSync(fixedPath)) {
206
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
207
- fileDaCancellare.push(fixedPath);
216
+ // TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
217
+ setTimeout(() => {
218
+ for (let file of fileDaCancellare) {
219
+ try {
220
+ if (fs.existsSync(file)) {
221
+ fs.unlinkSync(file);
208
222
  }
209
- } else {
210
- output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
223
+ } catch (err) {
224
+ node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
211
225
  }
212
- resolve();
213
- });
214
- });
226
+ }
227
+ }, 120000);
215
228
  }
216
- } catch (videoErr) {
217
- node.error(`[ERROR] Errore esecuzione: ${videoErr.message}`);
218
- }
219
229
 
220
- if (output.foto_base64 || output.video_base64) {
221
- node.send({ payload: output });
222
- setTimeout(() => {
223
- for (let file of fileDaCancellare) {
224
- try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
225
- }
226
- }, 60000);
230
+ } catch (e) {
231
+ node.error(`Errore Download Cam ${channelID}: ${e.message}`);
227
232
  }
228
233
  updateNodeStatus();
229
234
  }
230
235
 
236
+ // --- ALERT STREAM ---
231
237
  function startAlertStream() {
232
238
  if (isClosing) return;
233
239
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
234
240
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
235
-
236
241
  nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
237
242
  .then(response => {
238
243
  streamRequest = response;
239
- if (!nvrOnline) { nvrOnline = true; }
244
+ if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
240
245
  updateNodeStatus();
241
-
242
246
  response.data.on('data', (chunk) => {
243
- if (isClosing) return;
244
247
  const data = chunk.toString().toLowerCase();
245
248
  if (data.includes("active")) {
246
249
  const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
247
250
  if (chMatch) {
248
251
  let ev = "Unknown";
249
252
  for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
250
- if (ev === "Unknown") return;
251
253
  downloadMedia(ev, chMatch[1]);
252
254
  }
253
255
  }
@@ -258,18 +260,17 @@ module.exports = function(RED) {
258
260
  }
259
261
 
260
262
  function handleNvrError() {
261
- nvrOnline = false;
263
+ if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
262
264
  updateNodeStatus();
263
265
  if (!isClosing) setTimeout(startAlertStream, 10000);
264
266
  }
265
267
 
266
268
  startAlertStream();
267
269
  setTimeout(checkCameras, 2000);
268
-
269
270
  node.on('close', (done) => {
270
271
  isClosing = true;
271
272
  clearInterval(heartbeatInterval);
272
- if (streamRequest && streamRequest.data) streamRequest.data.destroy();
273
+ if (streamRequest) streamRequest.data.destroy();
273
274
  done();
274
275
  });
275
276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.71",
3
+ "version": "1.1.72",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",