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