node-red-contrib-hik-media-buffer 1.0.19 → 1.1.1
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 +74 -51
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
const
|
|
2
|
-
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
1
|
+
const urllib = require('urllib');
|
|
3
2
|
const https = require('https');
|
|
4
3
|
const fs = require('fs');
|
|
5
4
|
const path = require('path');
|
|
@@ -11,57 +10,83 @@ module.exports = function(RED) {
|
|
|
11
10
|
RED.nodes.createNode(this, config);
|
|
12
11
|
const node = this;
|
|
13
12
|
|
|
14
|
-
|
|
13
|
+
node.name = config.name;
|
|
15
14
|
node.host = config.host;
|
|
16
15
|
node.port = config.port || "80";
|
|
17
16
|
node.protocol = config.protocol || "http";
|
|
18
17
|
node.user = config.user;
|
|
19
18
|
node.pass = config.pass;
|
|
20
|
-
node.camPass = config.camPass || config.pass;
|
|
19
|
+
node.camPass = config.camPass || config.pass;
|
|
21
20
|
node.cameras = config.cameras || [];
|
|
22
|
-
// --------------------------------------
|
|
23
21
|
|
|
24
22
|
let streamRequest = null;
|
|
25
23
|
let isClosing = false;
|
|
26
24
|
let lastTriggerTime = 0;
|
|
27
|
-
|
|
28
|
-
// Stati di connessione
|
|
29
25
|
let nvrOnline = true;
|
|
30
|
-
let statoCamera = {};
|
|
26
|
+
let statoCamera = {};
|
|
31
27
|
|
|
32
|
-
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
33
28
|
const tempDir = os.tmpdir();
|
|
34
29
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
35
30
|
|
|
36
31
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
37
32
|
|
|
38
33
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
39
|
-
|
|
40
|
-
// ---
|
|
34
|
+
|
|
35
|
+
// --- HELPER AGGIORNATO: Solo Urllib con Digest ---
|
|
36
|
+
async function hikRequest(options) {
|
|
37
|
+
const { method, url, data, responseType, user, pass, headers = {} } = options;
|
|
38
|
+
|
|
39
|
+
const res = await urllib.request(url, {
|
|
40
|
+
method: method,
|
|
41
|
+
digestAuth: `${user}:${pass}`,
|
|
42
|
+
content: data,
|
|
43
|
+
headers: headers,
|
|
44
|
+
dataType: responseType === 'arraybuffer' ? 'buffer' : 'text',
|
|
45
|
+
timeout: 15000,
|
|
46
|
+
rejectUnauthorized: false
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Urllib restituisce i dati in .data
|
|
50
|
+
return res;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function getCameraName(cam) {
|
|
54
|
+
try {
|
|
55
|
+
const res = await hikRequest({
|
|
56
|
+
method: 'GET',
|
|
57
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}`,
|
|
58
|
+
user: node.user,
|
|
59
|
+
pass: node.camPass
|
|
60
|
+
});
|
|
61
|
+
const match = res.data.toString().match(/<name>([^<]+)<\/name>/);
|
|
62
|
+
return match ? match[1] : `Cam_${cam.channel}`;
|
|
63
|
+
} catch (e) {
|
|
64
|
+
return `Camera_${cam.ip}`;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
async function checkCameras() {
|
|
42
69
|
if (isClosing) return;
|
|
43
70
|
for (let cam of node.cameras) {
|
|
44
|
-
const camAuth = new AxiosDigestAuth({
|
|
45
|
-
username: node.user,
|
|
46
|
-
password: node.camPass
|
|
47
|
-
});
|
|
48
71
|
try {
|
|
49
|
-
await
|
|
72
|
+
await hikRequest({
|
|
50
73
|
method: 'GET',
|
|
51
74
|
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
|
|
52
|
-
|
|
53
|
-
|
|
75
|
+
user: node.user,
|
|
76
|
+
pass: node.camPass
|
|
54
77
|
});
|
|
55
78
|
|
|
56
79
|
if (statoCamera[cam.ip] === false) {
|
|
57
|
-
|
|
80
|
+
const nomeOnline = await getCameraName(cam);
|
|
81
|
+
node.send({ payload: { status: "online", nomeCliente: node.name, nome_telecamera: nomeOnline, ip: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
|
|
58
82
|
statoCamera[cam.ip] = true;
|
|
59
83
|
} else if (statoCamera[cam.ip] === undefined) {
|
|
60
84
|
statoCamera[cam.ip] = true;
|
|
61
85
|
}
|
|
62
86
|
} catch (e) {
|
|
63
87
|
if (statoCamera[cam.ip] !== false) {
|
|
64
|
-
|
|
88
|
+
const nomeOffline = await getCameraName(cam);
|
|
89
|
+
node.send({ payload: { status: "offline", nomeCliente: node.name, nome_telecamera: nomeOffline, ip: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
|
|
65
90
|
statoCamera[cam.ip] = false;
|
|
66
91
|
}
|
|
67
92
|
}
|
|
@@ -82,7 +107,6 @@ module.exports = function(RED) {
|
|
|
82
107
|
|
|
83
108
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
84
109
|
|
|
85
|
-
// --- FUNZIONE DOWNLOAD ---
|
|
86
110
|
async function downloadMedia(evento, channelID) {
|
|
87
111
|
const camera = node.cameras.find(c => c.channel == channelID);
|
|
88
112
|
if (!camera) return;
|
|
@@ -91,10 +115,7 @@ module.exports = function(RED) {
|
|
|
91
115
|
if (nowTime - lastTriggerTime < 10000) return;
|
|
92
116
|
lastTriggerTime = nowTime;
|
|
93
117
|
|
|
94
|
-
const
|
|
95
|
-
username: node.user,
|
|
96
|
-
password: node.camPass
|
|
97
|
-
});
|
|
118
|
+
const nomeCamera = await getCameraName(camera);
|
|
98
119
|
const referenceTime = new Date();
|
|
99
120
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
100
121
|
const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
|
|
@@ -103,9 +124,10 @@ module.exports = function(RED) {
|
|
|
103
124
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
104
125
|
|
|
105
126
|
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
106
|
-
let output = { ip: camera.ip, channel: channelID, event: evento, videoPath: null, imageBuffer: null };
|
|
127
|
+
let output = { ip: camera.ip, nomeCliente: node.name, nome_telecamera: nomeCamera, channel: channelID, event: evento, videoPath: null, imageBuffer: null };
|
|
107
128
|
|
|
108
129
|
try {
|
|
130
|
+
// RIPRISTINATA LA TUA LOGICA DEI TRACKS
|
|
109
131
|
const tracks = [{ name: "termicoV", id: "201" }, { name: "termico", id: "203" }];
|
|
110
132
|
for (let t of tracks) {
|
|
111
133
|
const searchXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
@@ -114,26 +136,30 @@ module.exports = function(RED) {
|
|
|
114
136
|
<trackIDList><trackID>${t.id}</trackID></trackIDList>
|
|
115
137
|
<timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList>
|
|
116
138
|
<maxResults>100</maxResults>
|
|
117
|
-
<searchResultPostion>0</searchResultPostion>
|
|
118
139
|
<metadataList><metadataDescriptor>//recordType.meta.std-cgi.com/${evento}</metadataDescriptor></metadataList>
|
|
119
140
|
</CMSearchDescription>`;
|
|
120
|
-
|
|
121
|
-
const resSearch = await
|
|
141
|
+
|
|
142
|
+
const resSearch = await hikRequest({
|
|
122
143
|
method: 'POST',
|
|
123
|
-
url: `${baseUrl}/search`,
|
|
124
|
-
data: searchXml,
|
|
125
|
-
headers: { "Content-Type": "application/xml" }
|
|
144
|
+
url: `${baseUrl}/search`,
|
|
145
|
+
data: searchXml,
|
|
146
|
+
headers: { "Content-Type": "application/xml" },
|
|
147
|
+
user: node.user,
|
|
148
|
+
pass: node.camPass
|
|
126
149
|
});
|
|
127
150
|
|
|
128
|
-
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
151
|
+
let xml = resSearch.data.toString().replace(/<(\/?)\w+:/g, "<$1");
|
|
129
152
|
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
130
153
|
|
|
131
154
|
if (uriMatch) {
|
|
132
155
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
133
|
-
const resDown = await
|
|
134
|
-
method: 'GET',
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
const resDown = await hikRequest({
|
|
157
|
+
method: 'GET',
|
|
158
|
+
url: `${baseUrl}/download`,
|
|
159
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
160
|
+
responseType: 'arraybuffer',
|
|
161
|
+
user: node.user,
|
|
162
|
+
pass: node.camPass
|
|
137
163
|
});
|
|
138
164
|
|
|
139
165
|
let buffer = Buffer.from(resDown.data);
|
|
@@ -144,6 +170,7 @@ module.exports = function(RED) {
|
|
|
144
170
|
const rawPath = path.join(tempDir, `raw_${Date.now()}.mp4`);
|
|
145
171
|
const fixedPath = path.join(tempDir, `hik_v_${channelID}_${Date.now()}.mp4`);
|
|
146
172
|
fs.writeFileSync(rawPath, buffer);
|
|
173
|
+
|
|
147
174
|
await new Promise((resolve) => {
|
|
148
175
|
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
149
176
|
if (!err) {
|
|
@@ -164,27 +191,24 @@ module.exports = function(RED) {
|
|
|
164
191
|
updateNodeStatus();
|
|
165
192
|
}
|
|
166
193
|
|
|
167
|
-
// --- GESTIONE NVR ALERT STREAM ---
|
|
168
194
|
function startAlertStream() {
|
|
169
195
|
if (isClosing) return;
|
|
170
|
-
const nvrAuth = new AxiosDigestAuth({
|
|
171
|
-
username: node.user,
|
|
172
|
-
password: node.pass
|
|
173
|
-
});
|
|
174
196
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
175
197
|
|
|
176
|
-
|
|
177
|
-
method: 'GET',
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
198
|
+
urllib.request(url, {
|
|
199
|
+
method: 'GET',
|
|
200
|
+
digestAuth: `${node.user}:${node.pass}`,
|
|
201
|
+
streaming: true,
|
|
202
|
+
timeout: 60000,
|
|
203
|
+
rejectUnauthorized: false
|
|
204
|
+
}).then(res => {
|
|
181
205
|
if (!nvrOnline) {
|
|
182
206
|
node.send({ payload: { status: "online", ip: node.host, msg: "NVR Online" } });
|
|
183
207
|
nvrOnline = true;
|
|
184
208
|
}
|
|
185
209
|
updateNodeStatus();
|
|
186
210
|
|
|
187
|
-
|
|
211
|
+
res.res.on('data', (chunk) => {
|
|
188
212
|
const data = chunk.toString().toLowerCase();
|
|
189
213
|
if (data.includes("active")) {
|
|
190
214
|
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
@@ -196,8 +220,8 @@ module.exports = function(RED) {
|
|
|
196
220
|
}
|
|
197
221
|
});
|
|
198
222
|
|
|
199
|
-
|
|
200
|
-
|
|
223
|
+
res.res.on('error', () => handleNvrError());
|
|
224
|
+
res.res.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
|
|
201
225
|
}).catch(() => handleNvrError());
|
|
202
226
|
}
|
|
203
227
|
|
|
@@ -216,7 +240,6 @@ module.exports = function(RED) {
|
|
|
216
240
|
node.on('close', (done) => {
|
|
217
241
|
isClosing = true;
|
|
218
242
|
clearInterval(heartbeatInterval);
|
|
219
|
-
if (streamRequest) streamRequest.data.destroy();
|
|
220
243
|
done();
|
|
221
244
|
});
|
|
222
245
|
}
|