node-red-contrib-hik-media-buffer 1.1.4 → 1.1.6
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 +37 -83
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
const
|
|
2
|
-
const https = require('https');
|
|
1
|
+
const axiosDigest = require('axios-digest');
|
|
3
2
|
const fs = require('fs');
|
|
4
3
|
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
4
|
const { exec } = require('child_process');
|
|
7
5
|
|
|
8
6
|
module.exports = function(RED) {
|
|
@@ -19,18 +17,6 @@ module.exports = function(RED) {
|
|
|
19
17
|
node.camPass = config.camPass || config.pass;
|
|
20
18
|
node.cameras = config.cameras || [];
|
|
21
19
|
|
|
22
|
-
// --- DEFINIZIONE PERCORSI PROGETTO ---
|
|
23
|
-
// Puntiamo direttamente alla cartella del tuo progetto Docker
|
|
24
|
-
const baseStorage = `C:\\Users\\APerucca\\Documents\\progetto-docker\\storage\\${node.name}`;
|
|
25
|
-
const imgDir = path.join(baseStorage, "allarmi");
|
|
26
|
-
const vidDir = path.join(baseStorage, "video");
|
|
27
|
-
|
|
28
|
-
// Creazione cartelle se non esistono
|
|
29
|
-
[imgDir, vidDir].forEach(dir => {
|
|
30
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
let streamRequest = null;
|
|
34
20
|
let isClosing = false;
|
|
35
21
|
let lastTriggerTime = 0;
|
|
36
22
|
let nvrOnline = true;
|
|
@@ -38,40 +24,26 @@ module.exports = function(RED) {
|
|
|
38
24
|
|
|
39
25
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
40
26
|
|
|
41
|
-
|
|
27
|
+
// --- DEFINIZIONE PERCORSI STORAGE ---
|
|
28
|
+
const baseStorage = `C:\\Users\\APerucca\\Documents\\progetto-docker\\storage\\${node.name}`;
|
|
29
|
+
const imgDir = path.join(baseStorage, "allarmi");
|
|
30
|
+
const vidDir = path.join(baseStorage, "video");
|
|
42
31
|
|
|
43
|
-
|
|
32
|
+
if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
|
|
33
|
+
if (!fs.existsSync(vidDir)) fs.mkdirSync(vidDir, { recursive: true });
|
|
44
34
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// Assicuriamoci che data sia una stringa se stiamo mandando XML
|
|
49
|
-
const body = (typeof data === 'object') ? JSON.stringify(data) : data;
|
|
35
|
+
// Client Digest per NVR e Camere
|
|
36
|
+
const nvrAuth = axiosDigest(node.user, node.pass);
|
|
50
37
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
content: body,
|
|
55
|
-
headers: {
|
|
56
|
-
'Content-Type': 'application/xml', // Questo dice alla telecamera: "Leggi l'XML"
|
|
57
|
-
'Accept': '*/*',
|
|
58
|
-
...headers
|
|
59
|
-
},
|
|
60
|
-
dataType: responseType === 'arraybuffer' ? 'buffer' : 'text',
|
|
61
|
-
timeout: 15000,
|
|
62
|
-
rejectUnauthorized: false
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
return res;
|
|
66
|
-
}
|
|
38
|
+
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
39
|
+
|
|
40
|
+
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
67
41
|
|
|
68
42
|
async function getCameraName(cam) {
|
|
69
43
|
try {
|
|
70
|
-
const res = await
|
|
44
|
+
const res = await nvrAuth.request({
|
|
71
45
|
method: 'GET',
|
|
72
|
-
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}
|
|
73
|
-
user: node.user,
|
|
74
|
-
pass: node.camPass
|
|
46
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}`
|
|
75
47
|
});
|
|
76
48
|
const match = res.data.toString().match(/<name>([^<]+)<\/name>/);
|
|
77
49
|
return match ? match[1] : `Cam_${cam.channel}`;
|
|
@@ -84,11 +56,9 @@ module.exports = function(RED) {
|
|
|
84
56
|
if (isClosing) return;
|
|
85
57
|
for (let cam of node.cameras) {
|
|
86
58
|
try {
|
|
87
|
-
await
|
|
59
|
+
await nvrAuth.request({
|
|
88
60
|
method: 'GET',
|
|
89
|
-
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo
|
|
90
|
-
user: node.user,
|
|
91
|
-
pass: node.camPass
|
|
61
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`
|
|
92
62
|
});
|
|
93
63
|
|
|
94
64
|
if (statoCamera[cam.ip] === false) {
|
|
@@ -130,26 +100,28 @@ module.exports = function(RED) {
|
|
|
130
100
|
if (nowTime - lastTriggerTime < 10000) return;
|
|
131
101
|
lastTriggerTime = nowTime;
|
|
132
102
|
|
|
103
|
+
const timestamp = Math.floor(nowTime / 1000);
|
|
133
104
|
const nomeCamera = await getCameraName(camera);
|
|
134
105
|
const referenceTime = new Date();
|
|
135
|
-
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
136
106
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
137
107
|
const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
|
|
138
108
|
|
|
109
|
+
const camAuth = axiosDigest(node.user, node.camPass);
|
|
110
|
+
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
111
|
+
|
|
139
112
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
140
113
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
141
114
|
|
|
142
|
-
|
|
143
|
-
let output = { ip: camera.ip, nomeCliente: node.name, nome_telecamera: nomeCamera, channel: channelID, event: evento, videoPath: null, imageBuffer: null, imagePath: null };
|
|
115
|
+
let output = { ip: camera.ip, nomeCliente: node.name, nome_telecamera: nomeCamera, channel: channelID, event: evento, videoPath: null, imagePath: null };
|
|
144
116
|
|
|
145
117
|
try {
|
|
146
|
-
const tracks = [{
|
|
118
|
+
const tracks = [{ id: "201" }, { id: "203" }];
|
|
147
119
|
for (let t of tracks) {
|
|
148
120
|
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>`;
|
|
149
121
|
|
|
150
|
-
const resSearch = await
|
|
122
|
+
const resSearch = await camAuth.request({
|
|
151
123
|
method: 'POST', url: `${baseUrl}/search`, data: searchXml,
|
|
152
|
-
headers: { "Content-Type": "application/xml" }
|
|
124
|
+
headers: { "Content-Type": "application/xml" }
|
|
153
125
|
});
|
|
154
126
|
|
|
155
127
|
let xml = resSearch.data.toString().replace(/<(\/?)\w+:/g, "<$1");
|
|
@@ -157,34 +129,25 @@ module.exports = function(RED) {
|
|
|
157
129
|
|
|
158
130
|
if (uriMatch) {
|
|
159
131
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
160
|
-
const resDown = await
|
|
161
|
-
method: '
|
|
132
|
+
const resDown = await camAuth.request({
|
|
133
|
+
method: 'POST', // POST risolve l'errore "Invalid XML"
|
|
162
134
|
url: `${baseUrl}/download`,
|
|
163
135
|
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
164
136
|
responseType: 'arraybuffer',
|
|
165
|
-
|
|
166
|
-
pass: node.camPass
|
|
137
|
+
headers: { "Content-Type": "application/xml" }
|
|
167
138
|
});
|
|
168
139
|
|
|
169
140
|
let buffer = Buffer.from(resDown.data);
|
|
170
|
-
|
|
171
141
|
if (t.id === "203") {
|
|
172
|
-
//
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
// AGGIUNTA: Salviamo fisicamente il file per il sito
|
|
176
|
-
const imgFileName = `img_${timestamp}.jpg`;
|
|
177
|
-
const fullImgPath = path.join(imgDir, imgFileName);
|
|
178
|
-
fs.writeFileSync(fullImgPath, buffer);
|
|
142
|
+
// FOTO
|
|
143
|
+
const fullImgPath = path.join(imgDir, `img_${timestamp}.jpg`);
|
|
144
|
+
fs.writeFileSync(fullImgPath, buffer);
|
|
179
145
|
output.imagePath = fullImgPath;
|
|
180
|
-
|
|
181
146
|
} else {
|
|
182
|
-
//
|
|
147
|
+
// VIDEO
|
|
183
148
|
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
184
|
-
|
|
185
149
|
const rawPath = path.join(vidDir, `raw_${timestamp}.mp4`);
|
|
186
150
|
const fixedPath = path.join(vidDir, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
187
|
-
|
|
188
151
|
fs.writeFileSync(rawPath, buffer);
|
|
189
152
|
|
|
190
153
|
await new Promise((resolve) => {
|
|
@@ -192,13 +155,10 @@ module.exports = function(RED) {
|
|
|
192
155
|
if (!err) {
|
|
193
156
|
output.videoPath = fixedPath;
|
|
194
157
|
try { fs.unlinkSync(rawPath); } catch(e) {}
|
|
195
|
-
} else {
|
|
196
|
-
output.videoPath = rawPath;
|
|
197
|
-
}
|
|
158
|
+
} else { output.videoPath = rawPath; }
|
|
198
159
|
resolve();
|
|
199
160
|
});
|
|
200
161
|
});
|
|
201
|
-
|
|
202
162
|
setTimeout(() => { if (output.videoPath && fs.existsSync(output.videoPath)) fs.unlinkSync(output.videoPath); }, 180000);
|
|
203
163
|
}
|
|
204
164
|
}
|
|
@@ -210,25 +170,19 @@ module.exports = function(RED) {
|
|
|
210
170
|
updateNodeStatus();
|
|
211
171
|
}
|
|
212
172
|
|
|
213
|
-
// --- GESTIONE NVR ALERT STREAM ---
|
|
214
173
|
function startAlertStream() {
|
|
215
174
|
if (isClosing) return;
|
|
216
175
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
217
176
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
digestAuth: `${node.user}:${node.pass}`,
|
|
221
|
-
streaming: true,
|
|
222
|
-
timeout: 60000,
|
|
223
|
-
rejectUnauthorized: false
|
|
224
|
-
}).then(res => {
|
|
177
|
+
nvrAuth.request({ method: 'GET', url: url, responseType: 'stream' })
|
|
178
|
+
.then(res => {
|
|
225
179
|
if (!nvrOnline) {
|
|
226
180
|
node.send({ payload: { status: "online", ip: node.host, msg: "NVR Online" } });
|
|
227
181
|
nvrOnline = true;
|
|
228
182
|
}
|
|
229
183
|
updateNodeStatus();
|
|
230
184
|
|
|
231
|
-
res.
|
|
185
|
+
res.data.on('data', (chunk) => {
|
|
232
186
|
const data = chunk.toString().toLowerCase();
|
|
233
187
|
if (data.includes("active")) {
|
|
234
188
|
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
@@ -240,8 +194,8 @@ module.exports = function(RED) {
|
|
|
240
194
|
}
|
|
241
195
|
});
|
|
242
196
|
|
|
243
|
-
res.
|
|
244
|
-
res.
|
|
197
|
+
res.data.on('error', () => handleNvrError());
|
|
198
|
+
res.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
|
|
245
199
|
}).catch(() => handleNvrError());
|
|
246
200
|
}
|
|
247
201
|
|