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.
- package/hik-media-buffer.js +55 -78
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,21 +1,16 @@
|
|
|
1
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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}`;
|
|
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
|
-
|
|
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(/&/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, '&')}</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(
|
|
188
|
-
const fixedPath = path.join(
|
|
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.
|
|
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
|
-
|
|
195
|
+
urllib.request(url, {
|
|
217
196
|
method: 'GET',
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}).then(
|
|
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
|
-
|
|
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
|
-
|
|
243
|
-
|
|
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
|
}
|