node-red-contrib-hik-media-buffer 1.1.70 → 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 -107
- 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,36 +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. YYYY-MM-DDTHH:mm:ssZ)
|
|
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 stabilizzare il file temporaneo
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
// CREIAMO LE FINESTRE TEMPORALI: Dal passato fino a ORA (Senza sforare nel futuro!)
|
|
146
|
-
const oraLocaleRichiesta = new Date(); // Questo è il momento esatto del termine degli 8s
|
|
147
|
-
const startLocale = formatLocaleDate(new Date(oraLocaleRichiesta.getTime() - (90 * 1000))); // 90 secondi fa
|
|
148
|
-
const endLocale = formatLocaleDate(oraLocaleRichiesta); // Esattamente ADESSO
|
|
149
|
-
|
|
150
|
-
const oraUtcRichiesta = new Date();
|
|
151
|
-
const startUtc = formatUtcDate(new Date(oraUtcRichiesta.getTime() - (90 * 1000))); // 90 secondi fa
|
|
152
|
-
const endUtc = formatUtcDate(oraUtcRichiesta); // Esattamente ADESSO
|
|
134
|
+
let evMinuscolo = evento.toLowerCase();
|
|
153
135
|
|
|
154
136
|
let output = {
|
|
155
137
|
tipo_messaggio: "evento",
|
|
@@ -165,85 +147,37 @@ module.exports = function(RED) {
|
|
|
165
147
|
};
|
|
166
148
|
|
|
167
149
|
const parsedChannel = parseInt(channelID, 10);
|
|
168
|
-
let fileDaCancellare = [];
|
|
169
|
-
let videoUriMatch = null;
|
|
170
|
-
|
|
171
|
-
// --- TENTATIVO 1: RICERCA IN ORA LOCALE (La più probabile per i dischi NVR) ---
|
|
172
|
-
const startLocale = formatLocaleDate(new Date(referenceTime.getTime() - (120 * 1000)));
|
|
173
|
-
const endLocale = formatLocaleDate(new Date(referenceTime.getTime() + (30 * 1000)));
|
|
174
150
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const trackVideoId = (parsedChannel * 100) + 1;
|
|
178
|
-
|
|
179
|
-
let payloadLocale = {
|
|
151
|
+
// Query JSON Pulita e Blindata (Ancorata al Canale Reale dell'NVR)
|
|
152
|
+
const searchJsonPayload = {
|
|
180
153
|
"EventSearchDescription": {
|
|
181
154
|
"searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
|
|
182
155
|
"searchResultPosition": 0,
|
|
183
156
|
"maxResults": 10,
|
|
184
|
-
"timeSpanList": [{"startTime":
|
|
157
|
+
"timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
|
|
185
158
|
"type": "all",
|
|
186
|
-
"channels": [
|
|
159
|
+
"channels": [parsedChannel],
|
|
187
160
|
"eventType": "behavior",
|
|
188
|
-
"behavior": {"behaviorEventType":
|
|
161
|
+
"behavior": { "behaviorEventType": "all" }
|
|
189
162
|
}
|
|
190
163
|
};
|
|
191
164
|
|
|
165
|
+
let fileDaCancellare = [];
|
|
166
|
+
|
|
192
167
|
try {
|
|
193
|
-
|
|
168
|
+
const resSearchVideo = await camAuth.request({
|
|
194
169
|
method: 'POST',
|
|
195
170
|
url: `${baseUrl}/eventRecordSearch?format=json`,
|
|
196
|
-
data:
|
|
197
|
-
headers: { "Content-Type": "application/json" }
|
|
198
|
-
timeout: 5000
|
|
171
|
+
data: searchJsonPayload,
|
|
172
|
+
headers: { "Content-Type": "application/json" }
|
|
199
173
|
});
|
|
200
|
-
let dataStr = JSON.stringify(resLocale.data);
|
|
201
|
-
videoUriMatch = dataStr.match(/rtsp:\/\/[^"]+/i) || dataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
202
|
-
} catch (e) {
|
|
203
|
-
node.warn(`[warn] [hik-media-buffer:test] Tentativo 1 fallito o timeout: ${e.message}`);
|
|
204
|
-
}
|
|
205
174
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const startUtc = formatUtcDate(new Date(referenceTime.getTime() - (120 * 1000)));
|
|
209
|
-
const endUtc = formatUtcDate(new Date(referenceTime.getTime() + (30 * 1000)));
|
|
210
|
-
|
|
211
|
-
node.warn(`[warn] [hik-media-buffer:test] [TENTATIVO 2] Video locale non trovato. Provo Fallback in Ora UTC: ${startUtc} -> ${endUtc}`);
|
|
175
|
+
const resDataStr = JSON.stringify(resSearchVideo.data);
|
|
176
|
+
const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
212
177
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
"searchResultPosition": 0,
|
|
217
|
-
"maxResults": 10,
|
|
218
|
-
"timeSpanList": [{"startTime": startUtc, "endTime": endUtc}],
|
|
219
|
-
"type": "all",
|
|
220
|
-
"channels": [trackVideoId],
|
|
221
|
-
"eventType": "behavior",
|
|
222
|
-
"behavior": {"behaviorEventType": evMinuscolo}
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
|
|
226
|
-
try {
|
|
227
|
-
let resUtc = await camAuth.request({
|
|
228
|
-
method: 'POST',
|
|
229
|
-
url: `${baseUrl}/eventRecordSearch?format=json`,
|
|
230
|
-
data: payloadUtc,
|
|
231
|
-
headers: { "Content-Type": "application/json" },
|
|
232
|
-
timeout: 5000
|
|
233
|
-
});
|
|
234
|
-
let dataStr = JSON.stringify(resUtc.data);
|
|
235
|
-
videoUriMatch = dataStr.match(/rtsp:\/\/[^"]+/i) || dataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
236
|
-
} catch (e) {
|
|
237
|
-
node.error(`[error] [hik-media-buffer:test] Errore critico anche nel Fallback UTC: ${e.message}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// --- PROCESSO DI SCARICAMENTO SE UNA DELLE DUE QUERIES HA TROVATO IL VIDEO ---
|
|
242
|
-
if (videoUriMatch) {
|
|
243
|
-
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
244
|
-
node.warn(`[warn] [hik-media-buffer:test] [FOUND] Trovato playbackURI video: ${rawVideoUri}`);
|
|
245
|
-
|
|
246
|
-
try {
|
|
178
|
+
if (videoUriMatch) {
|
|
179
|
+
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
180
|
+
|
|
247
181
|
const resDownVideo = await camAuth.request({
|
|
248
182
|
method: 'GET',
|
|
249
183
|
url: `${baseUrl}/download`,
|
|
@@ -261,51 +195,39 @@ module.exports = function(RED) {
|
|
|
261
195
|
fs.writeFileSync(rawPath, videoBuffer);
|
|
262
196
|
fileDaCancellare.push(rawPath);
|
|
263
197
|
|
|
264
|
-
node.warn(`[warn] [hik-media-buffer:test] Esecuzione FFMPEG (Estrazione JPG + FastStart MP4)...`);
|
|
265
198
|
await new Promise((resolve) => {
|
|
266
199
|
exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
267
200
|
if (!err) {
|
|
268
201
|
if (fs.existsSync(extractedImgPath)) {
|
|
269
202
|
output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
|
|
270
203
|
fileDaCancellare.push(extractedImgPath);
|
|
271
|
-
node.warn(`[warn] [hik-media-buffer:test] Snapshot FOTO estratto con successo.`);
|
|
272
204
|
}
|
|
273
205
|
if (fs.existsSync(fixedPath)) {
|
|
274
206
|
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
275
207
|
fileDaCancellare.push(fixedPath);
|
|
276
|
-
node.warn(`[warn] [hik-media-buffer:test] Video MP4 indicizzato con successo.`);
|
|
277
208
|
}
|
|
278
209
|
} else {
|
|
279
|
-
node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, uso file grezzo: ${err.message}`);
|
|
280
210
|
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
281
211
|
}
|
|
282
212
|
resolve();
|
|
283
213
|
});
|
|
284
214
|
});
|
|
285
|
-
} catch (downErr) {
|
|
286
|
-
node.error(`[error] [hik-media-buffer:test] Errore durante il download fisico del file: ${downErr.message}`);
|
|
287
215
|
}
|
|
288
|
-
}
|
|
289
|
-
node.
|
|
216
|
+
} catch (videoErr) {
|
|
217
|
+
node.error(`[ERROR] Errore esecuzione: ${videoErr.message}`);
|
|
290
218
|
}
|
|
291
219
|
|
|
292
|
-
// --- SPEDIZIONE PAYLOAD VERSO PYTHON ---
|
|
293
220
|
if (output.foto_base64 || output.video_base64) {
|
|
294
|
-
node.warn(`[warn] [hik-media-buffer:test] Spedizione payload multimediale completata con successo!`);
|
|
295
221
|
node.send({ payload: output });
|
|
296
|
-
|
|
297
222
|
setTimeout(() => {
|
|
298
223
|
for (let file of fileDaCancellare) {
|
|
299
224
|
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
300
225
|
}
|
|
301
|
-
},
|
|
302
|
-
} else {
|
|
303
|
-
node.warn(`[warn] [hik-media-buffer:test] Nessun pacchetto inviato a Node-RED.`);
|
|
226
|
+
}, 60000);
|
|
304
227
|
}
|
|
305
228
|
updateNodeStatus();
|
|
306
229
|
}
|
|
307
230
|
|
|
308
|
-
// --- ALERT STREAM ---
|
|
309
231
|
function startAlertStream() {
|
|
310
232
|
if (isClosing) return;
|
|
311
233
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
@@ -314,7 +236,7 @@ module.exports = function(RED) {
|
|
|
314
236
|
nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
|
|
315
237
|
.then(response => {
|
|
316
238
|
streamRequest = response;
|
|
317
|
-
if (!nvrOnline) {
|
|
239
|
+
if (!nvrOnline) { nvrOnline = true; }
|
|
318
240
|
updateNodeStatus();
|
|
319
241
|
|
|
320
242
|
response.data.on('data', (chunk) => {
|
|
@@ -336,7 +258,7 @@ module.exports = function(RED) {
|
|
|
336
258
|
}
|
|
337
259
|
|
|
338
260
|
function handleNvrError() {
|
|
339
|
-
|
|
261
|
+
nvrOnline = false;
|
|
340
262
|
updateNodeStatus();
|
|
341
263
|
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
342
264
|
}
|