node-red-contrib-hik-media-buffer 1.1.17 → 1.1.20
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 +96 -57
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
-
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
3
2
|
const https = require('https');
|
|
4
3
|
const fs = require('fs');
|
|
5
4
|
const path = require('path');
|
|
@@ -28,59 +27,69 @@ module.exports = function(RED) {
|
|
|
28
27
|
|
|
29
28
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
30
29
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
31
|
-
|
|
32
|
-
// --- CARTELLA TEMPORANEA SUL PC DEL CLIENTE ---
|
|
33
30
|
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
31
|
+
|
|
34
32
|
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
35
33
|
|
|
36
34
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
37
35
|
|
|
38
36
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
39
37
|
|
|
38
|
+
// --- HELPER CENTRALIZZATO PER GLI HEADERS DI AUTENTICAZIONE ---
|
|
39
|
+
// È QUI che si concentra la flessibilità richiesta dal tuo capo!
|
|
40
|
+
function getAuthHeaders() {
|
|
41
|
+
// SCENARIO ATTUALE: Basic Auth standard (o se integrerai un calcolatore Digest custom)
|
|
42
|
+
// Axios gestisce nativamente la Basic Auth codificandola in Base64 nell'header
|
|
43
|
+
const tokenBase64 = Buffer.from(`${node.user}:${node.camPass}`).toString('base64');
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"Authorization": `Basic ${tokenBase64}`, // <-- OGGI: Usiamo Basic/Digest
|
|
47
|
+
// "Authorization": `Bearer ${node.futuroToken}`, // <-- DOMANI: Basterà scommentare questo!
|
|
48
|
+
"Content-Type": "application/xml"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
40
52
|
// --- PRENDE IL NOME DELLA TELECAMERA ---
|
|
41
53
|
async function getCameraName(cam) {
|
|
42
|
-
const camAuth = new AxiosDigestAuth({
|
|
43
|
-
username: node.user,
|
|
44
|
-
password: node.camPass
|
|
45
|
-
});
|
|
46
|
-
|
|
47
54
|
try {
|
|
48
|
-
const res = await
|
|
55
|
+
const res = await axios({
|
|
49
56
|
method: 'GET',
|
|
50
57
|
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
|
|
51
58
|
timeout: 5000,
|
|
59
|
+
headers: getAuthHeaders(),
|
|
52
60
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
53
61
|
});
|
|
54
|
-
|
|
55
62
|
const data = res.data.toString();
|
|
56
63
|
const match = data.match(/<name>([^<]+)<\/name>/i);
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
return match[1].trim();
|
|
60
|
-
} else {
|
|
61
|
-
return `Canale_${cam.channel}`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
} catch (e) {
|
|
65
|
-
return `Camera_${cam.ip}`;
|
|
66
|
-
}
|
|
64
|
+
return match && match[1] ? match[1].trim() : `Canale_${cam.channel}`;
|
|
65
|
+
} catch (e) { return `Camera_${cam.ip}`; }
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
// --- CONTROLLO STATUS CAMERE ---
|
|
70
69
|
async function checkCameras() {
|
|
71
70
|
if (isClosing) return;
|
|
72
71
|
for (let cam of node.cameras) {
|
|
73
|
-
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
|
|
74
72
|
try {
|
|
75
|
-
await
|
|
73
|
+
await axios({
|
|
76
74
|
method: 'GET',
|
|
77
75
|
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
|
|
78
76
|
timeout: 5000,
|
|
77
|
+
headers: getAuthHeaders(),
|
|
79
78
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
80
79
|
});
|
|
81
80
|
if (statoCamera[cam.ip] === false) {
|
|
82
81
|
const nomeOnline = await getCameraName(cam);
|
|
83
|
-
|
|
82
|
+
|
|
83
|
+
node.send({
|
|
84
|
+
payload: {
|
|
85
|
+
tipo_messaggio: "status",
|
|
86
|
+
nome_cliente: node.name,
|
|
87
|
+
nome_telecamera: nomeOnline,
|
|
88
|
+
ip_telecamera: cam.ip,
|
|
89
|
+
channel: cam.channel,
|
|
90
|
+
stato_telecamera: "ONLINE",
|
|
91
|
+
}
|
|
92
|
+
});
|
|
84
93
|
statoCamera[cam.ip] = true;
|
|
85
94
|
} else if (statoCamera[cam.ip] === undefined) {
|
|
86
95
|
statoCamera[cam.ip] = true;
|
|
@@ -88,7 +97,17 @@ module.exports = function(RED) {
|
|
|
88
97
|
} catch (e) {
|
|
89
98
|
if (statoCamera[cam.ip] !== false) {
|
|
90
99
|
const nomeOffline = await getCameraName(cam);
|
|
91
|
-
|
|
100
|
+
|
|
101
|
+
node.send({
|
|
102
|
+
payload: {
|
|
103
|
+
tipo_messaggio: "status",
|
|
104
|
+
nome_cliente: node.name,
|
|
105
|
+
nome_telecamera: nomeOffline,
|
|
106
|
+
ip_telecamera: cam.ip,
|
|
107
|
+
channel: cam.channel,
|
|
108
|
+
stato_telecamera: "OFFLINE",
|
|
109
|
+
}
|
|
110
|
+
});
|
|
92
111
|
statoCamera[cam.ip] = false;
|
|
93
112
|
}
|
|
94
113
|
}
|
|
@@ -109,7 +128,7 @@ module.exports = function(RED) {
|
|
|
109
128
|
|
|
110
129
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
111
130
|
|
|
112
|
-
// --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E
|
|
131
|
+
// --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E PULIZIA ---
|
|
113
132
|
async function downloadMedia(evento, channelID) {
|
|
114
133
|
const camera = node.cameras.find(c => c.channel == channelID);
|
|
115
134
|
const nomeCamera = await getCameraName(camera);
|
|
@@ -119,7 +138,6 @@ module.exports = function(RED) {
|
|
|
119
138
|
if (nowTime - lastTriggerTime < 10000) return;
|
|
120
139
|
lastTriggerTime = nowTime;
|
|
121
140
|
|
|
122
|
-
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
|
|
123
141
|
const referenceTime = new Date();
|
|
124
142
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
125
143
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
@@ -130,7 +148,6 @@ module.exports = function(RED) {
|
|
|
130
148
|
|
|
131
149
|
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
132
150
|
|
|
133
|
-
// Prepariamo la struttura del payload per Python
|
|
134
151
|
let output = {
|
|
135
152
|
tipo_messaggio: "evento",
|
|
136
153
|
nome_cliente: node.name,
|
|
@@ -144,7 +161,6 @@ module.exports = function(RED) {
|
|
|
144
161
|
video_base64: null
|
|
145
162
|
};
|
|
146
163
|
|
|
147
|
-
// Teniamo traccia dei percorsi per poterli cancellare tra 2 minuti
|
|
148
164
|
let fileDaCancellare = [];
|
|
149
165
|
|
|
150
166
|
try {
|
|
@@ -152,8 +168,13 @@ module.exports = function(RED) {
|
|
|
152
168
|
for (let t of tracks) {
|
|
153
169
|
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>`;
|
|
154
170
|
|
|
155
|
-
|
|
156
|
-
|
|
171
|
+
// Usiamo Axios puro con gli header dinamici
|
|
172
|
+
const resSearch = await axios({
|
|
173
|
+
method: 'POST',
|
|
174
|
+
url: `${baseUrl}/search`,
|
|
175
|
+
data: searchXml,
|
|
176
|
+
headers: getAuthHeaders(),
|
|
177
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
157
178
|
});
|
|
158
179
|
|
|
159
180
|
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
@@ -161,32 +182,28 @@ module.exports = function(RED) {
|
|
|
161
182
|
|
|
162
183
|
if (uriMatch) {
|
|
163
184
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
164
|
-
|
|
185
|
+
|
|
186
|
+
const resDown = await axios({
|
|
165
187
|
method: 'GET',
|
|
166
188
|
url: `${baseUrl}/download`,
|
|
167
189
|
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
168
|
-
responseType: 'arraybuffer'
|
|
190
|
+
responseType: 'arraybuffer',
|
|
191
|
+
headers: getAuthHeaders(),
|
|
192
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
169
193
|
});
|
|
170
194
|
|
|
171
195
|
let buffer = Buffer.from(resDown.data);
|
|
172
196
|
if (t.id === "203") {
|
|
173
|
-
// --- SALVA FOTO IN LOCALE COME PRIMA ---
|
|
174
197
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
175
198
|
fs.writeFileSync(fullImgPath, buffer);
|
|
176
|
-
|
|
177
|
-
// La convertiamo subito in testo per il payload
|
|
178
199
|
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
179
|
-
|
|
180
|
-
// Registriamo il file per la distruzione futura
|
|
181
200
|
fileDaCancellare.push(fullImgPath);
|
|
182
201
|
} else {
|
|
183
|
-
// --- SALVA VIDEO IN LOCALE COME PRIMA ---
|
|
184
202
|
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
185
203
|
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
186
204
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
187
205
|
fs.writeFileSync(rawPath, buffer);
|
|
188
206
|
|
|
189
|
-
// Eseguiamo ffmpeg localmente sul PC del cliente
|
|
190
207
|
await new Promise((resolve) => {
|
|
191
208
|
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
192
209
|
if (!err && fs.existsSync(fixedPath)) {
|
|
@@ -203,26 +220,19 @@ module.exports = function(RED) {
|
|
|
203
220
|
}
|
|
204
221
|
}
|
|
205
222
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
// --- TIMER RIGIDO A 2 MINUTI PER LA PULIZIA DEL DISCO ---
|
|
223
|
+
node.send({ payload: output });
|
|
224
|
+
|
|
225
|
+
if (fileDaCancellare.length > 0) {
|
|
211
226
|
setTimeout(() => {
|
|
212
227
|
for (let file of fileDaCancellare) {
|
|
213
|
-
try {
|
|
214
|
-
if (fs.existsSync(file)) {
|
|
215
|
-
fs.unlinkSync(file);
|
|
216
|
-
}
|
|
217
|
-
} catch (err) {
|
|
218
|
-
node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
|
|
219
|
-
}
|
|
228
|
+
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
220
229
|
}
|
|
221
|
-
}, 120000);
|
|
230
|
+
}, 120000);
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
} catch (e) {
|
|
225
|
-
node.error(`Errore Download Cam ${channelID}
|
|
234
|
+
node.error(`Errore Download Cam ${channelID} (${e.message}). Invio allarme base.`);
|
|
235
|
+
node.send({ payload: output });
|
|
226
236
|
}
|
|
227
237
|
updateNodeStatus();
|
|
228
238
|
}
|
|
@@ -230,12 +240,30 @@ module.exports = function(RED) {
|
|
|
230
240
|
// --- ALERT STREAM ---
|
|
231
241
|
function startAlertStream() {
|
|
232
242
|
if (isClosing) return;
|
|
233
|
-
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
234
243
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
235
|
-
|
|
244
|
+
|
|
245
|
+
axios({
|
|
246
|
+
method: 'GET',
|
|
247
|
+
url: url,
|
|
248
|
+
responseType: 'stream',
|
|
249
|
+
headers: getAuthHeaders(),
|
|
250
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
251
|
+
})
|
|
236
252
|
.then(response => {
|
|
237
253
|
streamRequest = response;
|
|
238
|
-
if (!nvrOnline) {
|
|
254
|
+
if (!nvrOnline) {
|
|
255
|
+
const timestamp = Math.floor(Date.now() / 1000);
|
|
256
|
+
node.send({
|
|
257
|
+
payload: {
|
|
258
|
+
tipo_messaggio: "status",
|
|
259
|
+
nome_cliente: node.name,
|
|
260
|
+
ip: node.host,
|
|
261
|
+
tipo_evento: "CAMBIO STATO",
|
|
262
|
+
stato_telecamera: "ONLINE",
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
nvrOnline = true;
|
|
266
|
+
}
|
|
239
267
|
updateNodeStatus();
|
|
240
268
|
response.data.on('data', (chunk) => {
|
|
241
269
|
const data = chunk.toString().toLowerCase();
|
|
@@ -254,7 +282,18 @@ module.exports = function(RED) {
|
|
|
254
282
|
}
|
|
255
283
|
|
|
256
284
|
function handleNvrError() {
|
|
257
|
-
if (nvrOnline) {
|
|
285
|
+
if (nvrOnline) {
|
|
286
|
+
node.send({
|
|
287
|
+
payload: {
|
|
288
|
+
tipo_messaggio: "status",
|
|
289
|
+
nome_cliente: node.name,
|
|
290
|
+
ip: node.host,
|
|
291
|
+
tipo_evento: "CAMBIO STATO",
|
|
292
|
+
stato_telecamera: "OFFLINE",
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
nvrOnline = false;
|
|
296
|
+
}
|
|
258
297
|
updateNodeStatus();
|
|
259
298
|
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
260
299
|
}
|