node-red-contrib-hik-media-buffer 1.1.42 → 1.1.44
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 +102 -87
- 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
|
|
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>([^<]+)</i); // Corretta regex di chiusura tag
|
|
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
|
|
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) {
|
|
@@ -108,37 +126,31 @@ module.exports = function(RED) {
|
|
|
108
126
|
|
|
109
127
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
110
128
|
|
|
111
|
-
// --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE
|
|
129
|
+
// --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DALL'NVR ---
|
|
112
130
|
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
131
|
const nowTime = Date.now();
|
|
119
|
-
if(!lastTriggerTime[
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
|
|
123
|
-
lastTriggerTime[camera.ip] = nowTime;
|
|
132
|
+
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
133
|
+
if (nowTime - lastTriggerTime[channelID] < 5000) return;
|
|
134
|
+
lastTriggerTime[channelID] = nowTime;
|
|
124
135
|
|
|
125
|
-
const
|
|
136
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
126
137
|
const referenceTime = new Date();
|
|
127
138
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
128
|
-
const startTime = toHikDate(new Date(referenceTime.getTime() - (
|
|
129
|
-
const endTime = toHikDate(new Date(referenceTime.getTime() + (
|
|
139
|
+
const startTime = toHikDate(new Date(referenceTime.getTime() - (15 * 1000))); // Allargato a 15s per sicurezza
|
|
140
|
+
const endTime = toHikDate(new Date(referenceTime.getTime() + (15 * 1000)));
|
|
130
141
|
|
|
131
142
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
132
|
-
|
|
143
|
+
const nomeCamera = await getCameraName(channelID);
|
|
133
144
|
|
|
134
|
-
|
|
145
|
+
// Ritardo tecnico per dare tempo all'NVR di chiudere e indicizzare il file su HDD
|
|
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) e.g. 201
|
|
165
|
+
const trackFoto = (parsedChannel * 100) + 3; // Formula dinamica NVR (*100+3) e.g. 203
|
|
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
|
-
const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>
|
|
171
|
+
const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>NVR_CATCH</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/${output.tipo_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
|
|
|
@@ -164,7 +179,7 @@ module.exports = function(RED) {
|
|
|
164
179
|
|
|
165
180
|
if (uriMatch) {
|
|
166
181
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
167
|
-
const resDown = await
|
|
182
|
+
const resDown = await nvrAuth.request({
|
|
168
183
|
method: 'GET',
|
|
169
184
|
url: `${baseUrl}/download`,
|
|
170
185
|
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
@@ -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,28 +214,20 @@ 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
|
}
|
|
226
226
|
|
|
227
227
|
} catch (e) {
|
|
228
|
-
node.error(`Errore Download
|
|
228
|
+
node.error(`Errore Download NVR per Canale ${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,19 +235,33 @@ 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) {
|
|
251
|
+
let channelID = chMatch[1];
|
|
248
252
|
let ev = "Unknown";
|
|
249
|
-
|
|
250
|
-
|
|
253
|
+
|
|
254
|
+
for (let e of EventList) {
|
|
255
|
+
if (data.includes(e.toLowerCase())) {
|
|
256
|
+
ev = e;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Se non è un allarme presente in lista o è un heartbeat, fermati qui
|
|
262
|
+
if (ev === "Unknown") return;
|
|
263
|
+
|
|
264
|
+
downloadMedia(ev, channelID);
|
|
251
265
|
}
|
|
252
266
|
}
|
|
253
267
|
});
|
|
@@ -264,6 +278,7 @@ module.exports = function(RED) {
|
|
|
264
278
|
|
|
265
279
|
startAlertStream();
|
|
266
280
|
setTimeout(checkCameras, 2000);
|
|
281
|
+
|
|
267
282
|
node.on('close', (done) => {
|
|
268
283
|
isClosing = true;
|
|
269
284
|
clearInterval(heartbeatInterval);
|