node-red-contrib-hik-media-buffer 1.1.71 → 1.1.72
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 +138 -137
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -5,6 +5,7 @@ const fs = require('fs');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const { exec } = require('child_process');
|
|
8
|
+
const { channel } = require('diagnostics_channel');
|
|
8
9
|
|
|
9
10
|
module.exports = function(RED) {
|
|
10
11
|
function HikMediaBufferNode(config) {
|
|
@@ -12,93 +13,91 @@ module.exports = function(RED) {
|
|
|
12
13
|
const node = this;
|
|
13
14
|
|
|
14
15
|
node.name = config.name || "TEST";
|
|
15
|
-
node.host = config.host;
|
|
16
|
+
node.host = config.host;
|
|
16
17
|
node.port = config.port || "80";
|
|
17
18
|
node.protocol = config.protocol || "http";
|
|
18
19
|
node.user = config.user;
|
|
19
20
|
node.pass = config.pass;
|
|
21
|
+
node.camPass = config.camPass || config.pass;
|
|
22
|
+
node.cameras = config.cameras || [];
|
|
20
23
|
|
|
21
24
|
let streamRequest = null;
|
|
22
25
|
let isClosing = false;
|
|
23
26
|
let lastTriggerTime = {};
|
|
24
27
|
let nvrOnline = true;
|
|
25
|
-
let
|
|
28
|
+
let statoCamera = {};
|
|
26
29
|
|
|
27
30
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
28
31
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
29
32
|
|
|
33
|
+
// CARTELLA TEMPORANEA
|
|
30
34
|
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
31
35
|
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
32
36
|
|
|
33
37
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
34
38
|
|
|
35
|
-
function toHikDate(d) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
40
|
+
|
|
41
|
+
// --- PRENDE IL NOME DELLA TELECAMERA ---
|
|
42
|
+
async function getCameraName(cam) {
|
|
43
|
+
const camAuth = new AxiosDigestAuth({
|
|
44
|
+
username: node.user,
|
|
45
|
+
password: node.camPass
|
|
46
|
+
});
|
|
39
47
|
|
|
40
|
-
async function getCameraName(channelID) {
|
|
41
|
-
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
42
48
|
try {
|
|
43
|
-
const res = await
|
|
49
|
+
const res = await camAuth.request({
|
|
44
50
|
method: 'GET',
|
|
45
|
-
url: `${node.protocol}://${
|
|
51
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
|
|
46
52
|
timeout: 5000,
|
|
47
53
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
48
54
|
});
|
|
55
|
+
|
|
49
56
|
const data = res.data.toString();
|
|
50
57
|
const match = data.match(/<name>([^<]+)<\/name>/i);
|
|
51
|
-
|
|
58
|
+
|
|
59
|
+
if (match && match[1]) {
|
|
60
|
+
return match[1].trim();
|
|
61
|
+
} else {
|
|
62
|
+
return `Canale_${cam.channel}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
52
65
|
} catch (e) {
|
|
53
|
-
return `
|
|
66
|
+
return `Camera_${cam.channel}`;
|
|
54
67
|
}
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
// --- CONTROLLO STATUS CAMERE ---
|
|
57
71
|
async function checkCameras() {
|
|
58
72
|
if (isClosing) return;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const statusStr = statusMatch[1].toLowerCase();
|
|
80
|
-
const isOnline = statusStr.includes("online") || statusStr.includes("recording");
|
|
81
|
-
|
|
82
|
-
if (isOnline && statoCanale[ch] === false) {
|
|
83
|
-
const nomeOnline = await getCameraName(ch);
|
|
84
|
-
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_nvr: node.host, channel: ch.toString(), msg: "Camera ripristinata su NVR" } });
|
|
85
|
-
statoCanale[ch] = true;
|
|
86
|
-
} else if (!isOnline && statoCanale[ch] !== false && statoCanale[ch] !== undefined) {
|
|
87
|
-
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Canale_${ch}`, ip_nvr: node.host, channel: ch.toString(), msg: "Camera scollegata o offline su NVR" } });
|
|
88
|
-
statoCanale[ch] = false;
|
|
89
|
-
} else if (statoCanale[ch] === undefined) {
|
|
90
|
-
statoCanale[ch] = isOnline;
|
|
91
|
-
}
|
|
73
|
+
for (let cam of node.cameras) {
|
|
74
|
+
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
|
|
75
|
+
try {
|
|
76
|
+
await camAuth.request({
|
|
77
|
+
method: 'GET',
|
|
78
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
|
|
79
|
+
timeout: 5000,
|
|
80
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
81
|
+
});
|
|
82
|
+
if (statoCamera[cam.ip] === false) {
|
|
83
|
+
const nomeOnline = await getCameraName(cam);
|
|
84
|
+
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" } });
|
|
85
|
+
statoCamera[cam.ip] = true;
|
|
86
|
+
} else if (statoCamera[cam.ip] === undefined) {
|
|
87
|
+
statoCamera[cam.ip] = true;
|
|
88
|
+
}
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (statoCamera[cam.ip] !== false) {
|
|
91
|
+
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${cam.channel}`, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
|
|
92
|
+
statoCamera[cam.ip] = false;
|
|
92
93
|
}
|
|
93
94
|
}
|
|
94
|
-
} catch (e) {
|
|
95
|
-
nvrOnline = false;
|
|
96
95
|
}
|
|
97
96
|
updateNodeStatus();
|
|
98
97
|
}
|
|
99
98
|
|
|
100
99
|
function updateNodeStatus() {
|
|
101
|
-
const offlineCams = Object.values(
|
|
100
|
+
const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
|
|
102
101
|
if (!nvrOnline) {
|
|
103
102
|
node.status({fill:"red", shape:"ring", text:"NVR Offline"});
|
|
104
103
|
} else if (offlineCams > 0) {
|
|
@@ -110,35 +109,38 @@ module.exports = function(RED) {
|
|
|
110
109
|
|
|
111
110
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
112
111
|
|
|
112
|
+
// --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
|
|
113
113
|
async function downloadMedia(evento, channelID) {
|
|
114
|
-
node.
|
|
114
|
+
const camera = node.cameras.find(c => c.channel == channelID);
|
|
115
|
+
if (!camera) return;
|
|
116
|
+
|
|
117
|
+
const nomeCamera = await getCameraName(camera);
|
|
115
118
|
|
|
116
119
|
const nowTime = Date.now();
|
|
117
|
-
if
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
if(!lastTriggerTime[camera.ip]){
|
|
121
|
+
lastTriggerTime[camera.ip] = 0;
|
|
122
|
+
}
|
|
123
|
+
if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
|
|
124
|
+
lastTriggerTime[camera.ip] = nowTime;
|
|
120
125
|
|
|
121
|
-
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.
|
|
126
|
+
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
|
|
122
127
|
const referenceTime = new Date();
|
|
123
128
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
const endTime = toHikDate(new Date(referenceTime.getTime() + (5 * 1000)));
|
|
129
|
+
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
130
|
+
const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
|
|
127
131
|
|
|
128
132
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
129
|
-
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
130
134
|
|
|
131
|
-
|
|
135
|
+
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
132
136
|
|
|
133
|
-
|
|
134
|
-
let evMinuscolo = evento.toLowerCase();
|
|
135
|
-
|
|
137
|
+
// struttura del payload per Python
|
|
136
138
|
let output = {
|
|
137
139
|
tipo_messaggio: "evento",
|
|
138
140
|
nome_cliente: node.name,
|
|
139
141
|
nome_telecamera: nomeCamera,
|
|
140
|
-
ip_telecamera:
|
|
141
|
-
tipo_evento:
|
|
142
|
+
ip_telecamera: camera.ip,
|
|
143
|
+
tipo_evento: evento,
|
|
142
144
|
timestamp_epoch: timestamp,
|
|
143
145
|
stato_telecamera: "ONLINE",
|
|
144
146
|
channel: channelID.toString(),
|
|
@@ -146,108 +148,108 @@ module.exports = function(RED) {
|
|
|
146
148
|
video_base64: null
|
|
147
149
|
};
|
|
148
150
|
|
|
149
|
-
const parsedChannel = parseInt(channelID, 10);
|
|
150
151
|
|
|
151
|
-
// Query JSON Pulita e Blindata (Ancorata al Canale Reale dell'NVR)
|
|
152
|
-
const searchJsonPayload = {
|
|
153
|
-
"EventSearchDescription": {
|
|
154
|
-
"searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
|
|
155
|
-
"searchResultPosition": 0,
|
|
156
|
-
"maxResults": 10,
|
|
157
|
-
"timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
|
|
158
|
-
"type": "all",
|
|
159
|
-
"channels": [parsedChannel],
|
|
160
|
-
"eventType": "behavior",
|
|
161
|
-
"behavior": { "behaviorEventType": "all" }
|
|
162
|
-
}
|
|
163
|
-
};
|
|
164
|
-
|
|
165
152
|
let fileDaCancellare = [];
|
|
166
153
|
|
|
167
154
|
try {
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
});
|
|
155
|
+
const trackV = (channelID*100+1).toString();
|
|
156
|
+
const trackI = (channelID*100+3).toString();
|
|
157
|
+
const tracks = [{ id: trackV }, { id: trackI }];
|
|
158
|
+
for (let t of tracks) {
|
|
159
|
+
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>`;
|
|
174
160
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (videoUriMatch) {
|
|
179
|
-
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
180
|
-
|
|
181
|
-
const resDownVideo = await camAuth.request({
|
|
182
|
-
method: 'GET',
|
|
183
|
-
url: `${baseUrl}/download`,
|
|
184
|
-
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawVideoUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
185
|
-
responseType: 'arraybuffer'
|
|
161
|
+
const resSearch = await camAuth.request({
|
|
162
|
+
method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
|
|
186
163
|
});
|
|
187
|
-
|
|
188
|
-
let videoBuffer = Buffer.from(resDownVideo.data);
|
|
189
|
-
if (videoBuffer.slice(0, 4).toString() === 'IMKH') videoBuffer = videoBuffer.slice(40);
|
|
190
164
|
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
165
|
+
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
166
|
+
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
167
|
+
|
|
168
|
+
if (uriMatch) {
|
|
169
|
+
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
170
|
+
const resDown = await camAuth.request({
|
|
171
|
+
method: 'GET',
|
|
172
|
+
url: `${baseUrl}/download`,
|
|
173
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
174
|
+
responseType: 'arraybuffer'
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
let buffer = Buffer.from(resDown.data);
|
|
178
|
+
if (t.id === "203") {
|
|
179
|
+
// SALVA FOTO IN LOCALE
|
|
180
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
181
|
+
fs.writeFileSync(fullImgPath, buffer);
|
|
182
|
+
|
|
183
|
+
// La convertiamo subito in testo per il payload
|
|
184
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
185
|
+
|
|
186
|
+
// Registriamo il file per la distruzione futura
|
|
187
|
+
fileDaCancellare.push(fullImgPath);
|
|
188
|
+
} else {
|
|
189
|
+
// SALVA VIDEO IN LOCALE-
|
|
190
|
+
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
191
|
+
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
192
|
+
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
193
|
+
fs.writeFileSync(rawPath, buffer);
|
|
194
|
+
|
|
195
|
+
// Eseguiamo ffmpeg localmente
|
|
196
|
+
await new Promise((resolve) => {
|
|
197
|
+
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
198
|
+
if (!err && fs.existsSync(fixedPath)) {
|
|
199
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
200
|
+
fileDaCancellare.push(fixedPath);
|
|
201
|
+
} else {
|
|
202
|
+
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
203
|
+
}
|
|
204
|
+
fileDaCancellare.push(rawPath);
|
|
205
|
+
resolve();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
|
|
213
|
+
if (output.foto_base64 || output.video_base64) {
|
|
214
|
+
node.send({ payload: output });
|
|
197
215
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
}
|
|
205
|
-
if (fs.existsSync(fixedPath)) {
|
|
206
|
-
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
207
|
-
fileDaCancellare.push(fixedPath);
|
|
216
|
+
// TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
|
|
217
|
+
setTimeout(() => {
|
|
218
|
+
for (let file of fileDaCancellare) {
|
|
219
|
+
try {
|
|
220
|
+
if (fs.existsSync(file)) {
|
|
221
|
+
fs.unlinkSync(file);
|
|
208
222
|
}
|
|
209
|
-
}
|
|
210
|
-
|
|
223
|
+
} catch (err) {
|
|
224
|
+
node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
|
|
211
225
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
});
|
|
226
|
+
}
|
|
227
|
+
}, 120000);
|
|
215
228
|
}
|
|
216
|
-
} catch (videoErr) {
|
|
217
|
-
node.error(`[ERROR] Errore esecuzione: ${videoErr.message}`);
|
|
218
|
-
}
|
|
219
229
|
|
|
220
|
-
|
|
221
|
-
node.
|
|
222
|
-
setTimeout(() => {
|
|
223
|
-
for (let file of fileDaCancellare) {
|
|
224
|
-
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
225
|
-
}
|
|
226
|
-
}, 60000);
|
|
230
|
+
} catch (e) {
|
|
231
|
+
node.error(`Errore Download Cam ${channelID}: ${e.message}`);
|
|
227
232
|
}
|
|
228
233
|
updateNodeStatus();
|
|
229
234
|
}
|
|
230
235
|
|
|
236
|
+
// --- ALERT STREAM ---
|
|
231
237
|
function startAlertStream() {
|
|
232
238
|
if (isClosing) return;
|
|
233
239
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
234
240
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
235
|
-
|
|
236
241
|
nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
|
|
237
242
|
.then(response => {
|
|
238
243
|
streamRequest = response;
|
|
239
|
-
if (!nvrOnline) { nvrOnline = true; }
|
|
244
|
+
if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
|
|
240
245
|
updateNodeStatus();
|
|
241
|
-
|
|
242
246
|
response.data.on('data', (chunk) => {
|
|
243
|
-
if (isClosing) return;
|
|
244
247
|
const data = chunk.toString().toLowerCase();
|
|
245
248
|
if (data.includes("active")) {
|
|
246
249
|
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
247
250
|
if (chMatch) {
|
|
248
251
|
let ev = "Unknown";
|
|
249
252
|
for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
|
|
250
|
-
if (ev === "Unknown") return;
|
|
251
253
|
downloadMedia(ev, chMatch[1]);
|
|
252
254
|
}
|
|
253
255
|
}
|
|
@@ -258,18 +260,17 @@ module.exports = function(RED) {
|
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
function handleNvrError() {
|
|
261
|
-
nvrOnline = false;
|
|
263
|
+
if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
|
|
262
264
|
updateNodeStatus();
|
|
263
265
|
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
264
266
|
}
|
|
265
267
|
|
|
266
268
|
startAlertStream();
|
|
267
269
|
setTimeout(checkCameras, 2000);
|
|
268
|
-
|
|
269
270
|
node.on('close', (done) => {
|
|
270
271
|
isClosing = true;
|
|
271
272
|
clearInterval(heartbeatInterval);
|
|
272
|
-
if (streamRequest
|
|
273
|
+
if (streamRequest) streamRequest.data.destroy();
|
|
273
274
|
done();
|
|
274
275
|
});
|
|
275
276
|
}
|