node-red-contrib-hik-media-buffer 1.1.65 → 1.1.67
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 +98 -148
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -12,7 +12,7 @@ module.exports = function(RED) {
|
|
|
12
12
|
const node = this;
|
|
13
13
|
|
|
14
14
|
node.name = config.name || "TEST";
|
|
15
|
-
node.host = config.host;
|
|
15
|
+
node.host = config.host;
|
|
16
16
|
node.port = config.port || "80";
|
|
17
17
|
node.protocol = config.protocol || "http";
|
|
18
18
|
node.user = config.user;
|
|
@@ -32,12 +32,6 @@ 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
|
-
// Genera esattamente: YYYY-MM-DDTHH:mm:ssZ
|
|
38
|
-
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
35
|
async function getCameraName(channelID) {
|
|
42
36
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
43
37
|
try {
|
|
@@ -111,49 +105,46 @@ module.exports = function(RED) {
|
|
|
111
105
|
|
|
112
106
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
113
107
|
|
|
114
|
-
// ---
|
|
108
|
+
// --- STRATEGIA DI DOWNLOAD MULTIMEDIALE 100% JSON + FFMPEG EXTRACTION ---
|
|
109
|
+
// --- STRATEGIA DI DOWNLOAD MULTIMEDIALE 100% JSON + FFMPEG EXTRACTION ---
|
|
115
110
|
async function downloadMedia(evento, channelID) {
|
|
116
|
-
node.warn(`[warn] [hik-media-buffer:test] [
|
|
111
|
+
node.warn(`[warn] [hik-media-buffer:test] [TRIGGER] Ricevuto evento. Evento: ${evento}, Canale originario: ${channelID}`);
|
|
117
112
|
|
|
118
113
|
const nowTime = Date.now();
|
|
119
114
|
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
120
|
-
if (nowTime - lastTriggerTime[channelID] < 5000)
|
|
121
|
-
node.warn(`[warn] [hik-media-buffer:test] Antirimbalzo bloccato.`);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
115
|
+
if (nowTime - lastTriggerTime[channelID] < 5000) return;
|
|
124
116
|
lastTriggerTime[channelID] = nowTime;
|
|
125
117
|
|
|
126
118
|
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
127
119
|
const referenceTime = new Date();
|
|
128
120
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
129
121
|
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
122
|
+
// FORMATTAZIONE DATA UTC PULITA PER IL JSON DELL'NVR
|
|
123
|
+
const formatJsonDate = (d) => {
|
|
124
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
125
|
+
return `${d.getUTCFullYear()}-${pad(d.getUTCMonth()+1)}-${pad(d.getUTCDate())}T${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}:${pad(d.getUTCSeconds())}Z`;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
// ALLARGHIAMO LA FINESTRA: Cerca da 2 minuti prima dell'evento a 30 secondi dopo
|
|
129
|
+
const startTimeJson = formatJsonDate(new Date(referenceTime.getTime() - (120 * 1000)));
|
|
130
|
+
const endTimeJson = formatJsonDate(new Date(referenceTime.getTime() + (30 * 1000)));
|
|
134
131
|
|
|
135
132
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
136
133
|
const nomeCamera = await getCameraName(channelID);
|
|
137
134
|
|
|
138
|
-
|
|
139
|
-
|
|
135
|
+
// PORTIAMO L'ATTESA A 10 SECONDI PER L'HARD DISK
|
|
136
|
+
node.warn(`[warn] [hik-media-buffer:test] Attesa scrittura completa su Hard Disk (10s)...`);
|
|
137
|
+
await new Promise(resolve => setTimeout(resolve, 10000));
|
|
140
138
|
|
|
141
139
|
const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
|
|
142
|
-
|
|
143
|
-
let eventoNVR = "FieldDetection";
|
|
144
|
-
for (let e of EventList) {
|
|
145
|
-
if (evento.toLowerCase() === e.toLowerCase()) {
|
|
146
|
-
eventoNVR = e;
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
140
|
+
const evMinuscolo = evento.toLowerCase();
|
|
150
141
|
|
|
151
142
|
let output = {
|
|
152
143
|
tipo_messaggio: "evento",
|
|
153
144
|
nome_cliente: node.name,
|
|
154
145
|
nome_telecamera: nomeCamera,
|
|
155
146
|
ip_telecamera: node.host,
|
|
156
|
-
tipo_evento:
|
|
147
|
+
tipo_evento: evMinuscolo.includes("linedetection") ? "LineDetection" : "FieldDetection",
|
|
157
148
|
timestamp_epoch: timestamp,
|
|
158
149
|
stato_telecamera: "ONLINE",
|
|
159
150
|
channel: channelID.toString(),
|
|
@@ -162,150 +153,109 @@ module.exports = function(RED) {
|
|
|
162
153
|
};
|
|
163
154
|
|
|
164
155
|
const parsedChannel = parseInt(channelID, 10);
|
|
165
|
-
const trackVideo = (parsedChannel * 100) + 1;
|
|
166
|
-
const trackFoto = (parsedChannel * 100) + 3;
|
|
167
|
-
|
|
168
|
-
// Le due tracce originali del tuo ciclo
|
|
169
|
-
const tracks = [
|
|
170
|
-
{ id: trackVideo.toString(), isFoto: false, label: "VIDEO" },
|
|
171
|
-
{ id: trackFoto.toString(), isFoto: true, label: "FOTO" }
|
|
172
|
-
];
|
|
173
156
|
let fileDaCancellare = [];
|
|
174
157
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
url: `${baseUrl}/search`,
|
|
191
|
-
data: searchXml,
|
|
192
|
-
headers: { "Content-Type": "application/xml" }
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
196
|
-
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
197
|
-
if (uriMatch) {
|
|
198
|
-
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
199
|
-
await scaricaIlFile(rawUri, t.isFoto);
|
|
200
|
-
}
|
|
201
|
-
} catch (err) {
|
|
202
|
-
node.error(`[error] [hik-media-buffer:test] Errore FOTO XML: ${err.message}`);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
} else {
|
|
206
|
-
// --- SE È IL VIDEO: USA IL JSON DELL'ISPEZIONA NVR CHE ANDAVA ---
|
|
207
|
-
// Adattiamo le date al formato del JSON dell'NVR (senza Z, con spazio offset se necessario, o UTC pulito)
|
|
208
|
-
const startTimeJson = startTime.replace('Z', ' 02:00'); // adatta al fuso orario del tuo NVR se serve, o lascialo Z
|
|
209
|
-
const endTimeJson = endTime.replace('Z', ' 02:00');
|
|
210
|
-
|
|
211
|
-
const searchJsonPayload = {
|
|
212
|
-
"EventSearchDescription": {
|
|
213
|
-
"searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
|
|
214
|
-
"searchResultPosition": 0,
|
|
215
|
-
"maxResults": 30,
|
|
216
|
-
"timeSpanList": [{
|
|
217
|
-
"startTime": startTime.replace('Z', ''), // Prova formato pulito senza Z richiesto dal JSON
|
|
218
|
-
"endTime": endTime.replace('Z', '')
|
|
219
|
-
}],
|
|
220
|
-
"type": "all",
|
|
221
|
-
"channels": [parsedChannel],
|
|
222
|
-
"eventType": "behavior",
|
|
223
|
-
"behavior": {
|
|
224
|
-
"behaviorEventType": evMinuscolo
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
};
|
|
228
|
-
|
|
229
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST JSON /eventRecordSearch per VIDEO...`);
|
|
230
|
-
|
|
231
|
-
try {
|
|
232
|
-
const resSearchVideo = await camAuth.request({
|
|
233
|
-
method: 'POST',
|
|
234
|
-
url: `${baseUrl}/eventRecordSearch?format=json`,
|
|
235
|
-
data: searchJsonPayload,
|
|
236
|
-
headers: { "Content-Type": "application/json" }
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
const resDataStr = JSON.stringify(resSearchVideo.data);
|
|
240
|
-
const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
241
|
-
|
|
242
|
-
if (videoUriMatch) {
|
|
243
|
-
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
244
|
-
await scaricaIlFile(rawVideoUri, t.isFoto);
|
|
245
|
-
}
|
|
246
|
-
} catch (err) {
|
|
247
|
-
node.error(`[error] [hik-media-buffer:test] Errore VIDEO JSON: ${err.message}`);
|
|
248
|
-
}
|
|
158
|
+
// --- STRUTTURA JSON DALL'ISPEZIONA NVR NATIVO ---
|
|
159
|
+
const searchJsonPayload = {
|
|
160
|
+
"EventSearchDescription": {
|
|
161
|
+
"searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
|
|
162
|
+
"searchResultPosition": 0,
|
|
163
|
+
"maxResults": 30,
|
|
164
|
+
"timeSpanList": [{
|
|
165
|
+
"startTime": startTimeJson,
|
|
166
|
+
"endTime": endTimeJson
|
|
167
|
+
}],
|
|
168
|
+
"type": "all",
|
|
169
|
+
"channels": [parsedChannel],
|
|
170
|
+
"eventType": "behavior",
|
|
171
|
+
"behavior": {
|
|
172
|
+
"behaviorEventType": evMinuscolo
|
|
249
173
|
}
|
|
250
174
|
}
|
|
175
|
+
};
|
|
251
176
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
method: '
|
|
257
|
-
url: `${baseUrl}/
|
|
258
|
-
data:
|
|
259
|
-
|
|
177
|
+
node.warn(`[warn] [hik-media-buffer:test] Invio query JSON a eventRecordSearch per canale ${parsedChannel}...`);
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const resSearchVideo = await camAuth.request({
|
|
181
|
+
method: 'POST',
|
|
182
|
+
url: `${baseUrl}/eventRecordSearch?format=json`,
|
|
183
|
+
data: searchJsonPayload,
|
|
184
|
+
headers: { "Content-Type": "application/json" }
|
|
260
185
|
});
|
|
261
186
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
187
|
+
const resDataStr = JSON.stringify(resSearchVideo.data);
|
|
188
|
+
// Estrae l'RTSP playbackURI o l'endpoint di download dal JSON di risposta
|
|
189
|
+
const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
190
|
+
|
|
191
|
+
if (videoUriMatch) {
|
|
192
|
+
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
193
|
+
node.warn(`[warn] [hik-media-buffer:test] Trovato playbackURI video nel JSON: ${rawVideoUri}`);
|
|
194
|
+
|
|
195
|
+
// Scarichiamo il filmato dall'NVR
|
|
196
|
+
const resDownVideo = await camAuth.request({
|
|
197
|
+
method: 'GET',
|
|
198
|
+
url: `${baseUrl}/download`,
|
|
199
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawVideoUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
200
|
+
responseType: 'arraybuffer'
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
let videoBuffer = Buffer.from(resDownVideo.data);
|
|
204
|
+
if (videoBuffer.slice(0, 4).toString() === 'IMKH') videoBuffer = videoBuffer.slice(40);
|
|
205
|
+
|
|
270
206
|
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
271
207
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
272
|
-
|
|
208
|
+
const extractedImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
273
209
|
|
|
210
|
+
fs.writeFileSync(rawPath, videoBuffer);
|
|
211
|
+
fileDaCancellare.push(rawPath);
|
|
212
|
+
|
|
213
|
+
node.warn(`[warn] [hik-media-buffer:test] Esecuzione FFMPEG simultanea (Estrazione frame JPG + conversione video MP4)...`);
|
|
274
214
|
await new Promise((resolve) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
215
|
+
// FFMPEG estrae il primo secondo in JPG e indicizza il video in FastStart in un colpo solo
|
|
216
|
+
exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
217
|
+
if (!err) {
|
|
218
|
+
if (fs.existsSync(extractedImgPath)) {
|
|
219
|
+
output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
|
|
220
|
+
fileDaCancellare.push(extractedImgPath);
|
|
221
|
+
node.warn(`[warn] [hik-media-buffer:test] Foto estratta dal video correttamente.`);
|
|
222
|
+
}
|
|
223
|
+
if (fs.existsSync(fixedPath)) {
|
|
224
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
225
|
+
fileDaCancellare.push(fixedPath);
|
|
226
|
+
node.warn(`[warn] [hik-media-buffer:test] Video ottimizzato convertito correttamente.`);
|
|
227
|
+
}
|
|
279
228
|
} else {
|
|
229
|
+
node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, invio video originale grezzo: ${err.message}`);
|
|
280
230
|
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
281
231
|
}
|
|
282
|
-
fileDaCancellare.push(rawPath);
|
|
283
232
|
resolve();
|
|
284
233
|
});
|
|
285
234
|
});
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
if (output.foto_base64 || output.video_base64) {
|
|
290
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
|
|
291
|
-
node.send({ payload: output });
|
|
292
|
-
|
|
293
|
-
setTimeout(() => {
|
|
294
|
-
for (let file of fileDaCancellare) {
|
|
295
|
-
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
296
|
-
}
|
|
297
|
-
}, 120000);
|
|
298
235
|
} else {
|
|
299
|
-
node.warn(`[warn] [hik-media-buffer:test]
|
|
236
|
+
node.warn(`[warn] [hik-media-buffer:test] Nessun file video trovato nel database JSON dell'NVR.`);
|
|
300
237
|
}
|
|
238
|
+
} catch (videoErr) {
|
|
239
|
+
node.error(`[error] [hik-media-buffer:test] Errore critico durante la ricerca JSON: ${videoErr.message}`);
|
|
240
|
+
}
|
|
301
241
|
|
|
302
|
-
|
|
303
|
-
|
|
242
|
+
// --- SPEDIZIONE VERSO PYTHON ---
|
|
243
|
+
if (output.foto_base64 || output.video_base64) {
|
|
244
|
+
node.warn(`[warn] [hik-media-buffer:test] Spedizione payload multimediale completata!`);
|
|
245
|
+
node.send({ payload: output });
|
|
246
|
+
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
for (let file of fileDaCancellare) {
|
|
249
|
+
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
250
|
+
}
|
|
251
|
+
}, 120000);
|
|
252
|
+
} else {
|
|
253
|
+
node.warn(`[warn] [hik-media-buffer:test] Nessun file multimediale generato.`);
|
|
304
254
|
}
|
|
305
255
|
updateNodeStatus();
|
|
306
256
|
}
|
|
307
257
|
|
|
308
|
-
// --- ALERT STREAM
|
|
258
|
+
// --- ALERT STREAM ---
|
|
309
259
|
function startAlertStream() {
|
|
310
260
|
if (isClosing) return;
|
|
311
261
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|