node-red-contrib-hik-media-buffer 1.1.69 → 1.1.71
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 +29 -98
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -32,6 +32,11 @@ module.exports = function(RED) {
|
|
|
32
32
|
|
|
33
33
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
34
34
|
|
|
35
|
+
function toHikDate(d) {
|
|
36
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
37
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
38
|
+
}
|
|
39
|
+
|
|
35
40
|
async function getCameraName(channelID) {
|
|
36
41
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
37
42
|
try {
|
|
@@ -105,11 +110,8 @@ module.exports = function(RED) {
|
|
|
105
110
|
|
|
106
111
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
107
112
|
|
|
108
|
-
// --- STRATEGIA DI DOWNLOAD MULTIMEDIALE 100% JSON + FFMPEG EXTRACTION ---
|
|
109
|
-
// --- STRATEGIA DI DOWNLOAD MULTIMEDIALE 100% JSON + FFMPEG EXTRACTION ---
|
|
110
|
-
// --- STRATEGIA DI DOWNLOAD MULTIMEDIALE CON DOPPIO FALLBACK ORARIO (LOCALE / UTC) ---
|
|
111
113
|
async function downloadMedia(evento, channelID) {
|
|
112
|
-
node.warn(`[
|
|
114
|
+
node.warn(`[TRIGGER] Ricevuto evento. Evento: ${evento}, Canale originario: ${channelID}`);
|
|
113
115
|
|
|
114
116
|
const nowTime = Date.now();
|
|
115
117
|
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
@@ -120,27 +122,16 @@ module.exports = function(RED) {
|
|
|
120
122
|
const referenceTime = new Date();
|
|
121
123
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
122
124
|
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
const pad = (num) => String(num).padStart(2, '0');
|
|
126
|
-
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
// FUNZIONE 2: FORMATTO IN ORA UTC (Es. 2026-06-09T12:30:00Z)
|
|
130
|
-
const formatUtcDate = (d) => {
|
|
131
|
-
const pad = (num) => String(num).padStart(2, '0');
|
|
132
|
-
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
133
|
-
};
|
|
125
|
+
const startTime = toHikDate(new Date(referenceTime.getTime() - (30 * 1000)));
|
|
126
|
+
const endTime = toHikDate(new Date(referenceTime.getTime() + (5 * 1000)));
|
|
134
127
|
|
|
135
128
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
136
129
|
const nomeCamera = await getCameraName(channelID);
|
|
137
130
|
|
|
138
|
-
// Aspettiamo 8 secondi per dare tempo all'Hard Disk di scrivere l'indice
|
|
139
|
-
node.warn(`[warn] [hik-media-buffer:test] Attesa scrittura indice Hard Disk (8s)...`);
|
|
140
131
|
await new Promise(resolve => setTimeout(resolve, 8000));
|
|
141
132
|
|
|
142
133
|
const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
|
|
143
|
-
|
|
134
|
+
let evMinuscolo = evento.toLowerCase();
|
|
144
135
|
|
|
145
136
|
let output = {
|
|
146
137
|
tipo_messaggio: "evento",
|
|
@@ -156,85 +147,37 @@ module.exports = function(RED) {
|
|
|
156
147
|
};
|
|
157
148
|
|
|
158
149
|
const parsedChannel = parseInt(channelID, 10);
|
|
159
|
-
let fileDaCancellare = [];
|
|
160
|
-
let videoUriMatch = null;
|
|
161
|
-
|
|
162
|
-
// --- TENTATIVO 1: RICERCA IN ORA LOCALE (La più probabile per i dischi NVR) ---
|
|
163
|
-
const startLocale = formatLocaleDate(new Date(referenceTime.getTime() - (120 * 1000)));
|
|
164
|
-
const endLocale = formatLocaleDate(new Date(referenceTime.getTime() + (30 * 1000)));
|
|
165
150
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const trackVideoId = (parsedChannel * 100) + 1;
|
|
169
|
-
|
|
170
|
-
let payloadLocale = {
|
|
151
|
+
// Query JSON Pulita e Blindata (Ancorata al Canale Reale dell'NVR)
|
|
152
|
+
const searchJsonPayload = {
|
|
171
153
|
"EventSearchDescription": {
|
|
172
154
|
"searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
|
|
173
155
|
"searchResultPosition": 0,
|
|
174
156
|
"maxResults": 10,
|
|
175
|
-
"timeSpanList": [{"startTime":
|
|
157
|
+
"timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
|
|
176
158
|
"type": "all",
|
|
177
|
-
"channels": [
|
|
159
|
+
"channels": [parsedChannel],
|
|
178
160
|
"eventType": "behavior",
|
|
179
|
-
"behavior": {"behaviorEventType":
|
|
161
|
+
"behavior": { "behaviorEventType": "all" }
|
|
180
162
|
}
|
|
181
163
|
};
|
|
182
164
|
|
|
165
|
+
let fileDaCancellare = [];
|
|
166
|
+
|
|
183
167
|
try {
|
|
184
|
-
|
|
168
|
+
const resSearchVideo = await camAuth.request({
|
|
185
169
|
method: 'POST',
|
|
186
170
|
url: `${baseUrl}/eventRecordSearch?format=json`,
|
|
187
|
-
data:
|
|
188
|
-
headers: { "Content-Type": "application/json" }
|
|
189
|
-
timeout: 5000
|
|
171
|
+
data: searchJsonPayload,
|
|
172
|
+
headers: { "Content-Type": "application/json" }
|
|
190
173
|
});
|
|
191
|
-
let dataStr = JSON.stringify(resLocale.data);
|
|
192
|
-
videoUriMatch = dataStr.match(/rtsp:\/\/[^"]+/i) || dataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
193
|
-
} catch (e) {
|
|
194
|
-
node.warn(`[warn] [hik-media-buffer:test] Tentativo 1 fallito o timeout: ${e.message}`);
|
|
195
|
-
}
|
|
196
174
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const startUtc = formatUtcDate(new Date(referenceTime.getTime() - (120 * 1000)));
|
|
200
|
-
const endUtc = formatUtcDate(new Date(referenceTime.getTime() + (30 * 1000)));
|
|
175
|
+
const resDataStr = JSON.stringify(resSearchVideo.data);
|
|
176
|
+
const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
201
177
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
"EventSearchDescription": {
|
|
206
|
-
"searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
|
|
207
|
-
"searchResultPosition": 0,
|
|
208
|
-
"maxResults": 10,
|
|
209
|
-
"timeSpanList": [{"startTime": startUtc, "endTime": endUtc}],
|
|
210
|
-
"type": "all",
|
|
211
|
-
"channels": [trackVideoId],
|
|
212
|
-
"eventType": "behavior",
|
|
213
|
-
"behavior": {"behaviorEventType": evMinuscolo}
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
try {
|
|
218
|
-
let resUtc = await camAuth.request({
|
|
219
|
-
method: 'POST',
|
|
220
|
-
url: `${baseUrl}/eventRecordSearch?format=json`,
|
|
221
|
-
data: payloadUtc,
|
|
222
|
-
headers: { "Content-Type": "application/json" },
|
|
223
|
-
timeout: 5000
|
|
224
|
-
});
|
|
225
|
-
let dataStr = JSON.stringify(resUtc.data);
|
|
226
|
-
videoUriMatch = dataStr.match(/rtsp:\/\/[^"]+/i) || dataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
227
|
-
} catch (e) {
|
|
228
|
-
node.error(`[error] [hik-media-buffer:test] Errore critico anche nel Fallback UTC: ${e.message}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// --- PROCESSO DI SCARICAMENTO SE UNA DELLE DUE QUERIES HA TROVATO IL VIDEO ---
|
|
233
|
-
if (videoUriMatch) {
|
|
234
|
-
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
235
|
-
node.warn(`[warn] [hik-media-buffer:test] [FOUND] Trovato playbackURI video: ${rawVideoUri}`);
|
|
236
|
-
|
|
237
|
-
try {
|
|
178
|
+
if (videoUriMatch) {
|
|
179
|
+
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
180
|
+
|
|
238
181
|
const resDownVideo = await camAuth.request({
|
|
239
182
|
method: 'GET',
|
|
240
183
|
url: `${baseUrl}/download`,
|
|
@@ -252,51 +195,39 @@ module.exports = function(RED) {
|
|
|
252
195
|
fs.writeFileSync(rawPath, videoBuffer);
|
|
253
196
|
fileDaCancellare.push(rawPath);
|
|
254
197
|
|
|
255
|
-
node.warn(`[warn] [hik-media-buffer:test] Esecuzione FFMPEG (Estrazione JPG + FastStart MP4)...`);
|
|
256
198
|
await new Promise((resolve) => {
|
|
257
199
|
exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
258
200
|
if (!err) {
|
|
259
201
|
if (fs.existsSync(extractedImgPath)) {
|
|
260
202
|
output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
|
|
261
203
|
fileDaCancellare.push(extractedImgPath);
|
|
262
|
-
node.warn(`[warn] [hik-media-buffer:test] Snapshot FOTO estratto con successo.`);
|
|
263
204
|
}
|
|
264
205
|
if (fs.existsSync(fixedPath)) {
|
|
265
206
|
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
266
207
|
fileDaCancellare.push(fixedPath);
|
|
267
|
-
node.warn(`[warn] [hik-media-buffer:test] Video MP4 indicizzato con successo.`);
|
|
268
208
|
}
|
|
269
209
|
} else {
|
|
270
|
-
node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, uso file grezzo: ${err.message}`);
|
|
271
210
|
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
272
211
|
}
|
|
273
212
|
resolve();
|
|
274
213
|
});
|
|
275
214
|
});
|
|
276
|
-
} catch (downErr) {
|
|
277
|
-
node.error(`[error] [hik-media-buffer:test] Errore durante il download fisico del file: ${downErr.message}`);
|
|
278
215
|
}
|
|
279
|
-
}
|
|
280
|
-
node.
|
|
216
|
+
} catch (videoErr) {
|
|
217
|
+
node.error(`[ERROR] Errore esecuzione: ${videoErr.message}`);
|
|
281
218
|
}
|
|
282
219
|
|
|
283
|
-
// --- SPEDIZIONE PAYLOAD VERSO PYTHON ---
|
|
284
220
|
if (output.foto_base64 || output.video_base64) {
|
|
285
|
-
node.warn(`[warn] [hik-media-buffer:test] Spedizione payload multimediale completata con successo!`);
|
|
286
221
|
node.send({ payload: output });
|
|
287
|
-
|
|
288
222
|
setTimeout(() => {
|
|
289
223
|
for (let file of fileDaCancellare) {
|
|
290
224
|
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
291
225
|
}
|
|
292
|
-
},
|
|
293
|
-
} else {
|
|
294
|
-
node.warn(`[warn] [hik-media-buffer:test] Nessun pacchetto inviato a Node-RED.`);
|
|
226
|
+
}, 60000);
|
|
295
227
|
}
|
|
296
228
|
updateNodeStatus();
|
|
297
229
|
}
|
|
298
230
|
|
|
299
|
-
// --- ALERT STREAM ---
|
|
300
231
|
function startAlertStream() {
|
|
301
232
|
if (isClosing) return;
|
|
302
233
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
@@ -305,7 +236,7 @@ module.exports = function(RED) {
|
|
|
305
236
|
nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
|
|
306
237
|
.then(response => {
|
|
307
238
|
streamRequest = response;
|
|
308
|
-
if (!nvrOnline) {
|
|
239
|
+
if (!nvrOnline) { nvrOnline = true; }
|
|
309
240
|
updateNodeStatus();
|
|
310
241
|
|
|
311
242
|
response.data.on('data', (chunk) => {
|
|
@@ -327,7 +258,7 @@ module.exports = function(RED) {
|
|
|
327
258
|
}
|
|
328
259
|
|
|
329
260
|
function handleNvrError() {
|
|
330
|
-
|
|
261
|
+
nvrOnline = false;
|
|
331
262
|
updateNodeStatus();
|
|
332
263
|
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
333
264
|
}
|