node-red-contrib-hik-media-buffer 1.1.0 → 1.1.2

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 +55 -78
  2. package/package.json +1 -1
@@ -1,21 +1,16 @@
1
- const axios = require('axios');
1
+ const urllib = require('urllib');
2
2
  const https = require('https');
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
  const { exec } = require('child_process');
7
7
 
8
- // --- STRATEGIA SWITCH: DEFINIZIONE MODALITÀ ---
9
- // Valori possibili: 'digest', 'basic', 'bearer'
10
- const AUTH_MODE = 'digest';
11
-
12
8
  module.exports = function(RED) {
13
9
  function HikMediaBufferNode(config) {
14
10
  RED.nodes.createNode(this, config);
15
11
  const node = this;
16
12
 
17
- // --- ASSEGNAZIONE PROPRIETÀ ---
18
- node.name = config.name;
13
+ node.name = config.name || "TEST";
19
14
  node.host = config.host;
20
15
  node.port = config.port || "80";
21
16
  node.protocol = config.protocol || "http";
@@ -24,21 +19,43 @@ module.exports = function(RED) {
24
19
  node.camPass = config.camPass || config.pass;
25
20
  node.cameras = config.cameras || [];
26
21
 
22
+ // --- DEFINIZIONE PERCORSI PROGETTO ---
23
+ // Puntiamo direttamente alla cartella del tuo progetto Docker
24
+ const baseStorage = `C:\\Users\\APerucca\\Documents\\progetto-docker\\storage\\${node.name}`;
25
+ const imgDir = path.join(baseStorage, "allarmi");
26
+ const vidDir = path.join(baseStorage, "video");
27
+
28
+ // Creazione cartelle se non esistono
29
+ [imgDir, vidDir].forEach(dir => {
30
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
31
+ });
32
+
27
33
  let streamRequest = null;
28
34
  let isClosing = false;
29
35
  let lastTriggerTime = 0;
30
36
  let nvrOnline = true;
31
37
  let statoCamera = {};
32
38
 
33
- const httpsAgent = new https.Agent({ rejectUnauthorized: false });
34
- const tempDir = os.tmpdir();
35
39
  const EventList = ["FieldDetection", "LineDetection"];
36
40
 
37
41
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
38
42
 
39
43
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
40
44
 
41
- // --- PRENDE IL NOME DELLA TELECAMERA ---
45
+ async function hikRequest(options) {
46
+ const { method, url, data, responseType, user, pass, headers = {} } = options;
47
+ const res = await urllib.request(url, {
48
+ method: method,
49
+ digestAuth: `${user}:${pass}`,
50
+ content: data,
51
+ headers: headers,
52
+ dataType: responseType === 'arraybuffer' ? 'buffer' : 'text',
53
+ timeout: 15000,
54
+ rejectUnauthorized: false
55
+ });
56
+ return res;
57
+ }
58
+
42
59
  async function getCameraName(cam) {
43
60
  try {
44
61
  const res = await hikRequest({
@@ -47,40 +64,13 @@ module.exports = function(RED) {
47
64
  user: node.user,
48
65
  pass: node.camPass
49
66
  });
50
- const match = res.data.match(/<name>([^<]+)<\/name>/);
67
+ const match = res.data.toString().match(/<name>([^<]+)<\/name>/);
51
68
  return match ? match[1] : `Cam_${cam.channel}`;
52
69
  } catch (e) {
53
- return `Camera_${cam.ip}`; // Fallback se la camera è già offline
70
+ return `Camera_${cam.ip}`;
54
71
  }
55
72
  }
56
73
 
57
- // --- HELPER UNICO PER LE CHIAMATE (STRATEGIA SWITCH) ---
58
- async function hikRequest(options) {
59
- const { method, url, data, responseType, user, pass, headers = {} } = options;
60
-
61
- const requestConfig = {
62
- method,
63
- url,
64
- data,
65
- responseType: responseType || 'data',
66
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined,
67
- timeout: 10000,
68
- headers: { ...headers }
69
- };
70
-
71
- // LOGICA DI AUTENTICAZIONE
72
- if (AUTH_MODE === 'digest' || AUTH_MODE === 'basic') {
73
- // Axios gestisce Basic/Digest internamente se configurato così:
74
- requestConfig.auth = { username: user, password: pass };
75
- } else if (AUTH_MODE === 'bearer') {
76
- // Esempio per Token futuro
77
- requestConfig.headers['Authorization'] = `Bearer ${node.context().get('myToken')}`;
78
- }
79
-
80
- return axios(requestConfig);
81
- }
82
-
83
- // --- CONTROLLO ONLINE TELECAMERE ---
84
74
  async function checkCameras() {
85
75
  if (isClosing) return;
86
76
  for (let cam of node.cameras) {
@@ -123,18 +113,17 @@ module.exports = function(RED) {
123
113
 
124
114
  const heartbeatInterval = setInterval(checkCameras, 30000);
125
115
 
126
- // --- FUNZIONE DOWNLOAD MEDIA ---
127
116
  async function downloadMedia(evento, channelID) {
128
117
  const camera = node.cameras.find(c => c.channel == channelID);
129
- const nomeCamera = await getCameraName(camera);
130
-
131
118
  if (!camera) return;
132
119
 
133
120
  const nowTime = Date.now();
134
121
  if (nowTime - lastTriggerTime < 10000) return;
135
122
  lastTriggerTime = nowTime;
136
123
 
124
+ const nomeCamera = await getCameraName(camera);
137
125
  const referenceTime = new Date();
126
+ const timestamp = Math.floor(referenceTime.getTime() / 1000);
138
127
  const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
139
128
  const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
140
129
 
@@ -142,50 +131,41 @@ module.exports = function(RED) {
142
131
  await new Promise(resolve => setTimeout(resolve, 6000));
143
132
 
144
133
  const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
145
- let output = { ip: camera.ip, nomeCliente: node.name, nome_telecamera: nomeCamera, channel: channelID, event: evento, videoPath: null, imageBuffer: null };
134
+ let output = { ip: camera.ip, nomeCliente: node.name, nome_telecamera: nomeCamera, channel: channelID, event: evento, videoPath: null, imageBuffer: null, imagePath: null };
146
135
 
147
136
  try {
148
137
  const tracks = [{ name: "termicoV", id: "201" }, { name: "termico", id: "203" }];
149
138
  for (let t of tracks) {
150
- const searchXml = `<?xml version="1.0" encoding="utf-8"?>
151
- <CMSearchDescription>
152
- <searchID>LAST_EVENT</searchID>
153
- <trackIDList><trackID>${t.id}</trackID></trackIDList>
154
- <timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList>
155
- <maxResults>100</maxResults>
156
- <metadataList><metadataDescriptor>//recordType.meta.std-cgi.com/${evento}</metadataDescriptor></metadataList>
157
- </CMSearchDescription>`;
139
+ 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>`;
158
140
 
159
141
  const resSearch = await hikRequest({
160
- method: 'POST',
161
- url: `${baseUrl}/search`,
162
- data: searchXml,
163
- headers: { "Content-Type": "application/xml" },
164
- user: node.user,
165
- pass: node.camPass
142
+ method: 'POST', url: `${baseUrl}/search`, data: searchXml,
143
+ headers: { "Content-Type": "application/xml" }, user: node.user, pass: node.camPass
166
144
  });
167
145
 
168
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
146
+ let xml = resSearch.data.toString().replace(/<(\/?)\w+:/g, "<$1");
169
147
  const uriMatch = xml.match(/<playbackURI>([^<]+)</);
170
148
 
171
149
  if (uriMatch) {
172
150
  const rawUri = uriMatch[1].replace(/&amp;/g, '&');
173
151
  const resDown = await hikRequest({
174
- method: 'GET',
175
- url: `${baseUrl}/download`,
152
+ method: 'GET', url: `${baseUrl}/download`,
176
153
  data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
177
- responseType: 'arraybuffer',
178
- user: node.user,
179
- pass: node.camPass
154
+ responseType: 'arraybuffer', user: node.user, pass: node.camPass
180
155
  });
181
156
 
182
157
  let buffer = Buffer.from(resDown.data);
183
158
  if (t.id === "203") {
159
+ // SALVATAGGIO IMMAGINE FISICO
160
+ const imgFileName = `img_${timestamp}.jpg`;
161
+ const fullImgPath = path.join(imgDir, imgFileName);
162
+ fs.writeFileSync(fullImgPath, buffer);
184
163
  output.imageBuffer = buffer;
164
+ output.imagePath = fullImgPath; // Passiamo il path fisico
185
165
  } else {
186
166
  if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
187
- const rawPath = path.join(tempDir, `raw_${Date.now()}.mp4`);
188
- const fixedPath = path.join(tempDir, `hik_v_${channelID}_${Date.now()}.mp4`);
167
+ const rawPath = path.join(vidDir, `raw_${timestamp}.mp4`);
168
+ const fixedPath = path.join(vidDir, `hik_v_${channelID}_${timestamp}.mp4`);
189
169
  fs.writeFileSync(rawPath, buffer);
190
170
 
191
171
  await new Promise((resolve) => {
@@ -197,11 +177,10 @@ module.exports = function(RED) {
197
177
  resolve();
198
178
  });
199
179
  });
200
- setTimeout(() => { if (output.videoPath && fs.existsSync(output.videoPath)) fs.unlinkSync(output.videoPath); }, 180000);
201
180
  }
202
181
  }
203
182
  }
204
- if (output.imageBuffer || output.videoPath) node.send({ payload: output });
183
+ if (output.imagePath || output.videoPath) node.send({ payload: output });
205
184
  } catch (e) {
206
185
  node.error(`Errore Download Cam ${channelID}: ${e.message}`);
207
186
  }
@@ -213,21 +192,20 @@ module.exports = function(RED) {
213
192
  if (isClosing) return;
214
193
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
215
194
 
216
- hikRequest({
195
+ urllib.request(url, {
217
196
  method: 'GET',
218
- url: url,
219
- responseType: 'stream',
220
- user: node.user,
221
- pass: node.pass
222
- }).then(response => {
223
- streamRequest = response;
197
+ digestAuth: `${node.user}:${node.pass}`,
198
+ streaming: true,
199
+ timeout: 60000,
200
+ rejectUnauthorized: false
201
+ }).then(res => {
224
202
  if (!nvrOnline) {
225
203
  node.send({ payload: { status: "online", ip: node.host, msg: "NVR Online" } });
226
204
  nvrOnline = true;
227
205
  }
228
206
  updateNodeStatus();
229
207
 
230
- response.data.on('data', (chunk) => {
208
+ res.res.on('data', (chunk) => {
231
209
  const data = chunk.toString().toLowerCase();
232
210
  if (data.includes("active")) {
233
211
  const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
@@ -239,8 +217,8 @@ module.exports = function(RED) {
239
217
  }
240
218
  });
241
219
 
242
- response.data.on('error', () => handleNvrError());
243
- response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
220
+ res.res.on('error', () => handleNvrError());
221
+ res.res.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
244
222
  }).catch(() => handleNvrError());
245
223
  }
246
224
 
@@ -259,7 +237,6 @@ module.exports = function(RED) {
259
237
  node.on('close', (done) => {
260
238
  isClosing = true;
261
239
  clearInterval(heartbeatInterval);
262
- if (streamRequest && streamRequest.data) streamRequest.data.destroy();
263
240
  done();
264
241
  });
265
242
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",