node-red-contrib-hik-media-buffer 1.1.6 → 1.1.8

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 +41 -62
  2. package/package.json +1 -1
@@ -1,6 +1,9 @@
1
- const axiosDigest = require('axios-digest');
1
+ const axios = require('axios');
2
+ const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
3
+ const https = require('https');
2
4
  const fs = require('fs');
3
5
  const path = require('path');
6
+ const os = require('os');
4
7
  const { exec } = require('child_process');
5
8
 
6
9
  module.exports = function(RED) {
@@ -17,61 +20,46 @@ module.exports = function(RED) {
17
20
  node.camPass = config.camPass || config.pass;
18
21
  node.cameras = config.cameras || [];
19
22
 
23
+ let streamRequest = null;
20
24
  let isClosing = false;
21
25
  let lastTriggerTime = 0;
22
26
  let nvrOnline = true;
23
27
  let statoCamera = {};
24
28
 
29
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
25
30
  const EventList = ["FieldDetection", "LineDetection"];
26
31
 
27
- // --- DEFINIZIONE PERCORSI STORAGE ---
32
+ // --- QUESTE RIGHE SERVONO PER IL SITO ---
28
33
  const baseStorage = `C:\\Users\\APerucca\\Documents\\progetto-docker\\storage\\${node.name}`;
29
34
  const imgDir = path.join(baseStorage, "allarmi");
30
35
  const vidDir = path.join(baseStorage, "video");
31
-
32
36
  if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
33
37
  if (!fs.existsSync(vidDir)) fs.mkdirSync(vidDir, { recursive: true });
34
38
 
35
- // Client Digest per NVR e Camere
36
- const nvrAuth = axiosDigest(node.user, node.pass);
37
-
38
39
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
39
40
 
40
41
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
41
42
 
42
- async function getCameraName(cam) {
43
- try {
44
- const res = await nvrAuth.request({
45
- method: 'GET',
46
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}`
47
- });
48
- const match = res.data.toString().match(/<name>([^<]+)<\/name>/);
49
- return match ? match[1] : `Cam_${cam.channel}`;
50
- } catch (e) {
51
- return `Camera_${cam.ip}`;
52
- }
53
- }
54
-
55
43
  async function checkCameras() {
56
44
  if (isClosing) return;
57
45
  for (let cam of node.cameras) {
46
+ const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
58
47
  try {
59
- await nvrAuth.request({
48
+ await camAuth.request({
60
49
  method: 'GET',
61
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`
50
+ url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
51
+ timeout: 5000,
52
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
62
53
  });
63
-
64
54
  if (statoCamera[cam.ip] === false) {
65
- const nomeOnline = await getCameraName(cam);
66
- node.send({ payload: { status: "online", nomeCliente: node.name, nome_telecamera: nomeOnline, ip: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
55
+ node.send({ payload: { status: "online", nomeCliente: node.name, ip: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
67
56
  statoCamera[cam.ip] = true;
68
57
  } else if (statoCamera[cam.ip] === undefined) {
69
58
  statoCamera[cam.ip] = true;
70
59
  }
71
60
  } catch (e) {
72
61
  if (statoCamera[cam.ip] !== false) {
73
- const nomeOffline = await getCameraName(cam);
74
- node.send({ payload: { status: "offline", nomeCliente: node.name, nome_telecamera: nomeOffline, ip: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
62
+ node.send({ payload: { status: "offline", nomeCliente: node.name, ip: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
75
63
  statoCamera[cam.ip] = false;
76
64
  }
77
65
  }
@@ -100,51 +88,48 @@ module.exports = function(RED) {
100
88
  if (nowTime - lastTriggerTime < 10000) return;
101
89
  lastTriggerTime = nowTime;
102
90
 
103
- const timestamp = Math.floor(nowTime / 1000);
104
- const nomeCamera = await getCameraName(camera);
91
+ const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
105
92
  const referenceTime = new Date();
93
+ const timestamp = Math.floor(referenceTime.getTime() / 1000);
106
94
  const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
107
95
  const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
108
96
 
109
- const camAuth = axiosDigest(node.user, node.camPass);
110
- const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
111
-
112
97
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
113
98
  await new Promise(resolve => setTimeout(resolve, 6000));
114
99
 
115
- let output = { ip: camera.ip, nomeCliente: node.name, nome_telecamera: nomeCamera, channel: channelID, event: evento, videoPath: null, imagePath: null };
100
+ const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
101
+ let output = { ip: camera.ip, nomeCliente: node.name, channel: channelID, event: evento, videoPath: null, imageBuffer: null, imagePath: null };
116
102
 
117
103
  try {
118
104
  const tracks = [{ id: "201" }, { id: "203" }];
119
105
  for (let t of tracks) {
120
106
  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>`;
121
-
122
- const resSearch = await camAuth.request({
123
- method: 'POST', url: `${baseUrl}/search`, data: searchXml,
124
- headers: { "Content-Type": "application/xml" }
107
+
108
+ const resSearch = await camAuth.request({
109
+ method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
125
110
  });
126
111
 
127
- let xml = resSearch.data.toString().replace(/<(\/?)\w+:/g, "<$1");
112
+ let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
128
113
  const uriMatch = xml.match(/<playbackURI>([^<]+)</);
129
114
 
130
115
  if (uriMatch) {
131
116
  const rawUri = uriMatch[1].replace(/&amp;/g, '&');
132
- const resDown = await camAuth.request({
133
- method: 'POST', // POST risolve l'errore "Invalid XML"
134
- url: `${baseUrl}/download`,
135
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
136
- responseType: 'arraybuffer',
137
- headers: { "Content-Type": "application/xml" }
117
+ const resDown = await camAuth.request({
118
+ method: 'GET',
119
+ url: `${baseUrl}/download`,
120
+ data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
121
+ responseType: 'arraybuffer'
138
122
  });
139
123
 
140
124
  let buffer = Buffer.from(resDown.data);
141
125
  if (t.id === "203") {
142
- // FOTO
126
+ // --- SALVA FOTO ---
143
127
  const fullImgPath = path.join(imgDir, `img_${timestamp}.jpg`);
144
128
  fs.writeFileSync(fullImgPath, buffer);
129
+ output.imageBuffer = buffer;
145
130
  output.imagePath = fullImgPath;
146
131
  } else {
147
- // VIDEO
132
+ // --- SALVA VIDEO ---
148
133
  if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
149
134
  const rawPath = path.join(vidDir, `raw_${timestamp}.mp4`);
150
135
  const fixedPath = path.join(vidDir, `hik_v_${channelID}_${timestamp}.mp4`);
@@ -170,19 +155,17 @@ module.exports = function(RED) {
170
155
  updateNodeStatus();
171
156
  }
172
157
 
158
+ // --- ALERT STREAM (TUTTO COME PRIMA) ---
173
159
  function startAlertStream() {
174
160
  if (isClosing) return;
161
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
175
162
  const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
176
-
177
- nvrAuth.request({ method: 'GET', url: url, responseType: 'stream' })
178
- .then(res => {
179
- if (!nvrOnline) {
180
- node.send({ payload: { status: "online", ip: node.host, msg: "NVR Online" } });
181
- nvrOnline = true;
182
- }
163
+ nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
164
+ .then(response => {
165
+ streamRequest = response;
166
+ if (!nvrOnline) { node.send({ payload: { status: "online", ip: node.host, msg: "NVR Online" } }); nvrOnline = true; }
183
167
  updateNodeStatus();
184
-
185
- res.data.on('data', (chunk) => {
168
+ response.data.on('data', (chunk) => {
186
169
  const data = chunk.toString().toLowerCase();
187
170
  if (data.includes("active")) {
188
171
  const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
@@ -193,27 +176,23 @@ module.exports = function(RED) {
193
176
  }
194
177
  }
195
178
  });
196
-
197
- res.data.on('error', () => handleNvrError());
198
- res.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
179
+ response.data.on('error', () => handleNvrError());
180
+ response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
199
181
  }).catch(() => handleNvrError());
200
182
  }
201
183
 
202
184
  function handleNvrError() {
203
- if (nvrOnline) {
204
- node.send({ payload: { status: "offline", ip: node.host, msg: "NVR Offline" } });
205
- nvrOnline = false;
206
- }
185
+ if (nvrOnline) { node.send({ payload: { status: "offline", ip: node.host, msg: "NVR Offline" } }); nvrOnline = false; }
207
186
  updateNodeStatus();
208
187
  if (!isClosing) setTimeout(startAlertStream, 10000);
209
188
  }
210
189
 
211
190
  startAlertStream();
212
191
  setTimeout(checkCameras, 2000);
213
-
214
192
  node.on('close', (done) => {
215
193
  isClosing = true;
216
194
  clearInterval(heartbeatInterval);
195
+ if (streamRequest) streamRequest.data.destroy();
217
196
  done();
218
197
  });
219
198
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",