node-red-contrib-hik-media-buffer 1.1.80 → 1.1.82
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 +123 -123
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
|
-
const
|
|
2
|
-
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
3
|
-
const https = require('https');
|
|
1
|
+
const got = require('got'); // Assicurati di aver installato got@11 nel container!
|
|
4
2
|
const fs = require('fs');
|
|
5
3
|
const path = require('path');
|
|
6
4
|
const os = require('os');
|
|
7
5
|
const { exec } = require('child_process');
|
|
8
|
-
const { channel } = require('diagnostics_channel');
|
|
9
6
|
|
|
10
7
|
module.exports = function(RED) {
|
|
11
8
|
function HikMediaBufferNode(config) {
|
|
@@ -18,16 +15,13 @@ module.exports = function(RED) {
|
|
|
18
15
|
node.protocol = config.protocol || "http";
|
|
19
16
|
node.user = config.user;
|
|
20
17
|
node.pass = config.pass;
|
|
21
|
-
node.camPass = config.camPass || config.pass;
|
|
22
|
-
node.cameras = config.cameras || [];
|
|
23
18
|
|
|
24
|
-
let
|
|
19
|
+
let activeStream = null;
|
|
25
20
|
let isClosing = false;
|
|
26
21
|
let lastTriggerTime = {};
|
|
27
22
|
let nvrOnline = true;
|
|
28
|
-
let statoCamera = {};
|
|
23
|
+
let statoCamera = {}; // Indicizzato per ChannelID: true = online, false = offline
|
|
29
24
|
|
|
30
|
-
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
31
25
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
32
26
|
|
|
33
27
|
// CARTELLA TEMPORANEA
|
|
@@ -38,60 +32,66 @@ module.exports = function(RED) {
|
|
|
38
32
|
|
|
39
33
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
40
34
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
35
|
+
// Client base di GOT configurato con il Digest nativo per l'NVR
|
|
36
|
+
const nvrClient = got.extend({
|
|
37
|
+
prefixUrl: `${node.protocol}://${node.host}:${node.port}/ISAPI`,
|
|
38
|
+
username: node.user,
|
|
39
|
+
password: node.pass,
|
|
40
|
+
authentication: 'digest',
|
|
41
|
+
retry: 0,
|
|
42
|
+
https: {
|
|
43
|
+
rejectUnauthorized: false
|
|
44
|
+
}
|
|
45
|
+
});
|
|
47
46
|
|
|
47
|
+
// --- PRENDE IL NOME DELLA TELECAMERA DIRETTAMENTE DALL'NVR ---
|
|
48
|
+
async function getCameraName(channelID) {
|
|
48
49
|
try {
|
|
49
|
-
const res = await
|
|
50
|
-
|
|
51
|
-
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
|
|
52
|
-
timeout: 5000,
|
|
53
|
-
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const data = res.data.toString();
|
|
50
|
+
const res = await nvrClient.get(`System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`, { timeout: 5000 });
|
|
51
|
+
const data = res.body.toString();
|
|
57
52
|
const match = data.match(/<name>([^<]+)<\/name>/i);
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
return match[1].trim();
|
|
61
|
-
} else {
|
|
62
|
-
return `Canale_${cam.channel}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
53
|
+
if (match && match[1]) return match[1].trim();
|
|
54
|
+
return `Canale_${channelID}`;
|
|
65
55
|
} catch (e) {
|
|
66
|
-
return `Camera_${
|
|
56
|
+
return `Camera_${channelID}`;
|
|
67
57
|
}
|
|
68
58
|
}
|
|
69
59
|
|
|
70
|
-
// --- CONTROLLO STATUS CAMERE ---
|
|
60
|
+
// --- CONTROLLO STATUS CAMERE AUTOMATICO DALL'NVR ---
|
|
71
61
|
async function checkCameras() {
|
|
72
|
-
if (isClosing) return;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
62
|
+
if (isClosing || !nvrOnline) return;
|
|
63
|
+
try {
|
|
64
|
+
// Interroghiamo lo stato di tutti i canali IP proxy sull'NVR
|
|
65
|
+
const res = await nvrClient.get('ContentMgmt/InputProxy/channels/status', { timeout: 8000 });
|
|
66
|
+
const xml = res.body.toString();
|
|
67
|
+
|
|
68
|
+
// Estraiamo tutti i blocchi <InputProxyChannelStatus> presenti nell'XML dell'NVR
|
|
69
|
+
const statusBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/gi) || [];
|
|
70
|
+
|
|
71
|
+
for (let block of statusBlocks) {
|
|
72
|
+
const idMatch = block.match(/<id>(\d+)<\/id>/i);
|
|
73
|
+
const statusMatch = block.match(/<online>([^<]+)<\/online>/i);
|
|
74
|
+
|
|
75
|
+
if (idMatch && statusMatch) {
|
|
76
|
+
const ch = idMatch[1].trim();
|
|
77
|
+
const isOnlineNow = statusMatch[1].trim().toLowerCase() === 'true';
|
|
78
|
+
|
|
79
|
+
if (isOnlineNow && statoCamera[ch] === false) {
|
|
80
|
+
// La telecamera è tornata online!
|
|
81
|
+
const nomeOnline = await getCameraName(ch);
|
|
82
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, channel: ch, msg: "Camera ripristinata" } });
|
|
83
|
+
statoCamera[ch] = true;
|
|
84
|
+
} else if (!isOnlineNow && statoCamera[ch] !== false && statoCamera[ch] !== undefined) {
|
|
85
|
+
// La telecamera si è scollegata! (Ignoriamo il primo avvio se è già scollegata per non intasare di alert)
|
|
86
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${ch}`, channel: ch, msg: "Camera scollegata o non raggiungibile dall'NVR" } });
|
|
87
|
+
statoCamera[ch] = false;
|
|
88
|
+
} else if (statoCamera[ch] === undefined) {
|
|
89
|
+
statoCamera[ch] = isOnlineNow;
|
|
90
|
+
}
|
|
93
91
|
}
|
|
94
92
|
}
|
|
93
|
+
} catch (e) {
|
|
94
|
+
node.error(`[DEBUG STATUS] Errore lettura canali da NVR: ${e.message}`);
|
|
95
95
|
}
|
|
96
96
|
updateNodeStatus();
|
|
97
97
|
}
|
|
@@ -109,21 +109,15 @@ module.exports = function(RED) {
|
|
|
109
109
|
|
|
110
110
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
111
111
|
|
|
112
|
-
// --- DOWNLOAD
|
|
112
|
+
// --- DOWNLOAD MEDIA AUTOMATICO DA EVENTO NVR ---
|
|
113
113
|
async function downloadMedia(evento, channelID) {
|
|
114
|
-
const
|
|
115
|
-
if (!camera) return;
|
|
116
|
-
|
|
117
|
-
const nomeCamera = await getCameraName(camera);
|
|
118
|
-
|
|
114
|
+
const chStr = channelID.toString();
|
|
119
115
|
const nowTime = Date.now();
|
|
120
|
-
if(!lastTriggerTime[
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
|
|
124
|
-
lastTriggerTime[camera.ip] = nowTime;
|
|
116
|
+
if (!lastTriggerTime[chStr]) lastTriggerTime[chStr] = 0;
|
|
117
|
+
if (nowTime - lastTriggerTime[chStr] < 5000) return;
|
|
118
|
+
lastTriggerTime[chStr] = nowTime;
|
|
125
119
|
|
|
126
|
-
const
|
|
120
|
+
const nomeCamera = await getCameraName(channelID);
|
|
127
121
|
const referenceTime = new Date();
|
|
128
122
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
129
123
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
@@ -132,73 +126,63 @@ module.exports = function(RED) {
|
|
|
132
126
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
133
127
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
134
128
|
|
|
135
|
-
const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
|
|
136
|
-
|
|
137
|
-
// struttura del payload per Python
|
|
138
129
|
let output = {
|
|
139
130
|
tipo_messaggio: "evento",
|
|
140
131
|
nome_cliente: node.name,
|
|
141
132
|
nome_telecamera: nomeCamera,
|
|
142
|
-
ip_telecamera:
|
|
133
|
+
ip_telecamera: null,
|
|
143
134
|
tipo_evento: evento,
|
|
144
135
|
timestamp_epoch: timestamp,
|
|
145
136
|
stato_telecamera: "ONLINE",
|
|
146
|
-
channel:
|
|
137
|
+
channel: chStr,
|
|
147
138
|
foto_base64: null,
|
|
148
139
|
video_base64: null
|
|
149
140
|
};
|
|
150
141
|
|
|
151
|
-
|
|
152
142
|
let fileDaCancellare = [];
|
|
153
143
|
|
|
154
144
|
try {
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
const trackI = (channelID*100+3).toString();
|
|
145
|
+
const trackV = (channelID * 100 + 1).toString();
|
|
146
|
+
const trackI = (channelID * 100 + 3).toString();
|
|
158
147
|
const tracks = [{ id: trackV }, { id: trackI }];
|
|
148
|
+
|
|
159
149
|
for (let t of tracks) {
|
|
160
|
-
|
|
150
|
+
let searchXml = ``;
|
|
151
|
+
if (t.id === trackV) {
|
|
161
152
|
searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackList><trackID>${t.id}</trackID></trackList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>100</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
162
|
-
|
|
163
|
-
} else{
|
|
153
|
+
} else {
|
|
164
154
|
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><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${evento}</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
165
155
|
}
|
|
166
156
|
|
|
167
|
-
const resSearch = await
|
|
168
|
-
|
|
157
|
+
const resSearch = await nvrClient.post('ContentMgmt/search', {
|
|
158
|
+
body: searchXml,
|
|
159
|
+
headers: { "Content-Type": "application/xml" }
|
|
169
160
|
});
|
|
170
161
|
|
|
171
|
-
let xml = resSearch.
|
|
162
|
+
let xml = resSearch.body.replace(/<(\/?)\w+:/g, "<$1");
|
|
172
163
|
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
173
164
|
|
|
174
165
|
if (uriMatch) {
|
|
175
166
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
167
|
+
|
|
168
|
+
const resDown = await nvrClient.post('ContentMgmt/download', {
|
|
169
|
+
body: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
170
|
+
responseType: 'buffer',
|
|
171
|
+
headers: { "Content-Type": "application/xml" }
|
|
181
172
|
});
|
|
182
173
|
|
|
183
|
-
let buffer =
|
|
174
|
+
let buffer = resDown.body;
|
|
184
175
|
if (t.id === trackI) {
|
|
185
|
-
// SALVA FOTO IN LOCALE
|
|
186
176
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
187
177
|
fs.writeFileSync(fullImgPath, buffer);
|
|
188
|
-
|
|
189
|
-
// La convertiamo subito in testo per il payload
|
|
190
178
|
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
191
|
-
|
|
192
|
-
// Registriamo il file per la distruzione futura
|
|
193
179
|
fileDaCancellare.push(fullImgPath);
|
|
194
180
|
} else {
|
|
195
|
-
// SALVA VIDEO IN LOCALE-
|
|
196
181
|
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
197
182
|
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
198
183
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
199
184
|
fs.writeFileSync(rawPath, buffer);
|
|
200
185
|
|
|
201
|
-
// Eseguiamo ffmpeg localmente
|
|
202
186
|
await new Promise((resolve) => {
|
|
203
187
|
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
204
188
|
if (!err && fs.existsSync(fixedPath)) {
|
|
@@ -215,20 +199,12 @@ module.exports = function(RED) {
|
|
|
215
199
|
}
|
|
216
200
|
}
|
|
217
201
|
|
|
218
|
-
// Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
|
|
219
202
|
if (output.foto_base64 || output.video_base64) {
|
|
220
203
|
node.send({ payload: output });
|
|
221
204
|
|
|
222
|
-
// TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
|
|
223
205
|
setTimeout(() => {
|
|
224
206
|
for (let file of fileDaCancellare) {
|
|
225
|
-
try {
|
|
226
|
-
if (fs.existsSync(file)) {
|
|
227
|
-
fs.unlinkSync(file);
|
|
228
|
-
}
|
|
229
|
-
} catch (err) {
|
|
230
|
-
node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
|
|
231
|
-
}
|
|
207
|
+
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
232
208
|
}
|
|
233
209
|
}, 120000);
|
|
234
210
|
}
|
|
@@ -242,41 +218,65 @@ module.exports = function(RED) {
|
|
|
242
218
|
// --- ALERT STREAM ---
|
|
243
219
|
function startAlertStream() {
|
|
244
220
|
if (isClosing) return;
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
.
|
|
249
|
-
|
|
250
|
-
|
|
221
|
+
|
|
222
|
+
const streamUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
223
|
+
|
|
224
|
+
const downloadStream = got.stream(streamUrl, {
|
|
225
|
+
username: node.user,
|
|
226
|
+
password: node.pass,
|
|
227
|
+
authentication: 'digest',
|
|
228
|
+
https: { rejectUnauthorized: false }
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
activeStream = downloadStream;
|
|
232
|
+
|
|
233
|
+
downloadStream.on('response', () => {
|
|
234
|
+
if (!nvrOnline) {
|
|
235
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } });
|
|
236
|
+
nvrOnline = true;
|
|
237
|
+
}
|
|
251
238
|
updateNodeStatus();
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
downloadStream.on('data', (chunk) => {
|
|
242
|
+
const data = chunk.toString().toLowerCase();
|
|
243
|
+
if (data.includes("active")) {
|
|
244
|
+
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
245
|
+
if (chMatch) {
|
|
246
|
+
let ev = "Unknown";
|
|
247
|
+
for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
|
|
248
|
+
downloadMedia(ev, chMatch[1]);
|
|
261
249
|
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
downloadStream.on('error', (err) => {
|
|
254
|
+
if (!isClosing) handleNvrError();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
downloadStream.on('end', () => {
|
|
258
|
+
if (!isClosing) setTimeout(startAlertStream, 5000);
|
|
259
|
+
});
|
|
266
260
|
}
|
|
267
261
|
|
|
268
262
|
function handleNvrError() {
|
|
269
|
-
if (nvrOnline) {
|
|
263
|
+
if (nvrOnline) {
|
|
264
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } });
|
|
265
|
+
nvrOnline = false;
|
|
266
|
+
}
|
|
270
267
|
updateNodeStatus();
|
|
271
268
|
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
272
269
|
}
|
|
273
270
|
|
|
274
271
|
startAlertStream();
|
|
275
272
|
setTimeout(checkCameras, 2000);
|
|
273
|
+
|
|
276
274
|
node.on('close', (done) => {
|
|
277
275
|
isClosing = true;
|
|
278
276
|
clearInterval(heartbeatInterval);
|
|
279
|
-
if (
|
|
277
|
+
if (activeStream) {
|
|
278
|
+
try { activeStream.destroy(); } catch(e){}
|
|
279
|
+
}
|
|
280
280
|
done();
|
|
281
281
|
});
|
|
282
282
|
}
|