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