node-red-contrib-hik-media-buffer 1.1.42 → 1.1.43
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/README.md +13 -8
- package/hik-media-buffer.js +90 -81
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,19 +11,24 @@ This node only detects **_"FieldDetection"_** and **_"LineDetection"_** alarms b
|
|
|
11
11
|
|
|
12
12
|
To configure the node you need to enter the **_IP, user and password of the NVR_**, you can also choose the **_protocol_** and **_port_** to use.</br>
|
|
13
13
|
You must also enter, by pressing the **_"add"_** button, the **_channel and the correspective IP of the camera_**, finally you must enter the **_password of the cameras_**.</br>
|
|
14
|
+
The node name is the name of the customer.</br>
|
|
14
15
|
|
|
15
16
|
This below is an example of msg output:</br>
|
|
16
17
|
|
|
17
18
|
```javascript
|
|
18
19
|
msg = {
|
|
19
|
-
payload: object
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
payload: object
|
|
21
|
+
tipo_messaggio: "evento" // Type of alarm deteced (event or status)
|
|
22
|
+
nome_cliente: "test" // Customer name (name of the node)
|
|
23
|
+
nome_telecamera: "Ufficio" // Camera name on hiklvision
|
|
24
|
+
ip_telecamera: "192.168.62.9" // IP of the camera
|
|
25
|
+
tipo_evento: "LineDetection" // Type of event deteced
|
|
26
|
+
timestamp_epoch: 1780645403 // Timestamp of the event
|
|
27
|
+
stato_telecamera: "ONLINE" // Status of the camera
|
|
28
|
+
channel: "2" // Channel of the camera
|
|
29
|
+
foto_base64: "/9j/2wCEAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSop..." // Buffer of the image base64
|
|
30
|
+
video_base64: "AAAAHGZ0eXBpc29tAAACAGlzb21pc28ybXA0MQAABtdtb292AAAAbG12aGQAAAAAAAAAAAAAAAAAAAPo..." // Buffer of the image base64
|
|
31
|
+
_msgid: "942a8c0c56860f42"
|
|
27
32
|
};
|
|
28
33
|
```
|
|
29
34
|
## HIK SNAPSHOT NODE
|
package/hik-media-buffer.js
CHANGED
|
@@ -12,21 +12,24 @@ module.exports = function(RED) {
|
|
|
12
12
|
const node = this;
|
|
13
13
|
|
|
14
14
|
node.name = config.name || "TEST";
|
|
15
|
-
node.host = config.host;
|
|
15
|
+
node.host = config.host; // IP dell'NVR centrale
|
|
16
16
|
node.port = config.port || "80";
|
|
17
17
|
node.protocol = config.protocol || "http";
|
|
18
18
|
node.user = config.user;
|
|
19
19
|
node.pass = config.pass;
|
|
20
|
-
node.camPass = config.camPass || config.pass;
|
|
21
|
-
node.cameras = config.cameras || [];
|
|
22
20
|
|
|
23
21
|
let streamRequest = null;
|
|
24
22
|
let isClosing = false;
|
|
25
23
|
let lastTriggerTime = {};
|
|
26
24
|
let nvrOnline = true;
|
|
27
|
-
|
|
25
|
+
|
|
26
|
+
// Strutture dinamiche per monitorare canali e stati senza liste fisse
|
|
27
|
+
let statoCanale = {};
|
|
28
|
+
let ultimoAllarmeTempo = {};
|
|
28
29
|
|
|
29
30
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
31
|
+
|
|
32
|
+
// La tua EventList originale per l'alertStream
|
|
30
33
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
31
34
|
|
|
32
35
|
// CARTELLA TEMPORANEA
|
|
@@ -37,66 +40,81 @@ module.exports = function(RED) {
|
|
|
37
40
|
|
|
38
41
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
39
42
|
|
|
40
|
-
// --- PRENDE IL NOME
|
|
41
|
-
async function getCameraName(
|
|
42
|
-
const
|
|
43
|
-
username: node.user,
|
|
44
|
-
password: node.camPass
|
|
45
|
-
});
|
|
46
|
-
|
|
43
|
+
// --- PRENDE IL NOME DEL CANALE DIRETTAMENTE DALL'NVR ---
|
|
44
|
+
async function getCameraName(channelID) {
|
|
45
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
47
46
|
try {
|
|
48
|
-
const res = await
|
|
47
|
+
const res = await nvrAuth.request({
|
|
49
48
|
method: 'GET',
|
|
50
|
-
url: `${node.protocol}://${
|
|
49
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
|
|
51
50
|
timeout: 5000,
|
|
52
51
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
53
52
|
});
|
|
54
53
|
|
|
55
54
|
const data = res.data.toString();
|
|
56
55
|
const match = data.match(/<name>([^<]+)<\/name>/i);
|
|
57
|
-
|
|
58
|
-
if (match && match[1]) {
|
|
59
|
-
return match[1].trim();
|
|
60
|
-
} else {
|
|
61
|
-
return `Canale_${cam.channel}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
56
|
+
return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
|
|
64
57
|
} catch (e) {
|
|
65
|
-
return `
|
|
58
|
+
return `Canale_${channelID}`;
|
|
66
59
|
}
|
|
67
60
|
}
|
|
68
61
|
|
|
69
|
-
// --- CONTROLLO STATUS
|
|
62
|
+
// --- CONTROLLO STATUS DINAMICO DEI CANALI DIGITALI DALL'NVR ---
|
|
70
63
|
async function checkCameras() {
|
|
71
64
|
if (isClosing) return;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
65
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
// Interroghiamo l'NVR sullo stato globale dei suoi canali proxy [ps. 57-58 del manuale]
|
|
69
|
+
const res = await nvrAuth.request({
|
|
70
|
+
method: 'GET',
|
|
71
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
|
|
72
|
+
timeout: 5000,
|
|
73
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
nvrOnline = true;
|
|
77
|
+
const xmlData = res.data.toString();
|
|
78
|
+
|
|
79
|
+
// Regex globale per estrarre TUTTI i blocchi <InputProxyChannelStatus> presenti nell'XML dell'NVR
|
|
80
|
+
const bloccoRegex = /<InputProxyChannelStatus[\s\\S]*?>([\s\S]*?)<\/InputProxyChannelStatus>/gi;
|
|
81
|
+
let matchBlocco;
|
|
82
|
+
|
|
83
|
+
while ((matchBlocco = bloccoRegex.exec(xmlData)) !== null) {
|
|
84
|
+
const bloccoContenuto = matchBlocco[1];
|
|
85
|
+
|
|
86
|
+
// Estraiamo l'ID del canale e il suo stato dal blocco attuale
|
|
87
|
+
const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
|
|
88
|
+
const statusMatch = bloccoContenuto.match(/<status>([^<]+)<\/status>/i);
|
|
89
|
+
|
|
90
|
+
if (idMatch && statusMatch) {
|
|
91
|
+
const ch = idMatch[1];
|
|
92
|
+
const statusStr = statusMatch[1].toLowerCase();
|
|
93
|
+
|
|
94
|
+
// Determiniamo se per l'NVR la telecamera su questo canale è attiva
|
|
95
|
+
const isOnline = statusStr.includes("online") || statusStr.includes("recording");
|
|
96
|
+
|
|
97
|
+
if (isOnline && statoCanale[ch] === false) {
|
|
98
|
+
const nomeOnline = await getCameraName(ch);
|
|
99
|
+
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" } });
|
|
100
|
+
statoCanale[ch] = true;
|
|
101
|
+
} else if (!isOnline && statoCanale[ch] !== false && statoCanale[ch] !== undefined) {
|
|
102
|
+
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" } });
|
|
103
|
+
statoCanale[ch] = false;
|
|
104
|
+
} else if (statoCanale[ch] === undefined) {
|
|
105
|
+
// Primo avvio: memorizziamo lo stato iniziale senza inviare messaggi di stato a vuoto
|
|
106
|
+
statoCanale[ch] = isOnline;
|
|
107
|
+
}
|
|
92
108
|
}
|
|
93
109
|
}
|
|
110
|
+
} catch (e) {
|
|
111
|
+
nvrOnline = false; // Se fallisce la GET, è l'NVR ad essere irraggiungibile
|
|
94
112
|
}
|
|
95
113
|
updateNodeStatus();
|
|
96
114
|
}
|
|
97
115
|
|
|
98
116
|
function updateNodeStatus() {
|
|
99
|
-
const offlineCams = Object.values(
|
|
117
|
+
const offlineCams = Object.values(statoCanale).filter(v => v === false).length;
|
|
100
118
|
if (!nvrOnline) {
|
|
101
119
|
node.status({fill:"red", shape:"ring", text:"NVR Offline"});
|
|
102
120
|
} else if (offlineCams > 0) {
|
|
@@ -106,39 +124,33 @@ module.exports = function(RED) {
|
|
|
106
124
|
}
|
|
107
125
|
}
|
|
108
126
|
|
|
127
|
+
// Il controllo camere gira in background ogni 30 secondi interrogando solo l'NVR
|
|
109
128
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
110
129
|
|
|
111
|
-
// --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE
|
|
130
|
+
// --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DALL'NVR ---
|
|
112
131
|
async function downloadMedia(evento, channelID) {
|
|
113
|
-
const camera = node.cameras.find(c => c.channel == channelID);
|
|
114
|
-
if (!camera) return;
|
|
115
|
-
|
|
116
|
-
const nomeCamera = await getCameraName(camera);
|
|
117
|
-
|
|
118
132
|
const nowTime = Date.now();
|
|
119
|
-
if(!lastTriggerTime[
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
|
|
123
|
-
lastTriggerTime[camera.ip] = nowTime;
|
|
133
|
+
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
134
|
+
if (nowTime - lastTriggerTime[channelID] < 5000) return;
|
|
135
|
+
lastTriggerTime[channelID] = nowTime;
|
|
124
136
|
|
|
125
|
-
const
|
|
137
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
126
138
|
const referenceTime = new Date();
|
|
127
139
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
128
140
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
129
141
|
const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
|
|
130
142
|
|
|
131
143
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
132
|
-
|
|
144
|
+
const nomeCamera = await getCameraName(channelID);
|
|
133
145
|
|
|
134
|
-
|
|
146
|
+
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
147
|
+
const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
|
|
135
148
|
|
|
136
|
-
// struttura del payload per Python
|
|
137
149
|
let output = {
|
|
138
150
|
tipo_messaggio: "evento",
|
|
139
151
|
nome_cliente: node.name,
|
|
140
152
|
nome_telecamera: nomeCamera,
|
|
141
|
-
|
|
153
|
+
ip_nvr: node.host,
|
|
142
154
|
tipo_evento: evento,
|
|
143
155
|
timestamp_epoch: timestamp,
|
|
144
156
|
stato_telecamera: "ONLINE",
|
|
@@ -147,15 +159,18 @@ module.exports = function(RED) {
|
|
|
147
159
|
video_base64: null
|
|
148
160
|
};
|
|
149
161
|
|
|
150
|
-
|
|
151
162
|
let fileDaCancellare = [];
|
|
163
|
+
const parsedChannel = parseInt(channelID, 10);
|
|
164
|
+
const trackVideo = (parsedChannel * 100) + 1; // Formula dinamica NVR (*100+1) [cite: 958]
|
|
165
|
+
const trackFoto = (parsedChannel * 100) + 3; // Formula dinamica NVR (*100+3) [cite: 958]
|
|
166
|
+
|
|
167
|
+
const tracks = [{ id: trackVideo.toString(), type: "video" }, { id: trackFoto.toString(), type: "foto" }];
|
|
152
168
|
|
|
153
169
|
try {
|
|
154
|
-
const tracks = [{ id: "201" }, { id: "203" }];
|
|
155
170
|
for (let t of tracks) {
|
|
156
171
|
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>`;
|
|
157
172
|
|
|
158
|
-
const resSearch = await
|
|
173
|
+
const resSearch = await nvrAuth.request({
|
|
159
174
|
method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
|
|
160
175
|
});
|
|
161
176
|
|
|
@@ -172,24 +187,17 @@ module.exports = function(RED) {
|
|
|
172
187
|
});
|
|
173
188
|
|
|
174
189
|
let buffer = Buffer.from(resDown.data);
|
|
175
|
-
if (t.
|
|
176
|
-
// SALVA FOTO IN LOCALE
|
|
190
|
+
if (t.type === "foto") {
|
|
177
191
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
178
192
|
fs.writeFileSync(fullImgPath, buffer);
|
|
179
|
-
|
|
180
|
-
// La convertiamo subito in testo per il payload
|
|
181
193
|
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
182
|
-
|
|
183
|
-
// Registriamo il file per la distruzione futura
|
|
184
194
|
fileDaCancellare.push(fullImgPath);
|
|
185
195
|
} else {
|
|
186
|
-
// SALVA VIDEO IN LOCALE-
|
|
187
196
|
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
188
197
|
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
189
198
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
190
199
|
fs.writeFileSync(rawPath, buffer);
|
|
191
200
|
|
|
192
|
-
// Eseguiamo ffmpeg localmente
|
|
193
201
|
await new Promise((resolve) => {
|
|
194
202
|
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
195
203
|
if (!err && fs.existsSync(fixedPath)) {
|
|
@@ -206,20 +214,12 @@ module.exports = function(RED) {
|
|
|
206
214
|
}
|
|
207
215
|
}
|
|
208
216
|
|
|
209
|
-
// Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
|
|
210
217
|
if (output.foto_base64 || output.video_base64) {
|
|
211
218
|
node.send({ payload: output });
|
|
212
219
|
|
|
213
|
-
// TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
|
|
214
220
|
setTimeout(() => {
|
|
215
221
|
for (let file of fileDaCancellare) {
|
|
216
|
-
try {
|
|
217
|
-
if (fs.existsSync(file)) {
|
|
218
|
-
fs.unlinkSync(file);
|
|
219
|
-
}
|
|
220
|
-
} catch (err) {
|
|
221
|
-
node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
|
|
222
|
-
}
|
|
222
|
+
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
223
223
|
}
|
|
224
224
|
}, 120000);
|
|
225
225
|
}
|
|
@@ -227,7 +227,7 @@ module.exports = function(RED) {
|
|
|
227
227
|
} catch (e) {
|
|
228
228
|
node.error(`Errore Download Cam ${channelID}: ${e.message}`);
|
|
229
229
|
}
|
|
230
|
-
|
|
230
|
+
node.status({fill:"green", shape:"ring", text:"In ascolto"});
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
// --- ALERT STREAM ---
|
|
@@ -235,18 +235,26 @@ module.exports = function(RED) {
|
|
|
235
235
|
if (isClosing) return;
|
|
236
236
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
237
237
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
238
|
+
|
|
238
239
|
nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
|
|
239
240
|
.then(response => {
|
|
240
241
|
streamRequest = response;
|
|
241
242
|
if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
|
|
242
243
|
updateNodeStatus();
|
|
244
|
+
|
|
243
245
|
response.data.on('data', (chunk) => {
|
|
244
246
|
const data = chunk.toString().toLowerCase();
|
|
247
|
+
|
|
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
|
-
for (let e of EventList) {
|
|
252
|
+
for (let e of EventList) {
|
|
253
|
+
if (data.includes(e.toLowerCase())) {
|
|
254
|
+
ev = e;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
250
258
|
downloadMedia(ev, chMatch[1]);
|
|
251
259
|
}
|
|
252
260
|
}
|
|
@@ -264,6 +272,7 @@ module.exports = function(RED) {
|
|
|
264
272
|
|
|
265
273
|
startAlertStream();
|
|
266
274
|
setTimeout(checkCameras, 2000);
|
|
275
|
+
|
|
267
276
|
node.on('close', (done) => {
|
|
268
277
|
isClosing = true;
|
|
269
278
|
clearInterval(heartbeatInterval);
|