node-red-contrib-hik-media-buffer 1.1.82 → 1.1.83
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 +62 -86
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const os = require('os');
|
|
@@ -16,15 +17,13 @@ module.exports = function(RED) {
|
|
|
16
17
|
node.user = config.user;
|
|
17
18
|
node.pass = config.pass;
|
|
18
19
|
|
|
19
|
-
let
|
|
20
|
+
let streamRequest = null;
|
|
20
21
|
let isClosing = false;
|
|
21
22
|
let lastTriggerTime = {};
|
|
22
23
|
let nvrOnline = true;
|
|
23
|
-
let statoCamera = {};
|
|
24
|
+
let statoCamera = {};
|
|
24
25
|
|
|
25
26
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
26
|
-
|
|
27
|
-
// CARTELLA TEMPORANEA
|
|
28
27
|
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
29
28
|
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
30
29
|
|
|
@@ -32,24 +31,18 @@ module.exports = function(RED) {
|
|
|
32
31
|
|
|
33
32
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
34
33
|
|
|
35
|
-
//
|
|
36
|
-
const
|
|
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
|
-
});
|
|
34
|
+
// Unica libreria di autenticazione usata per tutto il flusso
|
|
35
|
+
const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
46
36
|
|
|
47
|
-
// --- PRENDE IL NOME DELLA TELECAMERA
|
|
37
|
+
// --- PRENDE IL NOME DELLA TELECAMERA DALL'NVR ---
|
|
48
38
|
async function getCameraName(channelID) {
|
|
49
39
|
try {
|
|
50
|
-
const res = await
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
const res = await nvrAuthAxios.request({
|
|
41
|
+
method: 'GET',
|
|
42
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
|
|
43
|
+
timeout: 5000
|
|
44
|
+
});
|
|
45
|
+
const match = res.data.toString().match(/<name>([^<]+)<\/name>/i);
|
|
53
46
|
if (match && match[1]) return match[1].trim();
|
|
54
47
|
return `Canale_${channelID}`;
|
|
55
48
|
} catch (e) {
|
|
@@ -61,11 +54,12 @@ module.exports = function(RED) {
|
|
|
61
54
|
async function checkCameras() {
|
|
62
55
|
if (isClosing || !nvrOnline) return;
|
|
63
56
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
57
|
+
const res = await nvrAuthAxios.request({
|
|
58
|
+
method: 'GET',
|
|
59
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
|
|
60
|
+
timeout: 8000
|
|
61
|
+
});
|
|
62
|
+
const xml = res.data.toString();
|
|
69
63
|
const statusBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/gi) || [];
|
|
70
64
|
|
|
71
65
|
for (let block of statusBlocks) {
|
|
@@ -77,13 +71,11 @@ module.exports = function(RED) {
|
|
|
77
71
|
const isOnlineNow = statusMatch[1].trim().toLowerCase() === 'true';
|
|
78
72
|
|
|
79
73
|
if (isOnlineNow && statoCamera[ch] === false) {
|
|
80
|
-
// La telecamera è tornata online!
|
|
81
74
|
const nomeOnline = await getCameraName(ch);
|
|
82
75
|
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, channel: ch, msg: "Camera ripristinata" } });
|
|
83
76
|
statoCamera[ch] = true;
|
|
84
77
|
} else if (!isOnlineNow && statoCamera[ch] !== false && statoCamera[ch] !== undefined) {
|
|
85
|
-
|
|
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" } });
|
|
78
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${ch}`, channel: ch, msg: "Camera scollegata dall'NVR" } });
|
|
87
79
|
statoCamera[ch] = false;
|
|
88
80
|
} else if (statoCamera[ch] === undefined) {
|
|
89
81
|
statoCamera[ch] = isOnlineNow;
|
|
@@ -91,7 +83,7 @@ module.exports = function(RED) {
|
|
|
91
83
|
}
|
|
92
84
|
}
|
|
93
85
|
} catch (e) {
|
|
94
|
-
|
|
86
|
+
// Silenzioso
|
|
95
87
|
}
|
|
96
88
|
updateNodeStatus();
|
|
97
89
|
}
|
|
@@ -109,7 +101,7 @@ module.exports = function(RED) {
|
|
|
109
101
|
|
|
110
102
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
111
103
|
|
|
112
|
-
// --- DOWNLOAD MEDIA
|
|
104
|
+
// --- DOWNLOAD MEDIA UTILIZZANDO SOLO AXIOS ---
|
|
113
105
|
async function downloadMedia(evento, channelID) {
|
|
114
106
|
const chStr = channelID.toString();
|
|
115
107
|
const nowTime = Date.now();
|
|
@@ -127,16 +119,9 @@ module.exports = function(RED) {
|
|
|
127
119
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
128
120
|
|
|
129
121
|
let output = {
|
|
130
|
-
tipo_messaggio: "evento",
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
ip_telecamera: null,
|
|
134
|
-
tipo_evento: evento,
|
|
135
|
-
timestamp_epoch: timestamp,
|
|
136
|
-
stato_telecamera: "ONLINE",
|
|
137
|
-
channel: chStr,
|
|
138
|
-
foto_base64: null,
|
|
139
|
-
video_base64: null
|
|
122
|
+
tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
|
|
123
|
+
ip_telecamera: null, tipo_evento: evento, timestamp_epoch: timestamp,
|
|
124
|
+
stato_telecamera: "ONLINE", channel: chStr, foto_base64: null, video_base64: null
|
|
140
125
|
};
|
|
141
126
|
|
|
142
127
|
let fileDaCancellare = [];
|
|
@@ -154,24 +139,32 @@ module.exports = function(RED) {
|
|
|
154
139
|
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>`;
|
|
155
140
|
}
|
|
156
141
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
142
|
+
// Forza l'XML in un buffer per evitare che Axios sballi la lunghezza della stringa HTTP
|
|
143
|
+
const xmlBuffer = Buffer.from(searchXml, 'utf-8');
|
|
144
|
+
|
|
145
|
+
const resSearch = await nvrAuthAxios.request({
|
|
146
|
+
method: 'POST',
|
|
147
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
|
|
148
|
+
data: xmlBuffer,
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "application/xml; charset=UTF-8",
|
|
151
|
+
"Content-Length": xmlBuffer.length
|
|
152
|
+
}
|
|
160
153
|
});
|
|
161
154
|
|
|
162
|
-
let xml = resSearch.
|
|
155
|
+
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
163
156
|
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
164
157
|
|
|
165
158
|
if (uriMatch) {
|
|
166
159
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
167
160
|
|
|
168
|
-
const resDown = await
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
161
|
+
const resDown = await nvrAuthAxios.request({
|
|
162
|
+
method: 'GET',
|
|
163
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(rawUri)}`,
|
|
164
|
+
responseType: 'arraybuffer'
|
|
172
165
|
});
|
|
173
166
|
|
|
174
|
-
let buffer = resDown.
|
|
167
|
+
let buffer = Buffer.from(resDown.data);
|
|
175
168
|
if (t.id === trackI) {
|
|
176
169
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
177
170
|
fs.writeFileSync(fullImgPath, buffer);
|
|
@@ -201,7 +194,6 @@ module.exports = function(RED) {
|
|
|
201
194
|
|
|
202
195
|
if (output.foto_base64 || output.video_base64) {
|
|
203
196
|
node.send({ payload: output });
|
|
204
|
-
|
|
205
197
|
setTimeout(() => {
|
|
206
198
|
for (let file of fileDaCancellare) {
|
|
207
199
|
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
@@ -215,48 +207,34 @@ module.exports = function(RED) {
|
|
|
215
207
|
updateNodeStatus();
|
|
216
208
|
}
|
|
217
209
|
|
|
218
|
-
// --- ALERT STREAM ---
|
|
210
|
+
// --- ALERT STREAM CON AXIOS ---
|
|
219
211
|
function startAlertStream() {
|
|
220
212
|
if (isClosing) return;
|
|
221
|
-
|
|
222
|
-
const streamUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
213
|
+
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
223
214
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
authentication: 'digest',
|
|
228
|
-
https: { rejectUnauthorized: false }
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
activeStream = downloadStream;
|
|
232
|
-
|
|
233
|
-
downloadStream.on('response', () => {
|
|
215
|
+
nvrAuthAxios.request({ method: 'GET', url: url, responseType: 'stream' })
|
|
216
|
+
.then(response => {
|
|
217
|
+
streamRequest = response;
|
|
234
218
|
if (!nvrOnline) {
|
|
235
219
|
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } });
|
|
236
220
|
nvrOnline = true;
|
|
237
221
|
}
|
|
238
222
|
updateNodeStatus();
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
223
|
+
|
|
224
|
+
response.data.on('data', (chunk) => {
|
|
225
|
+
const data = chunk.toString().toLowerCase();
|
|
226
|
+
if (data.includes("active")) {
|
|
227
|
+
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
228
|
+
if (chMatch) {
|
|
229
|
+
let ev = "Unknown";
|
|
230
|
+
for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
|
|
231
|
+
downloadMedia(ev, chMatch[1]);
|
|
232
|
+
}
|
|
249
233
|
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if (!isClosing) handleNvrError();
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
downloadStream.on('end', () => {
|
|
258
|
-
if (!isClosing) setTimeout(startAlertStream, 5000);
|
|
259
|
-
});
|
|
234
|
+
});
|
|
235
|
+
response.data.on('error', () => handleNvrError());
|
|
236
|
+
response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
|
|
237
|
+
}).catch(() => handleNvrError());
|
|
260
238
|
}
|
|
261
239
|
|
|
262
240
|
function handleNvrError() {
|
|
@@ -274,9 +252,7 @@ module.exports = function(RED) {
|
|
|
274
252
|
node.on('close', (done) => {
|
|
275
253
|
isClosing = true;
|
|
276
254
|
clearInterval(heartbeatInterval);
|
|
277
|
-
if (
|
|
278
|
-
try { activeStream.destroy(); } catch(e){}
|
|
279
|
-
}
|
|
255
|
+
if (streamRequest) streamRequest.data.destroy();
|
|
280
256
|
done();
|
|
281
257
|
});
|
|
282
258
|
}
|