node-red-contrib-hik-media-buffer 1.1.55 → 1.1.56
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 +40 -27
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -12,7 +12,7 @@ module.exports = function(RED) {
|
|
|
12
12
|
const node = this;
|
|
13
13
|
|
|
14
14
|
node.name = config.name || "TEST";
|
|
15
|
-
node.host = config.host;
|
|
15
|
+
node.host = config.host; // IP dell'NVR centrale
|
|
16
16
|
node.port = config.port || "80";
|
|
17
17
|
node.protocol = config.protocol || "http";
|
|
18
18
|
node.user = config.user;
|
|
@@ -25,7 +25,11 @@ module.exports = function(RED) {
|
|
|
25
25
|
let statoCanale = {};
|
|
26
26
|
|
|
27
27
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
28
|
+
|
|
29
|
+
// EventList per l'alertStream
|
|
28
30
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
31
|
+
|
|
32
|
+
// CARTELLA TEMPORANEA
|
|
29
33
|
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
30
34
|
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
31
35
|
|
|
@@ -33,6 +37,7 @@ module.exports = function(RED) {
|
|
|
33
37
|
|
|
34
38
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
35
39
|
|
|
40
|
+
// --- PRENDE IL NOME DEL CANALE DIRETTAMENTE DALL'NVR ---
|
|
36
41
|
async function getCameraName(channelID) {
|
|
37
42
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
38
43
|
try {
|
|
@@ -42,6 +47,7 @@ module.exports = function(RED) {
|
|
|
42
47
|
timeout: 5000,
|
|
43
48
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
44
49
|
});
|
|
50
|
+
|
|
45
51
|
const data = res.data.toString();
|
|
46
52
|
const match = data.match(/<name>([^<]+)<\/name>/i);
|
|
47
53
|
return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
|
|
@@ -50,9 +56,11 @@ module.exports = function(RED) {
|
|
|
50
56
|
}
|
|
51
57
|
}
|
|
52
58
|
|
|
59
|
+
// --- CONTROLLO STATUS DINAMICO DEI CANALI DIGITALI DALL'NVR ---
|
|
53
60
|
async function checkCameras() {
|
|
54
61
|
if (isClosing) return;
|
|
55
62
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
63
|
+
|
|
56
64
|
try {
|
|
57
65
|
const res = await nvrAuth.request({
|
|
58
66
|
method: 'GET',
|
|
@@ -60,19 +68,23 @@ module.exports = function(RED) {
|
|
|
60
68
|
timeout: 5000,
|
|
61
69
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
62
70
|
});
|
|
71
|
+
|
|
63
72
|
nvrOnline = true;
|
|
64
73
|
const xmlData = res.data.toString();
|
|
74
|
+
|
|
65
75
|
const bloccoRegex = /<InputProxyChannelStatus[\s\\S]*?>([\s\S]*?)<\/InputProxyChannelStatus>/gi;
|
|
66
76
|
let matchBlocco;
|
|
67
77
|
|
|
68
78
|
while ((matchBlocco = bloccoRegex.exec(xmlData)) !== null) {
|
|
69
79
|
const bloccoContenuto = matchBlocco[1];
|
|
80
|
+
|
|
70
81
|
const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
|
|
71
82
|
const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i);
|
|
72
83
|
|
|
73
84
|
if (idMatch && statusMatch) {
|
|
74
85
|
const ch = idMatch[1];
|
|
75
86
|
const statusStr = statusMatch[1].toLowerCase();
|
|
87
|
+
|
|
76
88
|
const isOnline = statusStr.includes("online") || statusStr.includes("recording");
|
|
77
89
|
|
|
78
90
|
if (isOnline && statoCanale[ch] === false) {
|
|
@@ -106,7 +118,7 @@ module.exports = function(RED) {
|
|
|
106
118
|
|
|
107
119
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
108
120
|
|
|
109
|
-
// --- DOWNLOAD
|
|
121
|
+
// --- DOWNLOAD MULTIMEDIALE (XML PER FOTO + JSON PER VIDEO) ---
|
|
110
122
|
async function downloadMedia(evento, channelID) {
|
|
111
123
|
node.warn(`[STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
|
|
112
124
|
|
|
@@ -119,12 +131,11 @@ module.exports = function(RED) {
|
|
|
119
131
|
const referenceTime = new Date();
|
|
120
132
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
121
133
|
|
|
122
|
-
// Finestre temporali
|
|
123
|
-
const startTimeZ = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
|
|
134
|
+
// Finestre temporali standard UTC
|
|
135
|
+
const startTimeZ = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
|
|
124
136
|
const endTimeZ = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
|
|
125
137
|
|
|
126
|
-
//
|
|
127
|
-
// Sfruttiamo una formattazione pulita per l'orario del JSON dell'NVR
|
|
138
|
+
// Finestre temporali per il JSON dell'NVR (con specifica dell'offset locale, es. "+02:00")
|
|
128
139
|
const formatJsonDate = (d) => {
|
|
129
140
|
const pad = (num) => String(num).padStart(2, '0');
|
|
130
141
|
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} 02:00`;
|
|
@@ -135,14 +146,13 @@ module.exports = function(RED) {
|
|
|
135
146
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
136
147
|
const nomeCamera = await getCameraName(channelID);
|
|
137
148
|
|
|
138
|
-
node.warn(`[STEP
|
|
149
|
+
node.warn(`[STEP 2] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
|
|
139
150
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
140
151
|
|
|
141
152
|
const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
let eventoNVR = "FieldDetection"; // "FieldDetection" o "LineDetection" per l'XML
|
|
154
|
+
const eventoMinuscolo = evento.toLowerCase();
|
|
155
|
+
let eventoNVR = "FieldDetection";
|
|
146
156
|
if (eventoMinuscolo.includes("linedetection")) eventoNVR = "LineDetection";
|
|
147
157
|
|
|
148
158
|
let output = {
|
|
@@ -159,11 +169,11 @@ module.exports = function(RED) {
|
|
|
159
169
|
};
|
|
160
170
|
|
|
161
171
|
const parsedChannel = parseInt(channelID, 10);
|
|
162
|
-
const trackFoto = (parsedChannel * 100) + 3; // Es. 203
|
|
172
|
+
const trackFoto = (parsedChannel * 100) + 3; // Es. Canale 2 -> 203
|
|
163
173
|
let fileDaCancellare = [];
|
|
164
174
|
|
|
165
|
-
// --- PARTE 1:
|
|
166
|
-
node.warn(`[STEP
|
|
175
|
+
// --- PARTE 1: RICERCA E SCARICO VIDEO VIA JSON ---
|
|
176
|
+
node.warn(`[STEP 3 - VIDEO] Invio query JSON a eventRecordSearch per canale ${parsedChannel}...`);
|
|
167
177
|
const searchJsonPayload = {
|
|
168
178
|
"EventSearchDescription": {
|
|
169
179
|
"searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
|
|
@@ -190,13 +200,12 @@ module.exports = function(RED) {
|
|
|
190
200
|
headers: { "Content-Type": "application/json" }
|
|
191
201
|
});
|
|
192
202
|
|
|
193
|
-
// Il payload JSON restituisce una struttura differente, estraiamo l'url di download (playbackURI)
|
|
194
203
|
const resDataStr = JSON.stringify(resSearchVideo.data);
|
|
195
204
|
const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
196
205
|
|
|
197
206
|
if (videoUriMatch) {
|
|
198
207
|
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
199
|
-
node.warn(`[STEP
|
|
208
|
+
node.warn(`[STEP 3.1 - VIDEO] Trovato URI video nel JSON: ${rawVideoUri}`);
|
|
200
209
|
|
|
201
210
|
const resDownVideo = await camAuth.request({
|
|
202
211
|
method: 'GET',
|
|
@@ -217,6 +226,7 @@ module.exports = function(RED) {
|
|
|
217
226
|
if (!err && fs.existsSync(fixedPath)) {
|
|
218
227
|
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
219
228
|
fileDaCancellare.push(fixedPath);
|
|
229
|
+
node.warn(`[STEP 3.2 - VIDEO] Convertito video con successo.`);
|
|
220
230
|
} else {
|
|
221
231
|
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
222
232
|
}
|
|
@@ -225,15 +235,16 @@ module.exports = function(RED) {
|
|
|
225
235
|
});
|
|
226
236
|
});
|
|
227
237
|
} else {
|
|
228
|
-
node.warn(`[STEP
|
|
238
|
+
node.warn(`[STEP 3.3 - VIDEO] Nessun file video indicizzato per questa finestra temporale.`);
|
|
229
239
|
}
|
|
230
240
|
} catch (videoErr) {
|
|
231
|
-
node.error(`[VIDEO ERR] Errore durante la ricerca
|
|
241
|
+
node.error(`[VIDEO ERR] Errore durante la ricerca JSON: ${videoErr.message}`);
|
|
232
242
|
}
|
|
233
243
|
|
|
234
|
-
// --- PARTE 2:
|
|
235
|
-
node.warn(`[STEP
|
|
236
|
-
|
|
244
|
+
// --- PARTE 2: RICERCA E SCARICO FOTO VIA XML (CORRETTO) ---
|
|
245
|
+
node.warn(`[STEP 4 - FOTO] Invio query XML a /search per traccia foto ${trackFoto}...`);
|
|
246
|
+
|
|
247
|
+
const searchXmlFoto = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${trackFoto}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTimeZ}</startTime><endTime>${endTimeZ}</endTime></timeSpan></timeSpanList><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPosition>0</searchResultPosition><metadataList><metadataDescriptor>recordType.meta.hikvision.com/timing</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
237
248
|
|
|
238
249
|
try {
|
|
239
250
|
const resSearchFoto = await camAuth.request({
|
|
@@ -248,7 +259,7 @@ module.exports = function(RED) {
|
|
|
248
259
|
|
|
249
260
|
if (uriMatch) {
|
|
250
261
|
const rawUriFoto = uriMatch[1].replace(/&/g, '&');
|
|
251
|
-
node.warn(`[STEP
|
|
262
|
+
node.warn(`[STEP 4.1 - FOTO] Trovato URI foto: ${rawUriFoto}`);
|
|
252
263
|
|
|
253
264
|
const resDownFoto = await camAuth.request({
|
|
254
265
|
method: 'GET',
|
|
@@ -261,16 +272,17 @@ module.exports = function(RED) {
|
|
|
261
272
|
fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
|
|
262
273
|
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
263
274
|
fileDaCancellare.push(fullImgPath);
|
|
275
|
+
node.warn(`[STEP 4.2 - FOTO] Foto salvata e convertita correttamente.`);
|
|
264
276
|
} else {
|
|
265
|
-
node.warn(`[STEP
|
|
277
|
+
node.warn(`[STEP 4.3 - FOTO] Nessuna foto trovata per questa finestra temporale.`);
|
|
266
278
|
}
|
|
267
279
|
} catch (fotoErr) {
|
|
268
|
-
node.error(`[FOTO ERR] Errore durante la ricerca
|
|
280
|
+
node.error(`[FOTO ERR] Errore durante la ricerca XML: ${fotoErr.message}`);
|
|
269
281
|
}
|
|
270
282
|
|
|
271
|
-
// ---
|
|
283
|
+
// --- PARTE 3: SPEDIZIONE VERSO PYTHON ---
|
|
272
284
|
if (output.foto_base64 || output.video_base64) {
|
|
273
|
-
node.warn(`[STEP
|
|
285
|
+
node.warn(`[STEP 5] Spedizione payload multimediale completata!`);
|
|
274
286
|
node.send({ payload: output });
|
|
275
287
|
|
|
276
288
|
setTimeout(() => {
|
|
@@ -279,11 +291,12 @@ module.exports = function(RED) {
|
|
|
279
291
|
}
|
|
280
292
|
}, 120000);
|
|
281
293
|
} else {
|
|
282
|
-
node.warn(`[STEP
|
|
294
|
+
node.warn(`[STEP 5] Nessun file multimediale scaricato, nessun pacchetto inviato.`);
|
|
283
295
|
}
|
|
284
|
-
|
|
296
|
+
updateNodeStatus();
|
|
285
297
|
}
|
|
286
298
|
|
|
299
|
+
// --- ALERT STREAM ---
|
|
287
300
|
function startAlertStream() {
|
|
288
301
|
if (isClosing) return;
|
|
289
302
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|