node-red-contrib-hik-media-buffer 1.1.6 → 1.1.7
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 +42 -48
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
|
-
const
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
3
|
+
const https = require('https');
|
|
2
4
|
const fs = require('fs');
|
|
3
5
|
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
4
7
|
const { exec } = require('child_process');
|
|
5
8
|
|
|
6
9
|
module.exports = function(RED) {
|
|
@@ -8,7 +11,7 @@ module.exports = function(RED) {
|
|
|
8
11
|
RED.nodes.createNode(this, config);
|
|
9
12
|
const node = this;
|
|
10
13
|
|
|
11
|
-
node.name = config.name || "TEST";
|
|
14
|
+
node.name = config.name || "TEST"; // Nome cliente usato per le cartelle
|
|
12
15
|
node.host = config.host;
|
|
13
16
|
node.port = config.port || "80";
|
|
14
17
|
node.protocol = config.protocol || "http";
|
|
@@ -17,61 +20,48 @@ module.exports = function(RED) {
|
|
|
17
20
|
node.camPass = config.camPass || config.pass;
|
|
18
21
|
node.cameras = config.cameras || [];
|
|
19
22
|
|
|
23
|
+
let streamRequest = null;
|
|
20
24
|
let isClosing = false;
|
|
21
25
|
let lastTriggerTime = 0;
|
|
22
26
|
let nvrOnline = true;
|
|
23
27
|
let statoCamera = {};
|
|
24
28
|
|
|
29
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
25
30
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
26
31
|
|
|
27
|
-
// ---
|
|
32
|
+
// --- CONFIGURAZIONE PERCORSI STORAGE ---
|
|
28
33
|
const baseStorage = `C:\\Users\\APerucca\\Documents\\progetto-docker\\storage\\${node.name}`;
|
|
29
34
|
const imgDir = path.join(baseStorage, "allarmi");
|
|
30
35
|
const vidDir = path.join(baseStorage, "video");
|
|
31
36
|
|
|
37
|
+
// Crea le cartelle se non esistono
|
|
32
38
|
if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
|
|
33
39
|
if (!fs.existsSync(vidDir)) fs.mkdirSync(vidDir, { recursive: true });
|
|
34
40
|
|
|
35
|
-
// Client Digest per NVR e Camere
|
|
36
|
-
const nvrAuth = axiosDigest(node.user, node.pass);
|
|
37
|
-
|
|
38
41
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
39
42
|
|
|
40
43
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
41
44
|
|
|
42
|
-
async function getCameraName(cam) {
|
|
43
|
-
try {
|
|
44
|
-
const res = await nvrAuth.request({
|
|
45
|
-
method: 'GET',
|
|
46
|
-
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}`
|
|
47
|
-
});
|
|
48
|
-
const match = res.data.toString().match(/<name>([^<]+)<\/name>/);
|
|
49
|
-
return match ? match[1] : `Cam_${cam.channel}`;
|
|
50
|
-
} catch (e) {
|
|
51
|
-
return `Camera_${cam.ip}`;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
45
|
async function checkCameras() {
|
|
56
46
|
if (isClosing) return;
|
|
57
47
|
for (let cam of node.cameras) {
|
|
48
|
+
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
|
|
58
49
|
try {
|
|
59
|
-
await
|
|
50
|
+
await camAuth.request({
|
|
60
51
|
method: 'GET',
|
|
61
|
-
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo
|
|
52
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
|
|
53
|
+
timeout: 5000,
|
|
54
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
62
55
|
});
|
|
63
|
-
|
|
64
56
|
if (statoCamera[cam.ip] === false) {
|
|
65
|
-
|
|
66
|
-
node.send({ payload: { status: "online", nomeCliente: node.name, nome_telecamera: nomeOnline, ip: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
|
|
57
|
+
node.send({ payload: { status: "online", nomeCliente: node.name, ip: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
|
|
67
58
|
statoCamera[cam.ip] = true;
|
|
68
59
|
} else if (statoCamera[cam.ip] === undefined) {
|
|
69
60
|
statoCamera[cam.ip] = true;
|
|
70
61
|
}
|
|
71
62
|
} catch (e) {
|
|
72
63
|
if (statoCamera[cam.ip] !== false) {
|
|
73
|
-
|
|
74
|
-
node.send({ payload: { status: "offline", nomeCliente: node.name, nome_telecamera: nomeOffline, ip: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
|
|
64
|
+
node.send({ payload: { status: "offline", nomeCliente: node.name, ip: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
|
|
75
65
|
statoCamera[cam.ip] = false;
|
|
76
66
|
}
|
|
77
67
|
}
|
|
@@ -100,51 +90,50 @@ module.exports = function(RED) {
|
|
|
100
90
|
if (nowTime - lastTriggerTime < 10000) return;
|
|
101
91
|
lastTriggerTime = nowTime;
|
|
102
92
|
|
|
103
|
-
const
|
|
104
|
-
const nomeCamera = await getCameraName(camera);
|
|
93
|
+
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
|
|
105
94
|
const referenceTime = new Date();
|
|
95
|
+
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
106
96
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
107
97
|
const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
|
|
108
98
|
|
|
109
|
-
const camAuth = axiosDigest(node.user, node.camPass);
|
|
110
|
-
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
111
|
-
|
|
112
99
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
113
100
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
114
101
|
|
|
115
|
-
|
|
102
|
+
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
103
|
+
let output = { ip: camera.ip, nomeCliente: node.name, channel: channelID, event: evento, videoPath: null, imageBuffer: null, imagePath: null };
|
|
116
104
|
|
|
117
105
|
try {
|
|
118
106
|
const tracks = [{ id: "201" }, { id: "203" }];
|
|
119
107
|
for (let t of tracks) {
|
|
120
108
|
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>`;
|
|
121
|
-
|
|
122
|
-
const resSearch = await camAuth.request({
|
|
123
|
-
method: 'POST', url: `${baseUrl}/search`, data: searchXml,
|
|
124
|
-
headers: { "Content-Type": "application/xml" }
|
|
109
|
+
|
|
110
|
+
const resSearch = await camAuth.request({
|
|
111
|
+
method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
|
|
125
112
|
});
|
|
126
113
|
|
|
127
|
-
let xml = resSearch.data.
|
|
114
|
+
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
128
115
|
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
129
116
|
|
|
130
117
|
if (uriMatch) {
|
|
131
118
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
132
|
-
const resDown = await camAuth.request({
|
|
133
|
-
method: '
|
|
134
|
-
url: `${baseUrl}/download`,
|
|
135
|
-
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
119
|
+
const resDown = await camAuth.request({
|
|
120
|
+
method: 'GET',
|
|
121
|
+
url: `${baseUrl}/download`,
|
|
122
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
136
123
|
responseType: 'arraybuffer',
|
|
137
124
|
headers: { "Content-Type": "application/xml" }
|
|
138
125
|
});
|
|
139
126
|
|
|
140
127
|
let buffer = Buffer.from(resDown.data);
|
|
128
|
+
|
|
141
129
|
if (t.id === "203") {
|
|
142
|
-
// FOTO
|
|
130
|
+
// --- SALVATAGGIO FOTO ---
|
|
143
131
|
const fullImgPath = path.join(imgDir, `img_${timestamp}.jpg`);
|
|
144
132
|
fs.writeFileSync(fullImgPath, buffer);
|
|
133
|
+
output.imageBuffer = buffer; // Mantengo il buffer per compatibilità
|
|
145
134
|
output.imagePath = fullImgPath;
|
|
146
135
|
} else {
|
|
147
|
-
// VIDEO
|
|
136
|
+
// --- SALVATAGGIO VIDEO ---
|
|
148
137
|
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
149
138
|
const rawPath = path.join(vidDir, `raw_${timestamp}.mp4`);
|
|
150
139
|
const fixedPath = path.join(vidDir, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
@@ -172,17 +161,21 @@ module.exports = function(RED) {
|
|
|
172
161
|
|
|
173
162
|
function startAlertStream() {
|
|
174
163
|
if (isClosing) return;
|
|
164
|
+
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
175
165
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
176
166
|
|
|
177
|
-
nvrAuth.request({
|
|
178
|
-
|
|
167
|
+
nvrAuth.request({
|
|
168
|
+
method: 'GET', url: url, responseType: 'stream',
|
|
169
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
170
|
+
}).then(response => {
|
|
171
|
+
streamRequest = response;
|
|
179
172
|
if (!nvrOnline) {
|
|
180
173
|
node.send({ payload: { status: "online", ip: node.host, msg: "NVR Online" } });
|
|
181
174
|
nvrOnline = true;
|
|
182
175
|
}
|
|
183
176
|
updateNodeStatus();
|
|
184
177
|
|
|
185
|
-
|
|
178
|
+
response.data.on('data', (chunk) => {
|
|
186
179
|
const data = chunk.toString().toLowerCase();
|
|
187
180
|
if (data.includes("active")) {
|
|
188
181
|
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
@@ -194,8 +187,8 @@ module.exports = function(RED) {
|
|
|
194
187
|
}
|
|
195
188
|
});
|
|
196
189
|
|
|
197
|
-
|
|
198
|
-
|
|
190
|
+
response.data.on('error', () => handleNvrError());
|
|
191
|
+
response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
|
|
199
192
|
}).catch(() => handleNvrError());
|
|
200
193
|
}
|
|
201
194
|
|
|
@@ -214,6 +207,7 @@ module.exports = function(RED) {
|
|
|
214
207
|
node.on('close', (done) => {
|
|
215
208
|
isClosing = true;
|
|
216
209
|
clearInterval(heartbeatInterval);
|
|
210
|
+
if (streamRequest) streamRequest.data.destroy();
|
|
217
211
|
done();
|
|
218
212
|
});
|
|
219
213
|
}
|