node-red-contrib-hik-media-buffer 1.1.114 → 1.1.115
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-snapshot.js +286 -72
- package/package.json +1 -1
package/hik-snapshot.js
CHANGED
|
@@ -1,106 +1,320 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
+
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
2
3
|
const https = require('https');
|
|
3
|
-
const
|
|
4
|
-
const
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { exec } = require('child_process');
|
|
5
8
|
|
|
6
9
|
module.exports = function(RED) {
|
|
7
|
-
function
|
|
10
|
+
function HikMediaBufferNode(config) {
|
|
8
11
|
RED.nodes.createNode(this, config);
|
|
9
12
|
const node = this;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
node.nodeName = config.name || `NVR_${config.host}`;
|
|
13
|
-
|
|
14
|
-
node.protocol = config.protocol || "http";
|
|
13
|
+
|
|
14
|
+
node.name = config.name || "TEST";
|
|
15
15
|
node.host = config.host;
|
|
16
16
|
node.port = config.port || "80";
|
|
17
|
+
node.protocol = config.protocol || "http";
|
|
17
18
|
node.user = config.user;
|
|
18
19
|
node.pass = config.pass;
|
|
19
|
-
|
|
20
|
+
|
|
21
|
+
let streamRequest = null;
|
|
22
|
+
let isClosing = false;
|
|
23
|
+
let lastTriggerTime = {};
|
|
24
|
+
let nvrOnline = true;
|
|
25
|
+
let statoCamera = {};
|
|
20
26
|
|
|
21
27
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
28
|
+
const EventList = ["FieldDetection", "LineDetection"];
|
|
22
29
|
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
// CARTELLA TEMPORANEA
|
|
31
|
+
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
32
|
+
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
25
33
|
|
|
26
|
-
|
|
34
|
+
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
37
|
+
|
|
38
|
+
// --- PRENDE IL NOME REALE DELLA TELECAMERA DALL'NVR ---
|
|
39
|
+
async function getCameraNameFromNVR(channelID) {
|
|
40
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
41
|
+
try {
|
|
42
|
+
const res = await nvrAuth.request({
|
|
43
|
+
method: 'GET',
|
|
44
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}`,
|
|
45
|
+
timeout: 5000,
|
|
46
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
47
|
+
});
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
const data = res.data.toString();
|
|
50
|
+
// Cerchiamo il tag channelName che contiene il nome impostato sull'NVR
|
|
51
|
+
const match = data.match(/<channelName>([^<]+)<\/channelName>/i);
|
|
52
|
+
if (match && match[1]) return match[1].trim();
|
|
53
|
+
return `Canale ${channelID}`;
|
|
54
|
+
} catch (e) {
|
|
55
|
+
return `Canale ${channelID}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
39
58
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
// --- CONTROLLO STATUS CAMERE (ONLINE/OFFLINE) INTERROGANDO L'NVR ---
|
|
60
|
+
async function checkCameras() {
|
|
61
|
+
if (isClosing || !nvrOnline) return;
|
|
62
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
63
|
+
try {
|
|
64
|
+
const res = await nvrAuth.request({
|
|
65
|
+
method: 'GET',
|
|
66
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels`,
|
|
67
|
+
timeout: 8000,
|
|
68
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
69
|
+
});
|
|
44
70
|
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
71
|
+
const xml = res.data.toString();
|
|
72
|
+
const channelBlocks = xml.match(/<VideoInputChannel>[\s\S]*?<\/VideoInputChannel>/g) || [];
|
|
73
|
+
|
|
74
|
+
for (let block of channelBlocks) {
|
|
75
|
+
const idMatch = block.match(/<id>(\d+)<\/id>/i);
|
|
76
|
+
// Hikvision usa il tag <online> true/false per lo stato della telecamera IP accoppiata
|
|
77
|
+
const onlineMatch = block.match(/<online>([^<]+)<\/online>/i) || block.match(/<onLine>([^<]+)<\/onLine>/i);
|
|
78
|
+
|
|
79
|
+
if (idMatch) {
|
|
80
|
+
const ch = idMatch[1];
|
|
81
|
+
// Se il tag dice true è online, altrimenti se dice false (o manca) è offline
|
|
82
|
+
const isOnline = onlineMatch ? onlineMatch[1].trim().toLowerCase() === "true" : true;
|
|
83
|
+
|
|
84
|
+
if (isOnline && statoCamera[ch] === false) {
|
|
85
|
+
const nomeOnline = await getCameraNameFromNVR(ch);
|
|
86
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: node.host, channel: ch, msg: "Camera ripristinata" } });
|
|
87
|
+
statoCamera[ch] = true;
|
|
88
|
+
} else if (!isOnline && statoCamera[ch] !== false) {
|
|
89
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera ${ch}`, ip_telecamera: node.host, channel: ch, msg: "Camera non raggiungibile" } });
|
|
90
|
+
statoCamera[ch] = false;
|
|
91
|
+
} else if (statoCamera[ch] === undefined) {
|
|
92
|
+
statoCamera[ch] = isOnline;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} catch (e) {
|
|
97
|
+
node.error(`Errore durante il check delle camere: ${e.message}`);
|
|
98
|
+
}
|
|
99
|
+
updateNodeStatus();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function updateNodeStatus() {
|
|
103
|
+
const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
|
|
104
|
+
if (!nvrOnline) {
|
|
105
|
+
node.status({fill:"red", shape:"ring", text:"NVR Offline"});
|
|
106
|
+
} else if (offlineCams > 0) {
|
|
107
|
+
node.status({fill:"yellow", shape:"dot", text: `${offlineCams} Cam Offline`});
|
|
108
|
+
} else {
|
|
109
|
+
node.status({fill:"green", shape:"ring", text:"In ascolto"});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
114
|
+
|
|
115
|
+
// --- DOWNLOAD CON URL E BODY SECONDO SPECIFICHE ---
|
|
116
|
+
async function downloadMedia(evento, channelID) {
|
|
117
|
+
const nowTime = Date.now();
|
|
118
|
+
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
119
|
+
if (nowTime - lastTriggerTime[channelID] < 5000) return;
|
|
120
|
+
lastTriggerTime[channelID] = nowTime;
|
|
121
|
+
|
|
122
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
123
|
+
const referenceTime = new Date();
|
|
124
|
+
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
125
|
+
|
|
126
|
+
const videoTrackID = (parseInt(channelID) * 100) + 1;
|
|
127
|
+
|
|
128
|
+
// Recupera il nome reale prima di inviare l'allarme
|
|
129
|
+
const nomeCamera = await getCameraNameFromNVR(channelID);
|
|
130
|
+
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
131
|
+
|
|
132
|
+
const startSearchStr = referenceTime.toISOString().split('T')[0] + "T00:00:00 01:00";
|
|
133
|
+
const endSearchStr = referenceTime.toISOString().split('T')[0] + "T23:59:59 01:00";
|
|
134
|
+
|
|
135
|
+
let output = {
|
|
136
|
+
tipo_messaggio: "evento",
|
|
137
|
+
nome_cliente: node.name,
|
|
138
|
+
nome_telecamera: nomeCamera,
|
|
139
|
+
ip_telecamera: node.host,
|
|
140
|
+
tipo_evento: evento,
|
|
141
|
+
timestamp_epoch: timestamp,
|
|
142
|
+
stato_telecamera: "ONLINE",
|
|
143
|
+
channel: channelID.toString(),
|
|
144
|
+
foto_base64: null,
|
|
145
|
+
video_base64: null
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
let fileDaCancellare = [];
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
// 1. SCARICAMENTO IMMAGINE
|
|
152
|
+
const payloadFoto = {
|
|
153
|
+
"EventSearchDescription": {
|
|
154
|
+
"searchID": "C5AFEE35-B1E0-4C01-83F8-47FD77892E4A",
|
|
155
|
+
"searchResultPosition": 0,
|
|
156
|
+
"maxResults": 30,
|
|
157
|
+
"timeSpanList": [{
|
|
158
|
+
"startTime": startSearchStr,
|
|
159
|
+
"endTime": endSearchStr
|
|
160
|
+
}],
|
|
161
|
+
"type": "all",
|
|
162
|
+
"channels": [parseInt(channelID)],
|
|
163
|
+
"eventType": "behavior",
|
|
164
|
+
"behavior": { "behaviorEventType": "linedetection" }
|
|
165
|
+
}
|
|
53
166
|
};
|
|
54
167
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
168
|
+
const resFotoSearch = await nvrAuth.request({
|
|
169
|
+
method: 'POST',
|
|
170
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`,
|
|
171
|
+
data: payloadFoto,
|
|
172
|
+
headers: { "Content-Type": "application/json" },
|
|
173
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (resFotoSearch.data?.EventSearchResult?.Targets?.[0]?.pictureUrl) {
|
|
177
|
+
const urlFotoGrezzo = resFotoSearch.data.EventSearchResult.Targets[0].pictureUrl;
|
|
178
|
+
|
|
179
|
+
const resDownFoto = await nvrAuth.request({
|
|
58
180
|
method: 'GET',
|
|
59
|
-
url:
|
|
181
|
+
url: urlFotoGrezzo,
|
|
60
182
|
responseType: 'arraybuffer',
|
|
61
|
-
httpsAgent: node.protocol ===
|
|
62
|
-
timeout: 5000
|
|
183
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
63
184
|
});
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
185
|
+
|
|
186
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}_ch${channelID}.jpg`);
|
|
187
|
+
fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
|
|
188
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
189
|
+
fileDaCancellare.push(fullImgPath);
|
|
68
190
|
}
|
|
69
191
|
|
|
70
|
-
// 2.
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
192
|
+
// 2. SCARICAMENTO VIDEO
|
|
193
|
+
const startVideoSearch = referenceTime.toISOString().split('T')[0] + "T00:00:00Z";
|
|
194
|
+
const endVideoSearch = referenceTime.toISOString().split('T')[0] + "T23:59:59Z";
|
|
195
|
+
|
|
196
|
+
const payloadVideoSearch = `<?xml version="1.0" encoding="utf-8"?>
|
|
197
|
+
<CMSearchDescription>
|
|
198
|
+
<searchID>CEC13F0F-1AEA-4474-A2DA-CFFF332C7C5B</searchID>
|
|
199
|
+
<trackList><trackID>${videoTrackID}</trackID></trackList>
|
|
200
|
+
<timeSpanList>
|
|
201
|
+
<timeSpan>
|
|
202
|
+
<startTime>${startVideoSearch}</startTime>
|
|
203
|
+
<endTime>${endVideoSearch}</endTime>
|
|
204
|
+
</timeSpan>
|
|
205
|
+
</timeSpanList>
|
|
206
|
+
<maxResults>100</maxResults>
|
|
207
|
+
<searchResultPostion>0</searchResultPostion>
|
|
208
|
+
<metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
|
|
209
|
+
</CMSearchDescription>`;
|
|
210
|
+
|
|
211
|
+
const resVideoSearch = await nvrAuth.request({
|
|
212
|
+
method: 'POST',
|
|
213
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
|
|
214
|
+
data: payloadVideoSearch,
|
|
215
|
+
headers: { "Content-Type": "application/xml" },
|
|
216
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
const xmlString = resVideoSearch.data.toString();
|
|
220
|
+
const uriMatch = xmlString.match(/<playbackURI>([\s\S]*?)<\/playbackURI>/i);
|
|
221
|
+
|
|
222
|
+
if (uriMatch && uriMatch[1]) {
|
|
223
|
+
const playbackURIGrezzo = uriMatch[1].trim();
|
|
224
|
+
|
|
225
|
+
const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
|
|
226
|
+
<downloadRequest>
|
|
227
|
+
<playbackURI>${playbackURIGrezzo}</playbackURI>
|
|
228
|
+
</downloadRequest>`;
|
|
229
|
+
|
|
230
|
+
const resDownVideo = await nvrAuth.request({
|
|
231
|
+
method: 'GET',
|
|
232
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download`,
|
|
233
|
+
data: payloadDownload,
|
|
234
|
+
headers: { "Content-Type": "application/xml" },
|
|
235
|
+
responseType: 'arraybuffer',
|
|
236
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
let bufferVideo = Buffer.from(resDownVideo.data);
|
|
240
|
+
if (bufferVideo.slice(0, 4).toString() === 'IMKH') bufferVideo = bufferVideo.slice(40);
|
|
241
|
+
|
|
242
|
+
const rawPath = path.join(baseStorage, `raw_${timestamp}_ch${channelID}.mp4`);
|
|
243
|
+
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
244
|
+
fs.writeFileSync(rawPath, bufferVideo);
|
|
245
|
+
|
|
246
|
+
await new Promise((resolve) => {
|
|
247
|
+
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
248
|
+
if (!err && fs.existsSync(fixedPath)) {
|
|
249
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
250
|
+
fileDaCancellare.push(fixedPath);
|
|
251
|
+
} else {
|
|
252
|
+
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
253
|
+
}
|
|
254
|
+
fileDaCancellare.push(rawPath);
|
|
255
|
+
resolve();
|
|
256
|
+
});
|
|
79
257
|
});
|
|
80
|
-
const xmlOutput = responseRec.data.toString();
|
|
81
|
-
const regex = new RegExp(`<id>${day}</id>[^]*?<record>true</record>`);
|
|
82
|
-
resCanale.isRecording = regex.test(xmlOutput);
|
|
83
|
-
} catch (err) {
|
|
84
|
-
resCanale.recError = err.message;
|
|
85
258
|
}
|
|
86
259
|
|
|
87
|
-
|
|
88
|
-
|
|
260
|
+
if (output.foto_base64 || output.video_base64) {
|
|
261
|
+
node.send({ payload: output });
|
|
262
|
+
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
for (let file of fileDaCancellare) {
|
|
265
|
+
try {
|
|
266
|
+
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
267
|
+
} catch (err) {}
|
|
268
|
+
}
|
|
269
|
+
}, 120000);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
} catch (e) {
|
|
273
|
+
node.error(`Errore Download Cam ${channelID}: ${e.message}`);
|
|
89
274
|
}
|
|
275
|
+
updateNodeStatus();
|
|
276
|
+
}
|
|
90
277
|
|
|
91
|
-
|
|
92
|
-
|
|
278
|
+
// --- ALERT STREAM ---
|
|
279
|
+
function startAlertStream() {
|
|
280
|
+
if (isClosing) return;
|
|
281
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
282
|
+
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
283
|
+
nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
|
|
284
|
+
.then(response => {
|
|
285
|
+
streamRequest = response;
|
|
286
|
+
if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
|
|
287
|
+
updateNodeStatus();
|
|
288
|
+
response.data.on('data', (chunk) => {
|
|
289
|
+
const data = chunk.toString().toLowerCase();
|
|
290
|
+
if (data.includes("active")) {
|
|
291
|
+
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
292
|
+
if (chMatch) {
|
|
293
|
+
let ev = "Unknown";
|
|
294
|
+
for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
|
|
295
|
+
downloadMedia(ev, chMatch[1]);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
});
|
|
299
|
+
response.data.on('error', () => handleNvrError());
|
|
300
|
+
response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
|
|
301
|
+
}).catch(() => handleNvrError());
|
|
302
|
+
}
|
|
93
303
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
304
|
+
function handleNvrError() {
|
|
305
|
+
if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
|
|
306
|
+
updateNodeStatus();
|
|
307
|
+
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
startAlertStream();
|
|
311
|
+
setTimeout(checkCameras, 2000);
|
|
312
|
+
node.on('close', (done) => {
|
|
313
|
+
isClosing = true;
|
|
314
|
+
clearInterval(heartbeatInterval);
|
|
315
|
+
if (streamRequest) streamRequest.data.destroy();
|
|
316
|
+
done();
|
|
103
317
|
});
|
|
104
318
|
}
|
|
105
|
-
RED.nodes.registerType("hik-
|
|
319
|
+
RED.nodes.registerType("hik-media-buffer", HikMediaBufferNode);
|
|
106
320
|
};
|