node-red-contrib-hik-media-buffer 1.1.81 → 1.1.83
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 +85 -109
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
2
|
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
3
|
-
const https = require('https');
|
|
4
3
|
const fs = require('fs');
|
|
5
4
|
const path = require('path');
|
|
6
5
|
const os = require('os');
|
|
7
6
|
const { exec } = require('child_process');
|
|
8
|
-
const { channel } = require('diagnostics_channel');
|
|
9
7
|
|
|
10
8
|
module.exports = function(RED) {
|
|
11
9
|
function HikMediaBufferNode(config) {
|
|
@@ -18,8 +16,6 @@ module.exports = function(RED) {
|
|
|
18
16
|
node.protocol = config.protocol || "http";
|
|
19
17
|
node.user = config.user;
|
|
20
18
|
node.pass = config.pass;
|
|
21
|
-
node.camPass = config.camPass || config.pass;
|
|
22
|
-
node.cameras = config.cameras || [];
|
|
23
19
|
|
|
24
20
|
let streamRequest = null;
|
|
25
21
|
let isClosing = false;
|
|
@@ -27,10 +23,7 @@ module.exports = function(RED) {
|
|
|
27
23
|
let nvrOnline = true;
|
|
28
24
|
let statoCamera = {};
|
|
29
25
|
|
|
30
|
-
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
31
26
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
32
|
-
|
|
33
|
-
// CARTELLA TEMPORANEA
|
|
34
27
|
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
35
28
|
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
36
29
|
|
|
@@ -38,60 +31,59 @@ module.exports = function(RED) {
|
|
|
38
31
|
|
|
39
32
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
40
33
|
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
-
const camAuth = new AxiosDigestAuth({
|
|
44
|
-
username: node.user,
|
|
45
|
-
password: node.camPass
|
|
46
|
-
});
|
|
34
|
+
// Unica libreria di autenticazione usata per tutto il flusso
|
|
35
|
+
const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
47
36
|
|
|
37
|
+
// --- PRENDE IL NOME DELLA TELECAMERA DALL'NVR ---
|
|
38
|
+
async function getCameraName(channelID) {
|
|
48
39
|
try {
|
|
49
|
-
const res = await
|
|
40
|
+
const res = await nvrAuthAxios.request({
|
|
50
41
|
method: 'GET',
|
|
51
|
-
url: `${node.protocol}://${
|
|
52
|
-
timeout: 5000
|
|
53
|
-
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
42
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
|
|
43
|
+
timeout: 5000
|
|
54
44
|
});
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (match && match[1]) {
|
|
60
|
-
return match[1].trim();
|
|
61
|
-
} else {
|
|
62
|
-
return `Canale_${cam.channel}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
45
|
+
const match = res.data.toString().match(/<name>([^<]+)<\/name>/i);
|
|
46
|
+
if (match && match[1]) return match[1].trim();
|
|
47
|
+
return `Canale_${channelID}`;
|
|
65
48
|
} catch (e) {
|
|
66
|
-
return `Camera_${
|
|
49
|
+
return `Camera_${channelID}`;
|
|
67
50
|
}
|
|
68
51
|
}
|
|
69
52
|
|
|
70
|
-
// --- CONTROLLO STATUS CAMERE ---
|
|
53
|
+
// --- CONTROLLO STATUS CAMERE AUTOMATICO DALL'NVR ---
|
|
71
54
|
async function checkCameras() {
|
|
72
|
-
if (isClosing) return;
|
|
73
|
-
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
55
|
+
if (isClosing || !nvrOnline) return;
|
|
56
|
+
try {
|
|
57
|
+
const res = await nvrAuthAxios.request({
|
|
58
|
+
method: 'GET',
|
|
59
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
|
|
60
|
+
timeout: 8000
|
|
61
|
+
});
|
|
62
|
+
const xml = res.data.toString();
|
|
63
|
+
const statusBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/gi) || [];
|
|
64
|
+
|
|
65
|
+
for (let block of statusBlocks) {
|
|
66
|
+
const idMatch = block.match(/<id>(\d+)<\/id>/i);
|
|
67
|
+
const statusMatch = block.match(/<online>([^<]+)<\/online>/i);
|
|
68
|
+
|
|
69
|
+
if (idMatch && statusMatch) {
|
|
70
|
+
const ch = idMatch[1].trim();
|
|
71
|
+
const isOnlineNow = statusMatch[1].trim().toLowerCase() === 'true';
|
|
72
|
+
|
|
73
|
+
if (isOnlineNow && statoCamera[ch] === false) {
|
|
74
|
+
const nomeOnline = await getCameraName(ch);
|
|
75
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, channel: ch, msg: "Camera ripristinata" } });
|
|
76
|
+
statoCamera[ch] = true;
|
|
77
|
+
} else if (!isOnlineNow && statoCamera[ch] !== false && statoCamera[ch] !== undefined) {
|
|
78
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${ch}`, channel: ch, msg: "Camera scollegata dall'NVR" } });
|
|
79
|
+
statoCamera[ch] = false;
|
|
80
|
+
} else if (statoCamera[ch] === undefined) {
|
|
81
|
+
statoCamera[ch] = isOnlineNow;
|
|
82
|
+
}
|
|
93
83
|
}
|
|
94
84
|
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// Silenzioso
|
|
95
87
|
}
|
|
96
88
|
updateNodeStatus();
|
|
97
89
|
}
|
|
@@ -109,21 +101,15 @@ module.exports = function(RED) {
|
|
|
109
101
|
|
|
110
102
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
111
103
|
|
|
112
|
-
// --- DOWNLOAD
|
|
104
|
+
// --- DOWNLOAD MEDIA UTILIZZANDO SOLO AXIOS ---
|
|
113
105
|
async function downloadMedia(evento, channelID) {
|
|
114
|
-
const
|
|
115
|
-
if (!camera) return;
|
|
116
|
-
|
|
117
|
-
const nomeCamera = await getCameraName(camera);
|
|
118
|
-
|
|
106
|
+
const chStr = channelID.toString();
|
|
119
107
|
const nowTime = Date.now();
|
|
120
|
-
if(!lastTriggerTime[
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
|
|
124
|
-
lastTriggerTime[camera.ip] = nowTime;
|
|
108
|
+
if (!lastTriggerTime[chStr]) lastTriggerTime[chStr] = 0;
|
|
109
|
+
if (nowTime - lastTriggerTime[chStr] < 5000) return;
|
|
110
|
+
lastTriggerTime[chStr] = nowTime;
|
|
125
111
|
|
|
126
|
-
const
|
|
112
|
+
const nomeCamera = await getCameraName(channelID);
|
|
127
113
|
const referenceTime = new Date();
|
|
128
114
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
129
115
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
@@ -132,40 +118,38 @@ module.exports = function(RED) {
|
|
|
132
118
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
133
119
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
134
120
|
|
|
135
|
-
const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
|
|
136
|
-
|
|
137
|
-
// struttura del payload per Python
|
|
138
121
|
let output = {
|
|
139
|
-
tipo_messaggio: "evento",
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
ip_telecamera: camera.ip,
|
|
143
|
-
tipo_evento: evento,
|
|
144
|
-
timestamp_epoch: timestamp,
|
|
145
|
-
stato_telecamera: "ONLINE",
|
|
146
|
-
channel: channelID.toString(),
|
|
147
|
-
foto_base64: null,
|
|
148
|
-
video_base64: null
|
|
122
|
+
tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
|
|
123
|
+
ip_telecamera: null, tipo_evento: evento, timestamp_epoch: timestamp,
|
|
124
|
+
stato_telecamera: "ONLINE", channel: chStr, foto_base64: null, video_base64: null
|
|
149
125
|
};
|
|
150
126
|
|
|
151
|
-
|
|
152
127
|
let fileDaCancellare = [];
|
|
153
128
|
|
|
154
129
|
try {
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
const trackI = (channelID*100+3).toString();
|
|
130
|
+
const trackV = (channelID * 100 + 1).toString();
|
|
131
|
+
const trackI = (channelID * 100 + 3).toString();
|
|
158
132
|
const tracks = [{ id: trackV }, { id: trackI }];
|
|
133
|
+
|
|
159
134
|
for (let t of tracks) {
|
|
160
|
-
|
|
135
|
+
let searchXml = ``;
|
|
136
|
+
if (t.id === trackV) {
|
|
161
137
|
searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackList><trackID>${t.id}</trackID></trackList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>100</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
162
|
-
|
|
163
|
-
} else{
|
|
138
|
+
} else {
|
|
164
139
|
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><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${evento}</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
165
140
|
}
|
|
166
141
|
|
|
167
|
-
|
|
168
|
-
|
|
142
|
+
// Forza l'XML in un buffer per evitare che Axios sballi la lunghezza della stringa HTTP
|
|
143
|
+
const xmlBuffer = Buffer.from(searchXml, 'utf-8');
|
|
144
|
+
|
|
145
|
+
const resSearch = await nvrAuthAxios.request({
|
|
146
|
+
method: 'POST',
|
|
147
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
|
|
148
|
+
data: xmlBuffer,
|
|
149
|
+
headers: {
|
|
150
|
+
"Content-Type": "application/xml; charset=UTF-8",
|
|
151
|
+
"Content-Length": xmlBuffer.length
|
|
152
|
+
}
|
|
169
153
|
});
|
|
170
154
|
|
|
171
155
|
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
@@ -173,32 +157,25 @@ module.exports = function(RED) {
|
|
|
173
157
|
|
|
174
158
|
if (uriMatch) {
|
|
175
159
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
176
|
-
|
|
160
|
+
|
|
161
|
+
const resDown = await nvrAuthAxios.request({
|
|
177
162
|
method: 'GET',
|
|
178
|
-
url: `${
|
|
179
|
-
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
163
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(rawUri)}`,
|
|
180
164
|
responseType: 'arraybuffer'
|
|
181
165
|
});
|
|
182
166
|
|
|
183
167
|
let buffer = Buffer.from(resDown.data);
|
|
184
168
|
if (t.id === trackI) {
|
|
185
|
-
// SALVA FOTO IN LOCALE
|
|
186
169
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
187
170
|
fs.writeFileSync(fullImgPath, buffer);
|
|
188
|
-
|
|
189
|
-
// La convertiamo subito in testo per il payload
|
|
190
171
|
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
191
|
-
|
|
192
|
-
// Registriamo il file per la distruzione futura
|
|
193
172
|
fileDaCancellare.push(fullImgPath);
|
|
194
173
|
} else {
|
|
195
|
-
// SALVA VIDEO IN LOCALE-
|
|
196
174
|
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
197
175
|
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
198
176
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
199
177
|
fs.writeFileSync(rawPath, buffer);
|
|
200
178
|
|
|
201
|
-
// Eseguiamo ffmpeg localmente
|
|
202
179
|
await new Promise((resolve) => {
|
|
203
180
|
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
204
181
|
if (!err && fs.existsSync(fixedPath)) {
|
|
@@ -215,20 +192,11 @@ module.exports = function(RED) {
|
|
|
215
192
|
}
|
|
216
193
|
}
|
|
217
194
|
|
|
218
|
-
// Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
|
|
219
195
|
if (output.foto_base64 || output.video_base64) {
|
|
220
196
|
node.send({ payload: output });
|
|
221
|
-
|
|
222
|
-
// TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
|
|
223
197
|
setTimeout(() => {
|
|
224
198
|
for (let file of fileDaCancellare) {
|
|
225
|
-
try {
|
|
226
|
-
if (fs.existsSync(file)) {
|
|
227
|
-
fs.unlinkSync(file);
|
|
228
|
-
}
|
|
229
|
-
} catch (err) {
|
|
230
|
-
node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
|
|
231
|
-
}
|
|
199
|
+
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
232
200
|
}
|
|
233
201
|
}, 120000);
|
|
234
202
|
}
|
|
@@ -239,16 +207,20 @@ module.exports = function(RED) {
|
|
|
239
207
|
updateNodeStatus();
|
|
240
208
|
}
|
|
241
209
|
|
|
242
|
-
// --- ALERT STREAM ---
|
|
210
|
+
// --- ALERT STREAM CON AXIOS ---
|
|
243
211
|
function startAlertStream() {
|
|
244
212
|
if (isClosing) return;
|
|
245
|
-
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
246
213
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
247
|
-
|
|
214
|
+
|
|
215
|
+
nvrAuthAxios.request({ method: 'GET', url: url, responseType: 'stream' })
|
|
248
216
|
.then(response => {
|
|
249
217
|
streamRequest = response;
|
|
250
|
-
if (!nvrOnline) {
|
|
218
|
+
if (!nvrOnline) {
|
|
219
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } });
|
|
220
|
+
nvrOnline = true;
|
|
221
|
+
}
|
|
251
222
|
updateNodeStatus();
|
|
223
|
+
|
|
252
224
|
response.data.on('data', (chunk) => {
|
|
253
225
|
const data = chunk.toString().toLowerCase();
|
|
254
226
|
if (data.includes("active")) {
|
|
@@ -266,13 +238,17 @@ module.exports = function(RED) {
|
|
|
266
238
|
}
|
|
267
239
|
|
|
268
240
|
function handleNvrError() {
|
|
269
|
-
if (nvrOnline) {
|
|
241
|
+
if (nvrOnline) {
|
|
242
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } });
|
|
243
|
+
nvrOnline = false;
|
|
244
|
+
}
|
|
270
245
|
updateNodeStatus();
|
|
271
246
|
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
272
247
|
}
|
|
273
248
|
|
|
274
249
|
startAlertStream();
|
|
275
250
|
setTimeout(checkCameras, 2000);
|
|
251
|
+
|
|
276
252
|
node.on('close', (done) => {
|
|
277
253
|
isClosing = true;
|
|
278
254
|
clearInterval(heartbeatInterval);
|