node-red-contrib-hik-media-buffer 1.0.19 → 1.1.0
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 -42
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,33 +1,34 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
|
-
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
3
2
|
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
7
|
|
|
8
|
+
// --- STRATEGIA SWITCH: DEFINIZIONE MODALITÀ ---
|
|
9
|
+
// Valori possibili: 'digest', 'basic', 'bearer'
|
|
10
|
+
const AUTH_MODE = 'digest';
|
|
11
|
+
|
|
9
12
|
module.exports = function(RED) {
|
|
10
13
|
function HikMediaBufferNode(config) {
|
|
11
14
|
RED.nodes.createNode(this, config);
|
|
12
15
|
const node = this;
|
|
13
16
|
|
|
14
|
-
// --- ASSEGNAZIONE PROPRIETÀ
|
|
17
|
+
// --- ASSEGNAZIONE PROPRIETÀ ---
|
|
18
|
+
node.name = config.name;
|
|
15
19
|
node.host = config.host;
|
|
16
20
|
node.port = config.port || "80";
|
|
17
21
|
node.protocol = config.protocol || "http";
|
|
18
22
|
node.user = config.user;
|
|
19
23
|
node.pass = config.pass;
|
|
20
|
-
node.camPass = config.camPass || config.pass;
|
|
24
|
+
node.camPass = config.camPass || config.pass;
|
|
21
25
|
node.cameras = config.cameras || [];
|
|
22
|
-
// --------------------------------------
|
|
23
26
|
|
|
24
27
|
let streamRequest = null;
|
|
25
28
|
let isClosing = false;
|
|
26
29
|
let lastTriggerTime = 0;
|
|
27
|
-
|
|
28
|
-
// Stati di connessione
|
|
29
30
|
let nvrOnline = true;
|
|
30
|
-
let statoCamera = {};
|
|
31
|
+
let statoCamera = {};
|
|
31
32
|
|
|
32
33
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
33
34
|
const tempDir = os.tmpdir();
|
|
@@ -36,32 +37,72 @@ module.exports = function(RED) {
|
|
|
36
37
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
37
38
|
|
|
38
39
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
39
|
-
|
|
40
|
-
// ---
|
|
40
|
+
|
|
41
|
+
// --- PRENDE IL NOME DELLA TELECAMERA ---
|
|
42
|
+
async function getCameraName(cam) {
|
|
43
|
+
try {
|
|
44
|
+
const res = await hikRequest({
|
|
45
|
+
method: 'GET',
|
|
46
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}`,
|
|
47
|
+
user: node.user,
|
|
48
|
+
pass: node.camPass
|
|
49
|
+
});
|
|
50
|
+
const match = res.data.match(/<name>([^<]+)<\/name>/);
|
|
51
|
+
return match ? match[1] : `Cam_${cam.channel}`;
|
|
52
|
+
} catch (e) {
|
|
53
|
+
return `Camera_${cam.ip}`; // Fallback se la camera è già offline
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// --- HELPER UNICO PER LE CHIAMATE (STRATEGIA SWITCH) ---
|
|
58
|
+
async function hikRequest(options) {
|
|
59
|
+
const { method, url, data, responseType, user, pass, headers = {} } = options;
|
|
60
|
+
|
|
61
|
+
const requestConfig = {
|
|
62
|
+
method,
|
|
63
|
+
url,
|
|
64
|
+
data,
|
|
65
|
+
responseType: responseType || 'data',
|
|
66
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined,
|
|
67
|
+
timeout: 10000,
|
|
68
|
+
headers: { ...headers }
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// LOGICA DI AUTENTICAZIONE
|
|
72
|
+
if (AUTH_MODE === 'digest' || AUTH_MODE === 'basic') {
|
|
73
|
+
// Axios gestisce Basic/Digest internamente se configurato così:
|
|
74
|
+
requestConfig.auth = { username: user, password: pass };
|
|
75
|
+
} else if (AUTH_MODE === 'bearer') {
|
|
76
|
+
// Esempio per Token futuro
|
|
77
|
+
requestConfig.headers['Authorization'] = `Bearer ${node.context().get('myToken')}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return axios(requestConfig);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- CONTROLLO ONLINE TELECAMERE ---
|
|
41
84
|
async function checkCameras() {
|
|
42
85
|
if (isClosing) return;
|
|
43
86
|
for (let cam of node.cameras) {
|
|
44
|
-
const camAuth = new AxiosDigestAuth({
|
|
45
|
-
username: node.user,
|
|
46
|
-
password: node.camPass
|
|
47
|
-
});
|
|
48
87
|
try {
|
|
49
|
-
await
|
|
88
|
+
await hikRequest({
|
|
50
89
|
method: 'GET',
|
|
51
90
|
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
|
|
52
|
-
|
|
53
|
-
|
|
91
|
+
user: node.user,
|
|
92
|
+
pass: node.camPass
|
|
54
93
|
});
|
|
55
94
|
|
|
56
95
|
if (statoCamera[cam.ip] === false) {
|
|
57
|
-
|
|
96
|
+
const nomeOnline = await getCameraName(cam);
|
|
97
|
+
node.send({ payload: { status: "online", nomeCliente: node.name, nome_telecamera: nomeOnline, ip: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
|
|
58
98
|
statoCamera[cam.ip] = true;
|
|
59
99
|
} else if (statoCamera[cam.ip] === undefined) {
|
|
60
100
|
statoCamera[cam.ip] = true;
|
|
61
101
|
}
|
|
62
102
|
} catch (e) {
|
|
63
103
|
if (statoCamera[cam.ip] !== false) {
|
|
64
|
-
|
|
104
|
+
const nomeOffline = await getCameraName(cam);
|
|
105
|
+
node.send({ payload: { status: "offline", nomeCliente: node.name, nome_telecamera: nomeOffline, ip: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
|
|
65
106
|
statoCamera[cam.ip] = false;
|
|
66
107
|
}
|
|
67
108
|
}
|
|
@@ -82,19 +123,17 @@ module.exports = function(RED) {
|
|
|
82
123
|
|
|
83
124
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
84
125
|
|
|
85
|
-
// --- FUNZIONE DOWNLOAD ---
|
|
126
|
+
// --- FUNZIONE DOWNLOAD MEDIA ---
|
|
86
127
|
async function downloadMedia(evento, channelID) {
|
|
87
128
|
const camera = node.cameras.find(c => c.channel == channelID);
|
|
129
|
+
const nomeCamera = await getCameraName(camera);
|
|
130
|
+
|
|
88
131
|
if (!camera) return;
|
|
89
132
|
|
|
90
133
|
const nowTime = Date.now();
|
|
91
134
|
if (nowTime - lastTriggerTime < 10000) return;
|
|
92
135
|
lastTriggerTime = nowTime;
|
|
93
136
|
|
|
94
|
-
const camAuth = new AxiosDigestAuth({
|
|
95
|
-
username: node.user,
|
|
96
|
-
password: node.camPass
|
|
97
|
-
});
|
|
98
137
|
const referenceTime = new Date();
|
|
99
138
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
100
139
|
const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
|
|
@@ -103,7 +142,7 @@ module.exports = function(RED) {
|
|
|
103
142
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
104
143
|
|
|
105
144
|
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
106
|
-
let output = { ip: camera.ip, channel: channelID, event: evento, videoPath: null, imageBuffer: null };
|
|
145
|
+
let output = { ip: camera.ip, nomeCliente: node.name, nome_telecamera: nomeCamera, channel: channelID, event: evento, videoPath: null, imageBuffer: null };
|
|
107
146
|
|
|
108
147
|
try {
|
|
109
148
|
const tracks = [{ name: "termicoV", id: "201" }, { name: "termico", id: "203" }];
|
|
@@ -114,15 +153,16 @@ module.exports = function(RED) {
|
|
|
114
153
|
<trackIDList><trackID>${t.id}</trackID></trackIDList>
|
|
115
154
|
<timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList>
|
|
116
155
|
<maxResults>100</maxResults>
|
|
117
|
-
<searchResultPostion>0</searchResultPostion>
|
|
118
156
|
<metadataList><metadataDescriptor>//recordType.meta.std-cgi.com/${evento}</metadataDescriptor></metadataList>
|
|
119
157
|
</CMSearchDescription>`;
|
|
120
|
-
|
|
121
|
-
const resSearch = await
|
|
158
|
+
|
|
159
|
+
const resSearch = await hikRequest({
|
|
122
160
|
method: 'POST',
|
|
123
|
-
url: `${baseUrl}/search`,
|
|
124
|
-
data: searchXml,
|
|
125
|
-
headers: { "Content-Type": "application/xml" }
|
|
161
|
+
url: `${baseUrl}/search`,
|
|
162
|
+
data: searchXml,
|
|
163
|
+
headers: { "Content-Type": "application/xml" },
|
|
164
|
+
user: node.user,
|
|
165
|
+
pass: node.camPass
|
|
126
166
|
});
|
|
127
167
|
|
|
128
168
|
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
@@ -130,10 +170,13 @@ module.exports = function(RED) {
|
|
|
130
170
|
|
|
131
171
|
if (uriMatch) {
|
|
132
172
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
133
|
-
const resDown = await
|
|
134
|
-
method: 'GET',
|
|
135
|
-
|
|
136
|
-
|
|
173
|
+
const resDown = await hikRequest({
|
|
174
|
+
method: 'GET',
|
|
175
|
+
url: `${baseUrl}/download`,
|
|
176
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
177
|
+
responseType: 'arraybuffer',
|
|
178
|
+
user: node.user,
|
|
179
|
+
pass: node.camPass
|
|
137
180
|
});
|
|
138
181
|
|
|
139
182
|
let buffer = Buffer.from(resDown.data);
|
|
@@ -144,6 +187,7 @@ module.exports = function(RED) {
|
|
|
144
187
|
const rawPath = path.join(tempDir, `raw_${Date.now()}.mp4`);
|
|
145
188
|
const fixedPath = path.join(tempDir, `hik_v_${channelID}_${Date.now()}.mp4`);
|
|
146
189
|
fs.writeFileSync(rawPath, buffer);
|
|
190
|
+
|
|
147
191
|
await new Promise((resolve) => {
|
|
148
192
|
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
149
193
|
if (!err) {
|
|
@@ -167,15 +211,14 @@ module.exports = function(RED) {
|
|
|
167
211
|
// --- GESTIONE NVR ALERT STREAM ---
|
|
168
212
|
function startAlertStream() {
|
|
169
213
|
if (isClosing) return;
|
|
170
|
-
const nvrAuth = new AxiosDigestAuth({
|
|
171
|
-
username: node.user,
|
|
172
|
-
password: node.pass
|
|
173
|
-
});
|
|
174
214
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
175
215
|
|
|
176
|
-
|
|
177
|
-
method: 'GET',
|
|
178
|
-
|
|
216
|
+
hikRequest({
|
|
217
|
+
method: 'GET',
|
|
218
|
+
url: url,
|
|
219
|
+
responseType: 'stream',
|
|
220
|
+
user: node.user,
|
|
221
|
+
pass: node.pass
|
|
179
222
|
}).then(response => {
|
|
180
223
|
streamRequest = response;
|
|
181
224
|
if (!nvrOnline) {
|
|
@@ -216,7 +259,7 @@ module.exports = function(RED) {
|
|
|
216
259
|
node.on('close', (done) => {
|
|
217
260
|
isClosing = true;
|
|
218
261
|
clearInterval(heartbeatInterval);
|
|
219
|
-
if (streamRequest) streamRequest.data.destroy();
|
|
262
|
+
if (streamRequest && streamRequest.data) streamRequest.data.destroy();
|
|
220
263
|
done();
|
|
221
264
|
});
|
|
222
265
|
}
|