node-red-contrib-hik-media-buffer 1.1.13 → 1.1.15
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 +65 -72
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -29,12 +29,9 @@ module.exports = function(RED) {
|
|
|
29
29
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
30
30
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
const baseStorage =
|
|
34
|
-
|
|
35
|
-
const vidDir = path.join(baseStorage, "video");
|
|
36
|
-
if (!fs.existsSync(imgDir)) fs.mkdirSync(imgDir, { recursive: true });
|
|
37
|
-
if (!fs.existsSync(vidDir)) fs.mkdirSync(vidDir, { recursive: true });
|
|
32
|
+
// BASE STORAGE TEMPORANEA
|
|
33
|
+
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
34
|
+
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
38
35
|
|
|
39
36
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
40
37
|
|
|
@@ -42,70 +39,39 @@ module.exports = function(RED) {
|
|
|
42
39
|
|
|
43
40
|
// --- PRENDE IL NOME DELLA TELECAMERA ---
|
|
44
41
|
async function getCameraName(cam) {
|
|
45
|
-
const camAuth = new AxiosDigestAuth({
|
|
46
|
-
username: node.user,
|
|
47
|
-
password: node.camPass
|
|
48
|
-
});
|
|
49
|
-
|
|
42
|
+
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
|
|
50
43
|
try {
|
|
51
|
-
// Interroghiamo il CANALE specifico (es. /channels/2)
|
|
52
44
|
const res = await camAuth.request({
|
|
53
45
|
method: 'GET',
|
|
54
|
-
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}`,
|
|
46
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
|
|
55
47
|
timeout: 5000,
|
|
56
48
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
57
49
|
});
|
|
58
|
-
|
|
59
50
|
const data = res.data.toString();
|
|
60
|
-
|
|
61
|
-
// In questo XML il tag è solitamente <name>
|
|
62
51
|
const match = data.match(/<name>([^<]+)<\/name>/i);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return match[1].trim(); // Restituirà "Ufficio"
|
|
66
|
-
} else {
|
|
67
|
-
return `Canale_${cam.channel}`;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
} catch (e) {
|
|
71
|
-
// Se l'URL sopra fallisce (alcuni modelli vecchi), proviamo questo URL alternativo
|
|
72
|
-
try {
|
|
73
|
-
const resAlt = await camAuth.request({
|
|
74
|
-
method: 'GET',
|
|
75
|
-
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/${cam.channel}`,
|
|
76
|
-
timeout: 3000
|
|
77
|
-
});
|
|
78
|
-
const dataAlt = resAlt.data.toString();
|
|
79
|
-
const matchAlt = dataAlt.match(/<name>([^<]+)<\/name>/i);
|
|
80
|
-
return matchAlt ? matchAlt[1].trim() : `Cam_${cam.channel}`;
|
|
81
|
-
} catch (err) {
|
|
82
|
-
return `Camera_${cam.ip}`;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
52
|
+
return match && match[1] ? match[1].trim() : `Canale_${cam.channel}`;
|
|
53
|
+
} catch (e) { return `Camera_${cam.ip}`; }
|
|
85
54
|
}
|
|
86
55
|
|
|
56
|
+
// --- CONTROLLO STATUS CAMERE ---
|
|
87
57
|
async function checkCameras() {
|
|
88
58
|
if (isClosing) return;
|
|
89
59
|
for (let cam of node.cameras) {
|
|
90
60
|
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
|
|
91
61
|
try {
|
|
92
62
|
await camAuth.request({
|
|
93
|
-
method: 'GET',
|
|
94
|
-
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
|
|
95
|
-
timeout: 5000,
|
|
63
|
+
method: 'GET', url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`, timeout: 5000,
|
|
96
64
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
97
65
|
});
|
|
98
66
|
if (statoCamera[cam.ip] === false) {
|
|
99
67
|
const nomeOnline = await getCameraName(cam);
|
|
100
|
-
node.send({ payload: { status: "online",
|
|
101
|
-
statoCamera[cam.ip] = true;
|
|
102
|
-
} else if (statoCamera[cam.ip] === undefined) {
|
|
68
|
+
node.send({ payload: { tipo_messaggio: "status", status: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
|
|
103
69
|
statoCamera[cam.ip] = true;
|
|
104
|
-
}
|
|
70
|
+
} else if (statoCamera[cam.ip] === undefined) { statoCamera[cam.ip] = true; }
|
|
105
71
|
} catch (e) {
|
|
106
72
|
if (statoCamera[cam.ip] !== false) {
|
|
107
73
|
const nomeOffline = await getCameraName(cam);
|
|
108
|
-
node.send({ payload: { status: "offline",
|
|
74
|
+
node.send({ payload: { tipo_messaggio: "status", status: "offline", nome_cliente: node.name, nome_telecamera: nomeOffline, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
|
|
109
75
|
statoCamera[cam.ip] = false;
|
|
110
76
|
}
|
|
111
77
|
}
|
|
@@ -115,17 +81,14 @@ module.exports = function(RED) {
|
|
|
115
81
|
|
|
116
82
|
function updateNodeStatus() {
|
|
117
83
|
const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
|
|
118
|
-
if (!nvrOnline) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
node.status({fill:"yellow", shape:"dot", text: `${offlineCams} Cam Offline`});
|
|
122
|
-
} else {
|
|
123
|
-
node.status({fill:"green", shape:"ring", text:"In ascolto"});
|
|
124
|
-
}
|
|
84
|
+
if (!nvrOnline) { node.status({fill:"red", shape:"ring", text:"NVR Offline"}); }
|
|
85
|
+
else if (offlineCams > 0) { node.status({fill:"yellow", shape:"dot", text: `${offlineCams} Cam Offline`}); }
|
|
86
|
+
else { node.status({fill:"green", shape:"ring", text:"In ascolto"}); }
|
|
125
87
|
}
|
|
126
88
|
|
|
127
89
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
128
90
|
|
|
91
|
+
// --- DOWNLOAD, CONVERSIONE E TIMEOUT DI ELIMINAZIONE A 2 MINUTI ---
|
|
129
92
|
async function downloadMedia(evento, channelID) {
|
|
130
93
|
const camera = node.cameras.find(c => c.channel == channelID);
|
|
131
94
|
const nomeCamera = await getCameraName(camera);
|
|
@@ -145,7 +108,18 @@ module.exports = function(RED) {
|
|
|
145
108
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
146
109
|
|
|
147
110
|
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
148
|
-
|
|
111
|
+
|
|
112
|
+
let output = {
|
|
113
|
+
tipo_messaggio: "evento",
|
|
114
|
+
nome_cliente: node.name,
|
|
115
|
+
nome_telecamera: nomeCamera,
|
|
116
|
+
ip_telecamera: camera.ip,
|
|
117
|
+
tipo_evento: evento,
|
|
118
|
+
timestamp_epoch: timestamp,
|
|
119
|
+
stato_telecamera: "ONLINE",
|
|
120
|
+
foto_base64: null,
|
|
121
|
+
video_base64: null
|
|
122
|
+
};
|
|
149
123
|
|
|
150
124
|
try {
|
|
151
125
|
const tracks = [{ id: "201" }, { id: "203" }];
|
|
@@ -162,46 +136,65 @@ module.exports = function(RED) {
|
|
|
162
136
|
if (uriMatch) {
|
|
163
137
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
164
138
|
const resDown = await camAuth.request({
|
|
165
|
-
method: 'GET',
|
|
166
|
-
url: `${baseUrl}/download`,
|
|
139
|
+
method: 'GET', url: `${baseUrl}/download`,
|
|
167
140
|
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
168
141
|
responseType: 'arraybuffer'
|
|
169
142
|
});
|
|
170
143
|
|
|
171
144
|
let buffer = Buffer.from(resDown.data);
|
|
145
|
+
|
|
172
146
|
if (t.id === "203") {
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
fs.writeFileSync(
|
|
176
|
-
|
|
177
|
-
|
|
147
|
+
// Scrittura immagine temporanea
|
|
148
|
+
const tempImgPath = path.join(baseStorage, `temp_${timestamp}.jpg`);
|
|
149
|
+
fs.writeFileSync(tempImgPath, buffer);
|
|
150
|
+
|
|
151
|
+
// Conversione immediata in stringa di testo Base64
|
|
152
|
+
output.foto_base64 = fs.readFileSync(tempImgPath, { encoding: 'base64' });
|
|
153
|
+
|
|
154
|
+
// MODIFICATO: Rimozione file posticipata a 2 minuti (120000 ms)
|
|
155
|
+
setTimeout(() => {
|
|
156
|
+
try { if (fs.existsSync(tempImgPath)) fs.unlinkSync(tempImgPath); } catch(e){}
|
|
157
|
+
}, 120000);
|
|
158
|
+
|
|
178
159
|
} else {
|
|
179
|
-
//
|
|
160
|
+
// Scrittura video temporaneo
|
|
180
161
|
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
181
|
-
const rawPath = path.join(
|
|
182
|
-
const fixedPath = path.join(
|
|
162
|
+
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
163
|
+
const fixedPath = path.join(baseStorage, `fixed_${timestamp}.mp4`);
|
|
183
164
|
fs.writeFileSync(rawPath, buffer);
|
|
184
165
|
|
|
185
166
|
await new Promise((resolve) => {
|
|
186
167
|
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
187
|
-
if (!err) {
|
|
188
|
-
output.
|
|
189
|
-
|
|
190
|
-
|
|
168
|
+
if (!err && fs.existsSync(fixedPath)) {
|
|
169
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
170
|
+
} else {
|
|
171
|
+
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// MODIFICATO: Rimozione file video posticipata a 2 minuti (120000 ms)
|
|
175
|
+
setTimeout(() => {
|
|
176
|
+
try { if (fs.existsSync(rawPath)) fs.unlinkSync(rawPath); } catch(e){}
|
|
177
|
+
try { if (fs.existsSync(fixedPath)) fs.unlinkSync(fixedPath); } catch(e){}
|
|
178
|
+
}, 120000);
|
|
179
|
+
|
|
191
180
|
resolve();
|
|
192
181
|
});
|
|
193
182
|
});
|
|
194
183
|
}
|
|
195
184
|
}
|
|
196
185
|
}
|
|
197
|
-
|
|
186
|
+
|
|
187
|
+
// Spedisce il payload al flusso di Node-RED
|
|
188
|
+
if (output.foto_base64 || output.video_base64) {
|
|
189
|
+
node.send({ payload: output });
|
|
190
|
+
}
|
|
198
191
|
} catch (e) {
|
|
199
192
|
node.error(`Errore Download Cam ${channelID}: ${e.message}`);
|
|
200
193
|
}
|
|
201
194
|
updateNodeStatus();
|
|
202
195
|
}
|
|
203
196
|
|
|
204
|
-
// --- ALERT STREAM
|
|
197
|
+
// --- ALERT STREAM ---
|
|
205
198
|
function startAlertStream() {
|
|
206
199
|
if (isClosing) return;
|
|
207
200
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
@@ -209,7 +202,7 @@ module.exports = function(RED) {
|
|
|
209
202
|
nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
|
|
210
203
|
.then(response => {
|
|
211
204
|
streamRequest = response;
|
|
212
|
-
if (!nvrOnline) { node.send({ payload: { status: "online",
|
|
205
|
+
if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", status: "online", ip_telecamera: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
|
|
213
206
|
updateNodeStatus();
|
|
214
207
|
response.data.on('data', (chunk) => {
|
|
215
208
|
const data = chunk.toString().toLowerCase();
|
|
@@ -228,7 +221,7 @@ module.exports = function(RED) {
|
|
|
228
221
|
}
|
|
229
222
|
|
|
230
223
|
function handleNvrError() {
|
|
231
|
-
if (nvrOnline) { node.send({ payload: { status: "offline",
|
|
224
|
+
if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", status: "offline", ip_telecamera: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
|
|
232
225
|
updateNodeStatus();
|
|
233
226
|
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
234
227
|
}
|