node-red-contrib-hik-media-buffer 1.1.115 → 1.1.117
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 +37 -37
- package/hik-snapshot.js +72 -286
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -35,27 +35,28 @@ module.exports = function(RED) {
|
|
|
35
35
|
|
|
36
36
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
37
37
|
|
|
38
|
-
// --- PRENDE IL NOME DELLA TELECAMERA DALL'NVR ---
|
|
38
|
+
// --- PRENDE IL NOME REALE DELLA TELECAMERA DALL'NVR ---
|
|
39
39
|
async function getCameraNameFromNVR(channelID) {
|
|
40
40
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
41
41
|
try {
|
|
42
42
|
const res = await nvrAuth.request({
|
|
43
43
|
method: 'GET',
|
|
44
|
-
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}
|
|
44
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}`,
|
|
45
45
|
timeout: 5000,
|
|
46
46
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const data = res.data.toString();
|
|
50
|
-
|
|
50
|
+
// Cerchiamo il tag channelName che contiene il nome impostato sull'NVR
|
|
51
|
+
const match = data.match(/<channelName>([^<]+)<\/channelName>/i);
|
|
51
52
|
if (match && match[1]) return match[1].trim();
|
|
52
|
-
return `
|
|
53
|
+
return `Canale ${channelID}`;
|
|
53
54
|
} catch (e) {
|
|
54
|
-
return `
|
|
55
|
+
return `Canale ${channelID}`;
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// --- CONTROLLO STATUS CAMERE INTERROGANDO
|
|
59
|
+
// --- CONTROLLO STATUS CAMERE (ONLINE/OFFLINE) INTERROGANDO L'NVR ---
|
|
59
60
|
async function checkCameras() {
|
|
60
61
|
if (isClosing || !nvrOnline) return;
|
|
61
62
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
@@ -72,25 +73,29 @@ module.exports = function(RED) {
|
|
|
72
73
|
|
|
73
74
|
for (let block of channelBlocks) {
|
|
74
75
|
const idMatch = block.match(/<id>(\d+)<\/id>/i);
|
|
75
|
-
|
|
76
|
+
// Hikvision usa il tag <online> true/false per lo stato della telecamera IP accoppiata
|
|
77
|
+
const onlineMatch = block.match(/<online>([^<]+)<\/online>/i) || block.match(/<onLine>([^<]+)<\/onLine>/i);
|
|
76
78
|
|
|
77
79
|
if (idMatch) {
|
|
78
80
|
const ch = idMatch[1];
|
|
79
|
-
|
|
81
|
+
// Se il tag dice true è online, altrimenti se dice false (o manca) è offline
|
|
82
|
+
const isOnline = onlineMatch ? onlineMatch[1].trim().toLowerCase() === "true" : true;
|
|
80
83
|
|
|
81
84
|
if (isOnline && statoCamera[ch] === false) {
|
|
82
85
|
const nomeOnline = await getCameraNameFromNVR(ch);
|
|
83
86
|
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: node.host, channel: ch, msg: "Camera ripristinata" } });
|
|
84
87
|
statoCamera[ch] = true;
|
|
85
88
|
} else if (!isOnline && statoCamera[ch] !== false) {
|
|
86
|
-
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `
|
|
89
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera ${ch}`, ip_telecamera: node.host, channel: ch, msg: "Camera non raggiungibile" } });
|
|
87
90
|
statoCamera[ch] = false;
|
|
88
91
|
} else if (statoCamera[ch] === undefined) {
|
|
89
92
|
statoCamera[ch] = isOnline;
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
|
-
} catch (e) {
|
|
96
|
+
} catch (e) {
|
|
97
|
+
node.error(`Errore durante il check delle camere: ${e.message}`);
|
|
98
|
+
}
|
|
94
99
|
updateNodeStatus();
|
|
95
100
|
}
|
|
96
101
|
|
|
@@ -107,7 +112,12 @@ module.exports = function(RED) {
|
|
|
107
112
|
|
|
108
113
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
109
114
|
|
|
110
|
-
// ---
|
|
115
|
+
// --- FORMATTATORE PER IL BODY DI RICERCA (Con i trattini: 2026-07-07T17:24:00Z) ---
|
|
116
|
+
function toHikSearchDate(d) {
|
|
117
|
+
return d.toISOString().split('.')[0] + "Z";
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// --- DOWNLOAD CON ORARIO DINAMICO STRETTO SULL'ULTIMO EVENTO ---
|
|
111
121
|
async function downloadMedia(evento, channelID) {
|
|
112
122
|
const nowTime = Date.now();
|
|
113
123
|
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
@@ -118,15 +128,16 @@ module.exports = function(RED) {
|
|
|
118
128
|
const referenceTime = new Date();
|
|
119
129
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
120
130
|
|
|
121
|
-
// Calcolo dinamico del Track ID per il Video (canale * 100 + 1)
|
|
122
131
|
const videoTrackID = (parseInt(channelID) * 100) + 1;
|
|
123
|
-
|
|
124
132
|
const nomeCamera = await getCameraNameFromNVR(channelID);
|
|
125
133
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
126
134
|
|
|
127
|
-
//
|
|
128
|
-
const
|
|
129
|
-
const
|
|
135
|
+
// FINESTRA TEMPORALE REALE: Cerchiamo l'evento da 2 minuti fa a 1 minuto nel futuro
|
|
136
|
+
const inizioFinestra = new Date(referenceTime.getTime() - (2 * 60 * 1000));
|
|
137
|
+
const fineFinestra = new Date(referenceTime.getTime() + (1 * 60 * 1000));
|
|
138
|
+
|
|
139
|
+
const startSearchStr = toHikSearchDate(inizioFinestra);
|
|
140
|
+
const endSearchStr = toHikSearchDate(fineFinestra);
|
|
130
141
|
|
|
131
142
|
let output = {
|
|
132
143
|
tipo_messaggio: "evento",
|
|
@@ -144,16 +155,13 @@ module.exports = function(RED) {
|
|
|
144
155
|
let fileDaCancellare = [];
|
|
145
156
|
|
|
146
157
|
try {
|
|
147
|
-
// 1. SCARICAMENTO IMMAGINE (
|
|
158
|
+
// 1. 📸 RICERCA E SCARICAMENTO IMMAGINE (Formato con i trattini nel body)
|
|
148
159
|
const payloadFoto = {
|
|
149
160
|
"EventSearchDescription": {
|
|
150
161
|
"searchID": "C5AFEE35-B1E0-4C01-83F8-47FD77892E4A",
|
|
151
162
|
"searchResultPosition": 0,
|
|
152
|
-
"maxResults":
|
|
153
|
-
"timeSpanList": [{
|
|
154
|
-
"startTime": startSearchStr,
|
|
155
|
-
"endTime": endSearchStr
|
|
156
|
-
}],
|
|
163
|
+
"maxResults": 1, // Prendiamo solo il più vicino all'evento attuale
|
|
164
|
+
"timeSpanList": [{ "startTime": startSearchStr, "endTime": endSearchStr }],
|
|
157
165
|
"type": "all",
|
|
158
166
|
"channels": [parseInt(channelID)],
|
|
159
167
|
"eventType": "behavior",
|
|
@@ -169,7 +177,6 @@ module.exports = function(RED) {
|
|
|
169
177
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
170
178
|
});
|
|
171
179
|
|
|
172
|
-
// CORRETTO: Adesso cerca dentro Targets e prende il pictureUrl grezzo come su Postman
|
|
173
180
|
if (resFotoSearch.data?.EventSearchResult?.Targets?.[0]?.pictureUrl) {
|
|
174
181
|
const urlFotoGrezzo = resFotoSearch.data.EventSearchResult.Targets[0].pictureUrl;
|
|
175
182
|
|
|
@@ -186,21 +193,18 @@ module.exports = function(RED) {
|
|
|
186
193
|
fileDaCancellare.push(fullImgPath);
|
|
187
194
|
}
|
|
188
195
|
|
|
189
|
-
// 2. SCARICAMENTO VIDEO (
|
|
190
|
-
const startVideoSearch = referenceTime.toISOString().split('T')[0] + "T00:00:00Z";
|
|
191
|
-
const endVideoSearch = referenceTime.toISOString().split('T')[0] + "T23:59:59Z";
|
|
192
|
-
|
|
196
|
+
// 2. 🎥 RICERCA E SCARICAMENTO VIDEO (Formato con i trattini nel body)
|
|
193
197
|
const payloadVideoSearch = `<?xml version="1.0" encoding="utf-8"?>
|
|
194
198
|
<CMSearchDescription>
|
|
195
199
|
<searchID>CEC13F0F-1AEA-4474-A2DA-CFFF332C7C5B</searchID>
|
|
196
200
|
<trackList><trackID>${videoTrackID}</trackID></trackList>
|
|
197
201
|
<timeSpanList>
|
|
198
202
|
<timeSpan>
|
|
199
|
-
<startTime>${
|
|
200
|
-
<endTime>${
|
|
203
|
+
<startTime>${startSearchStr}</startTime>
|
|
204
|
+
<endTime>${endSearchStr}</endTime>
|
|
201
205
|
</timeSpan>
|
|
202
206
|
</timeSpanList>
|
|
203
|
-
<maxResults>
|
|
207
|
+
<maxResults>1</maxResults>
|
|
204
208
|
<searchResultPostion>0</searchResultPostion>
|
|
205
209
|
<metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
|
|
206
210
|
</CMSearchDescription>`;
|
|
@@ -217,8 +221,10 @@ module.exports = function(RED) {
|
|
|
217
221
|
const uriMatch = xmlString.match(/<playbackURI>([\s\S]*?)<\/playbackURI>/i);
|
|
218
222
|
|
|
219
223
|
if (uriMatch && uriMatch[1]) {
|
|
224
|
+
// Estrae il playbackURI grezzo che contiene GIÀ la data compressa senza trattini generata dall'NVR
|
|
220
225
|
const playbackURIGrezzo = uriMatch[1].trim();
|
|
221
226
|
|
|
227
|
+
// Inviamo il downloadRequest passando l'URI nativo fornito dall'NVR (che rispetta il formato compresso)
|
|
222
228
|
const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
|
|
223
229
|
<downloadRequest>
|
|
224
230
|
<playbackURI>${playbackURIGrezzo}</playbackURI>
|
|
@@ -254,17 +260,11 @@ module.exports = function(RED) {
|
|
|
254
260
|
});
|
|
255
261
|
}
|
|
256
262
|
|
|
257
|
-
// Invio dei media elaborati
|
|
258
263
|
if (output.foto_base64 || output.video_base64) {
|
|
259
264
|
node.send({ payload: output });
|
|
260
|
-
|
|
261
265
|
setTimeout(() => {
|
|
262
266
|
for (let file of fileDaCancellare) {
|
|
263
|
-
try {
|
|
264
|
-
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
265
|
-
} catch (err) {
|
|
266
|
-
node.error(`Errore pulizia file temporaneo ${file}: ${err.message}`);
|
|
267
|
-
}
|
|
267
|
+
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
268
268
|
}
|
|
269
269
|
}, 120000);
|
|
270
270
|
}
|
package/hik-snapshot.js
CHANGED
|
@@ -1,320 +1,106 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
-
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
3
2
|
const https = require('https');
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const os = require('os');
|
|
7
|
-
const { exec } = require('child_process');
|
|
3
|
+
const mhocDigestSnapshot = require('@mhoc/axios-digest-auth');
|
|
4
|
+
const DigestAuthClass = mhocDigestSnapshot.default;
|
|
8
5
|
|
|
9
6
|
module.exports = function(RED) {
|
|
10
|
-
function
|
|
7
|
+
function HikSnapshotNode(config) {
|
|
11
8
|
RED.nodes.createNode(this, config);
|
|
12
9
|
const node = this;
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
|
|
11
|
+
// Recuperiamo il nome del nodo dalla configurazione (se vuoto usa l'host)
|
|
12
|
+
node.nodeName = config.name || `NVR_${config.host}`;
|
|
13
|
+
|
|
14
|
+
node.protocol = config.protocol || "http";
|
|
15
15
|
node.host = config.host;
|
|
16
16
|
node.port = config.port || "80";
|
|
17
|
-
node.protocol = config.protocol || "http";
|
|
18
17
|
node.user = config.user;
|
|
19
18
|
node.pass = config.pass;
|
|
20
|
-
|
|
21
|
-
let streamRequest = null;
|
|
22
|
-
let isClosing = false;
|
|
23
|
-
let lastTriggerTime = {};
|
|
24
|
-
let nvrOnline = true;
|
|
25
|
-
let statoCamera = {};
|
|
19
|
+
node.maxChannels = parseInt(config.channel) || 1;
|
|
26
20
|
|
|
27
21
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
28
|
-
const EventList = ["FieldDetection", "LineDetection"];
|
|
29
|
-
|
|
30
|
-
// CARTELLA TEMPORANEA
|
|
31
|
-
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
32
|
-
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
33
|
-
|
|
34
|
-
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
35
|
-
|
|
36
|
-
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
37
|
-
|
|
38
|
-
// --- PRENDE IL NOME REALE DELLA TELECAMERA DALL'NVR ---
|
|
39
|
-
async function getCameraNameFromNVR(channelID) {
|
|
40
|
-
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
41
|
-
try {
|
|
42
|
-
const res = await nvrAuth.request({
|
|
43
|
-
method: 'GET',
|
|
44
|
-
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}`,
|
|
45
|
-
timeout: 5000,
|
|
46
|
-
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const data = res.data.toString();
|
|
50
|
-
// Cerchiamo il tag channelName che contiene il nome impostato sull'NVR
|
|
51
|
-
const match = data.match(/<channelName>([^<]+)<\/channelName>/i);
|
|
52
|
-
if (match && match[1]) return match[1].trim();
|
|
53
|
-
return `Canale ${channelID}`;
|
|
54
|
-
} catch (e) {
|
|
55
|
-
return `Canale ${channelID}`;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// --- CONTROLLO STATUS CAMERE (ONLINE/OFFLINE) INTERROGANDO L'NVR ---
|
|
60
|
-
async function checkCameras() {
|
|
61
|
-
if (isClosing || !nvrOnline) return;
|
|
62
|
-
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
63
|
-
try {
|
|
64
|
-
const res = await nvrAuth.request({
|
|
65
|
-
method: 'GET',
|
|
66
|
-
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels`,
|
|
67
|
-
timeout: 8000,
|
|
68
|
-
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
const xml = res.data.toString();
|
|
72
|
-
const channelBlocks = xml.match(/<VideoInputChannel>[\s\S]*?<\/VideoInputChannel>/g) || [];
|
|
73
|
-
|
|
74
|
-
for (let block of channelBlocks) {
|
|
75
|
-
const idMatch = block.match(/<id>(\d+)<\/id>/i);
|
|
76
|
-
// Hikvision usa il tag <online> true/false per lo stato della telecamera IP accoppiata
|
|
77
|
-
const onlineMatch = block.match(/<online>([^<]+)<\/online>/i) || block.match(/<onLine>([^<]+)<\/onLine>/i);
|
|
78
|
-
|
|
79
|
-
if (idMatch) {
|
|
80
|
-
const ch = idMatch[1];
|
|
81
|
-
// Se il tag dice true è online, altrimenti se dice false (o manca) è offline
|
|
82
|
-
const isOnline = onlineMatch ? onlineMatch[1].trim().toLowerCase() === "true" : true;
|
|
83
|
-
|
|
84
|
-
if (isOnline && statoCamera[ch] === false) {
|
|
85
|
-
const nomeOnline = await getCameraNameFromNVR(ch);
|
|
86
|
-
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: node.host, channel: ch, msg: "Camera ripristinata" } });
|
|
87
|
-
statoCamera[ch] = true;
|
|
88
|
-
} else if (!isOnline && statoCamera[ch] !== false) {
|
|
89
|
-
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera ${ch}`, ip_telecamera: node.host, channel: ch, msg: "Camera non raggiungibile" } });
|
|
90
|
-
statoCamera[ch] = false;
|
|
91
|
-
} else if (statoCamera[ch] === undefined) {
|
|
92
|
-
statoCamera[ch] = isOnline;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
} catch (e) {
|
|
97
|
-
node.error(`Errore durante il check delle camere: ${e.message}`);
|
|
98
|
-
}
|
|
99
|
-
updateNodeStatus();
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function updateNodeStatus() {
|
|
103
|
-
const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
|
|
104
|
-
if (!nvrOnline) {
|
|
105
|
-
node.status({fill:"red", shape:"ring", text:"NVR Offline"});
|
|
106
|
-
} else if (offlineCams > 0) {
|
|
107
|
-
node.status({fill:"yellow", shape:"dot", text: `${offlineCams} Cam Offline`});
|
|
108
|
-
} else {
|
|
109
|
-
node.status({fill:"green", shape:"ring", text:"In ascolto"});
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
22
|
|
|
113
|
-
|
|
23
|
+
node.on('input', async function(msg) {
|
|
24
|
+
if (msg.payload !== true) return;
|
|
114
25
|
|
|
115
|
-
|
|
116
|
-
async function downloadMedia(evento, channelID) {
|
|
117
|
-
const nowTime = Date.now();
|
|
118
|
-
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
119
|
-
if (nowTime - lastTriggerTime[channelID] < 5000) return;
|
|
120
|
-
lastTriggerTime[channelID] = nowTime;
|
|
121
|
-
|
|
122
|
-
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
123
|
-
const referenceTime = new Date();
|
|
124
|
-
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
125
|
-
|
|
126
|
-
const videoTrackID = (parseInt(channelID) * 100) + 1;
|
|
26
|
+
node.status({fill: "blue", shape: "dot", text: "Verifica canali..."});
|
|
127
27
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
28
|
+
const digest = new DigestAuthClass({
|
|
29
|
+
username: node.user,
|
|
30
|
+
password: node.pass
|
|
31
|
+
});
|
|
131
32
|
|
|
132
|
-
const
|
|
133
|
-
const
|
|
33
|
+
const data = new Date();
|
|
34
|
+
const year = data.getFullYear();
|
|
35
|
+
const month = data.getMonth() + 1;
|
|
36
|
+
const day = data.getDate();
|
|
134
37
|
|
|
135
|
-
let
|
|
136
|
-
tipo_messaggio: "evento",
|
|
137
|
-
nome_cliente: node.name,
|
|
138
|
-
nome_telecamera: nomeCamera,
|
|
139
|
-
ip_telecamera: node.host,
|
|
140
|
-
tipo_evento: evento,
|
|
141
|
-
timestamp_epoch: timestamp,
|
|
142
|
-
stato_telecamera: "ONLINE",
|
|
143
|
-
channel: channelID.toString(),
|
|
144
|
-
foto_base64: null,
|
|
145
|
-
video_base64: null
|
|
146
|
-
};
|
|
38
|
+
let snapshotResults = [];
|
|
147
39
|
|
|
148
|
-
let
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
"type": "all",
|
|
162
|
-
"channels": [parseInt(channelID)],
|
|
163
|
-
"eventType": "behavior",
|
|
164
|
-
"behavior": { "behaviorEventType": "linedetection" }
|
|
165
|
-
}
|
|
40
|
+
for (let i = 1; i <= node.maxChannels; i++) {
|
|
41
|
+
const chanId = i + "01";
|
|
42
|
+
const snapUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/Streaming/channels/${chanId}/picture`;
|
|
43
|
+
const recordUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/record/tracks/${chanId}/dailyDistribution`;
|
|
44
|
+
|
|
45
|
+
const recordXml = `<?xml version="1.0" encoding="utf-8"?><trackDailyParam><year>${year}</year><monthOfYear>${month}</monthOfYear><dayOfMonth>${day}</dayOfMonth></trackDailyParam>`;
|
|
46
|
+
|
|
47
|
+
let resCanale = {
|
|
48
|
+
name: node.nodeName, // <--- INIEZIONE RIGIDA DEL NOME DEL CONDOMINIO
|
|
49
|
+
channel: i,
|
|
50
|
+
photo: null,
|
|
51
|
+
snapOk: false,
|
|
52
|
+
isRecording: false
|
|
166
53
|
};
|
|
167
54
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
data: payloadFoto,
|
|
172
|
-
headers: { "Content-Type": "application/json" },
|
|
173
|
-
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
if (resFotoSearch.data?.EventSearchResult?.Targets?.[0]?.pictureUrl) {
|
|
177
|
-
const urlFotoGrezzo = resFotoSearch.data.EventSearchResult.Targets[0].pictureUrl;
|
|
178
|
-
|
|
179
|
-
const resDownFoto = await nvrAuth.request({
|
|
55
|
+
// 1. SNAPSHOT
|
|
56
|
+
try {
|
|
57
|
+
const responseSnap = await digest.request({
|
|
180
58
|
method: 'GET',
|
|
181
|
-
url:
|
|
59
|
+
url: snapUrl,
|
|
182
60
|
responseType: 'arraybuffer',
|
|
183
|
-
httpsAgent: node.protocol ===
|
|
61
|
+
httpsAgent: node.protocol === 'https' ? httpsAgent : undefined,
|
|
62
|
+
timeout: 5000
|
|
184
63
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
fileDaCancellare.push(fullImgPath);
|
|
64
|
+
resCanale.photo = responseSnap.data;
|
|
65
|
+
resCanale.snapOk = true;
|
|
66
|
+
} catch (err) {
|
|
67
|
+
resCanale.snapError = err.message;
|
|
190
68
|
}
|
|
191
69
|
|
|
192
|
-
// 2.
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
<timeSpan>
|
|
202
|
-
<startTime>${startVideoSearch}</startTime>
|
|
203
|
-
<endTime>${endVideoSearch}</endTime>
|
|
204
|
-
</timeSpan>
|
|
205
|
-
</timeSpanList>
|
|
206
|
-
<maxResults>100</maxResults>
|
|
207
|
-
<searchResultPostion>0</searchResultPostion>
|
|
208
|
-
<metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
|
|
209
|
-
</CMSearchDescription>`;
|
|
210
|
-
|
|
211
|
-
const resVideoSearch = await nvrAuth.request({
|
|
212
|
-
method: 'POST',
|
|
213
|
-
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
|
|
214
|
-
data: payloadVideoSearch,
|
|
215
|
-
headers: { "Content-Type": "application/xml" },
|
|
216
|
-
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
217
|
-
});
|
|
218
|
-
|
|
219
|
-
const xmlString = resVideoSearch.data.toString();
|
|
220
|
-
const uriMatch = xmlString.match(/<playbackURI>([\s\S]*?)<\/playbackURI>/i);
|
|
221
|
-
|
|
222
|
-
if (uriMatch && uriMatch[1]) {
|
|
223
|
-
const playbackURIGrezzo = uriMatch[1].trim();
|
|
224
|
-
|
|
225
|
-
const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
|
|
226
|
-
<downloadRequest>
|
|
227
|
-
<playbackURI>${playbackURIGrezzo}</playbackURI>
|
|
228
|
-
</downloadRequest>`;
|
|
229
|
-
|
|
230
|
-
const resDownVideo = await nvrAuth.request({
|
|
231
|
-
method: 'GET',
|
|
232
|
-
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download`,
|
|
233
|
-
data: payloadDownload,
|
|
234
|
-
headers: { "Content-Type": "application/xml" },
|
|
235
|
-
responseType: 'arraybuffer',
|
|
236
|
-
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
let bufferVideo = Buffer.from(resDownVideo.data);
|
|
240
|
-
if (bufferVideo.slice(0, 4).toString() === 'IMKH') bufferVideo = bufferVideo.slice(40);
|
|
241
|
-
|
|
242
|
-
const rawPath = path.join(baseStorage, `raw_${timestamp}_ch${channelID}.mp4`);
|
|
243
|
-
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
244
|
-
fs.writeFileSync(rawPath, bufferVideo);
|
|
245
|
-
|
|
246
|
-
await new Promise((resolve) => {
|
|
247
|
-
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
248
|
-
if (!err && fs.existsSync(fixedPath)) {
|
|
249
|
-
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
250
|
-
fileDaCancellare.push(fixedPath);
|
|
251
|
-
} else {
|
|
252
|
-
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
253
|
-
}
|
|
254
|
-
fileDaCancellare.push(rawPath);
|
|
255
|
-
resolve();
|
|
256
|
-
});
|
|
70
|
+
// 2. RECORDING
|
|
71
|
+
try {
|
|
72
|
+
const responseRec = await digest.request({
|
|
73
|
+
method: 'POST',
|
|
74
|
+
url: recordUrl,
|
|
75
|
+
data: recordXml,
|
|
76
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
77
|
+
httpsAgent: node.protocol === 'https' ? httpsAgent : undefined,
|
|
78
|
+
timeout: 5000
|
|
257
79
|
});
|
|
80
|
+
const xmlOutput = responseRec.data.toString();
|
|
81
|
+
const regex = new RegExp(`<id>${day}</id>[^]*?<record>true</record>`);
|
|
82
|
+
resCanale.isRecording = regex.test(xmlOutput);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
resCanale.recError = err.message;
|
|
258
85
|
}
|
|
259
86
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
setTimeout(() => {
|
|
264
|
-
for (let file of fileDaCancellare) {
|
|
265
|
-
try {
|
|
266
|
-
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
267
|
-
} catch (err) {}
|
|
268
|
-
}
|
|
269
|
-
}, 120000);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
} catch (e) {
|
|
273
|
-
node.error(`Errore Download Cam ${channelID}: ${e.message}`);
|
|
87
|
+
snapshotResults.push(resCanale);
|
|
88
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
274
89
|
}
|
|
275
|
-
updateNodeStatus();
|
|
276
|
-
}
|
|
277
90
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
if (isClosing) return;
|
|
281
|
-
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
282
|
-
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
283
|
-
nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
|
|
284
|
-
.then(response => {
|
|
285
|
-
streamRequest = response;
|
|
286
|
-
if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
|
|
287
|
-
updateNodeStatus();
|
|
288
|
-
response.data.on('data', (chunk) => {
|
|
289
|
-
const data = chunk.toString().toLowerCase();
|
|
290
|
-
if (data.includes("active")) {
|
|
291
|
-
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
292
|
-
if (chMatch) {
|
|
293
|
-
let ev = "Unknown";
|
|
294
|
-
for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
|
|
295
|
-
downloadMedia(ev, chMatch[1]);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
});
|
|
299
|
-
response.data.on('error', () => handleNvrError());
|
|
300
|
-
response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
|
|
301
|
-
}).catch(() => handleNvrError());
|
|
302
|
-
}
|
|
91
|
+
msg.payload = snapshotResults;
|
|
92
|
+
node.send(msg);
|
|
303
93
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
isClosing = true;
|
|
314
|
-
clearInterval(heartbeatInterval);
|
|
315
|
-
if (streamRequest) streamRequest.data.destroy();
|
|
316
|
-
done();
|
|
94
|
+
// Conteggi per lo stato del nodo
|
|
95
|
+
const snapCount = snapshotResults.filter(v => v.snapOk).length;
|
|
96
|
+
const recCount = snapshotResults.filter(v => v.isRecording).length;
|
|
97
|
+
|
|
98
|
+
node.status({
|
|
99
|
+
fill: (snapCount === node.maxChannels && recCount === node.maxChannels) ? "green" : "yellow",
|
|
100
|
+
shape: "dot",
|
|
101
|
+
text: `Snap: ${snapCount}/${node.maxChannels} | Rec: ${recCount}/${node.maxChannels}`
|
|
102
|
+
});
|
|
317
103
|
});
|
|
318
104
|
}
|
|
319
|
-
RED.nodes.registerType("hik-
|
|
105
|
+
RED.nodes.registerType("hik-snapshot", HikSnapshotNode);
|
|
320
106
|
};
|