node-red-contrib-hik-media-buffer 1.1.65 → 1.1.66
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 +93 -147
- 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,42 @@ 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 ---
|
|
115
109
|
async function downloadMedia(evento, channelID) {
|
|
116
|
-
node.warn(`[warn] [hik-media-buffer:test] [
|
|
110
|
+
node.warn(`[warn] [hik-media-buffer:test] [TRIGGER] Ricevuto evento. Evento: ${evento}, Canale originario: ${channelID}`);
|
|
117
111
|
|
|
118
112
|
const nowTime = Date.now();
|
|
119
113
|
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
|
-
}
|
|
114
|
+
if (nowTime - lastTriggerTime[channelID] < 5000) return;
|
|
124
115
|
lastTriggerTime[channelID] = nowTime;
|
|
125
116
|
|
|
126
117
|
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
127
118
|
const referenceTime = new Date();
|
|
128
119
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
129
120
|
|
|
130
|
-
//
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
121
|
+
// Orario formattato per il database JSON dell'NVR (Ora locale con indicazione fuso es. 02:00)
|
|
122
|
+
const formatJsonDate = (d) => {
|
|
123
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
124
|
+
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())} 02:00`;
|
|
125
|
+
};
|
|
126
|
+
const startTimeJson = formatJsonDate(new Date(referenceTime.getTime() - (15 * 1000)));
|
|
127
|
+
const endTimeJson = formatJsonDate(new Date(referenceTime.getTime() + (15 * 1000)));
|
|
134
128
|
|
|
135
129
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
136
130
|
const nomeCamera = await getCameraName(channelID);
|
|
137
131
|
|
|
138
|
-
node.warn(`[warn] [hik-media-buffer:test]
|
|
132
|
+
node.warn(`[warn] [hik-media-buffer:test] Attesa scrittura file su Hard Disk (6s)...`);
|
|
139
133
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
140
134
|
|
|
141
135
|
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
|
-
}
|
|
136
|
+
const evMinuscolo = evento.toLowerCase();
|
|
150
137
|
|
|
151
138
|
let output = {
|
|
152
139
|
tipo_messaggio: "evento",
|
|
153
140
|
nome_cliente: node.name,
|
|
154
141
|
nome_telecamera: nomeCamera,
|
|
155
142
|
ip_telecamera: node.host,
|
|
156
|
-
tipo_evento:
|
|
143
|
+
tipo_evento: evMinuscolo.includes("linedetection") ? "LineDetection" : "FieldDetection",
|
|
157
144
|
timestamp_epoch: timestamp,
|
|
158
145
|
stato_telecamera: "ONLINE",
|
|
159
146
|
channel: channelID.toString(),
|
|
@@ -162,150 +149,109 @@ module.exports = function(RED) {
|
|
|
162
149
|
};
|
|
163
150
|
|
|
164
151
|
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
152
|
let fileDaCancellare = [];
|
|
174
153
|
|
|
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
|
-
}
|
|
154
|
+
// --- STRUTTURA JSON DALL'ISPEZIONA NVR NATIVO ---
|
|
155
|
+
const searchJsonPayload = {
|
|
156
|
+
"EventSearchDescription": {
|
|
157
|
+
"searchID": "BA80CAF2-A1E5-42E3-8624-529C3D6665D7",
|
|
158
|
+
"searchResultPosition": 0,
|
|
159
|
+
"maxResults": 30,
|
|
160
|
+
"timeSpanList": [{
|
|
161
|
+
"startTime": startTimeJson,
|
|
162
|
+
"endTime": endTimeJson
|
|
163
|
+
}],
|
|
164
|
+
"type": "all",
|
|
165
|
+
"channels": [parsedChannel],
|
|
166
|
+
"eventType": "behavior",
|
|
167
|
+
"behavior": {
|
|
168
|
+
"behaviorEventType": evMinuscolo
|
|
249
169
|
}
|
|
250
170
|
}
|
|
171
|
+
};
|
|
251
172
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
const
|
|
256
|
-
method: '
|
|
257
|
-
url: `${baseUrl}/
|
|
258
|
-
data:
|
|
259
|
-
|
|
173
|
+
node.warn(`[warn] [hik-media-buffer:test] Invio query JSON a eventRecordSearch per canale ${parsedChannel}...`);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const resSearchVideo = await camAuth.request({
|
|
177
|
+
method: 'POST',
|
|
178
|
+
url: `${baseUrl}/eventRecordSearch?format=json`,
|
|
179
|
+
data: searchJsonPayload,
|
|
180
|
+
headers: { "Content-Type": "application/json" }
|
|
260
181
|
});
|
|
261
182
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
183
|
+
const resDataStr = JSON.stringify(resSearchVideo.data);
|
|
184
|
+
// Estrae l'RTSP playbackURI o l'endpoint di download dal JSON di risposta
|
|
185
|
+
const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
186
|
+
|
|
187
|
+
if (videoUriMatch) {
|
|
188
|
+
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
189
|
+
node.warn(`[warn] [hik-media-buffer:test] Trovato playbackURI video nel JSON: ${rawVideoUri}`);
|
|
190
|
+
|
|
191
|
+
// Scarichiamo il filmato dall'NVR
|
|
192
|
+
const resDownVideo = await camAuth.request({
|
|
193
|
+
method: 'GET',
|
|
194
|
+
url: `${baseUrl}/download`,
|
|
195
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawVideoUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
196
|
+
responseType: 'arraybuffer'
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
let videoBuffer = Buffer.from(resDownVideo.data);
|
|
200
|
+
if (videoBuffer.slice(0, 4).toString() === 'IMKH') videoBuffer = videoBuffer.slice(40);
|
|
201
|
+
|
|
270
202
|
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
271
203
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
272
|
-
|
|
204
|
+
const extractedImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
273
205
|
|
|
206
|
+
fs.writeFileSync(rawPath, videoBuffer);
|
|
207
|
+
fileDaCancellare.push(rawPath);
|
|
208
|
+
|
|
209
|
+
node.warn(`[warn] [hik-media-buffer:test] Esecuzione FFMPEG simultanea (Estrazione frame JPG + conversione video MP4)...`);
|
|
274
210
|
await new Promise((resolve) => {
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
211
|
+
// FFMPEG estrae il primo secondo in JPG e indicizza il video in FastStart in un colpo solo
|
|
212
|
+
exec(`ffmpeg -y -i "${rawPath}" -ss 00:00:01 -vframes 1 "${extractedImgPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
213
|
+
if (!err) {
|
|
214
|
+
if (fs.existsSync(extractedImgPath)) {
|
|
215
|
+
output.foto_base64 = fs.readFileSync(extractedImgPath, { encoding: 'base64' });
|
|
216
|
+
fileDaCancellare.push(extractedImgPath);
|
|
217
|
+
node.warn(`[warn] [hik-media-buffer:test] Foto estratta dal video correttamente.`);
|
|
218
|
+
}
|
|
219
|
+
if (fs.existsSync(fixedPath)) {
|
|
220
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
221
|
+
fileDaCancellare.push(fixedPath);
|
|
222
|
+
node.warn(`[warn] [hik-media-buffer:test] Video ottimizzato convertito correttamente.`);
|
|
223
|
+
}
|
|
279
224
|
} else {
|
|
225
|
+
node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, invio video originale grezzo: ${err.message}`);
|
|
280
226
|
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
281
227
|
}
|
|
282
|
-
fileDaCancellare.push(rawPath);
|
|
283
228
|
resolve();
|
|
284
229
|
});
|
|
285
230
|
});
|
|
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
231
|
} else {
|
|
299
|
-
node.warn(`[warn] [hik-media-buffer:test]
|
|
232
|
+
node.warn(`[warn] [hik-media-buffer:test] Nessun file video trovato nel database JSON dell'NVR.`);
|
|
300
233
|
}
|
|
234
|
+
} catch (videoErr) {
|
|
235
|
+
node.error(`[error] [hik-media-buffer:test] Errore critico durante la ricerca JSON: ${videoErr.message}`);
|
|
236
|
+
}
|
|
301
237
|
|
|
302
|
-
|
|
303
|
-
|
|
238
|
+
// --- SPEDIZIONE VERSO PYTHON ---
|
|
239
|
+
if (output.foto_base64 || output.video_base64) {
|
|
240
|
+
node.warn(`[warn] [hik-media-buffer:test] Spedizione payload multimediale completata!`);
|
|
241
|
+
node.send({ payload: output });
|
|
242
|
+
|
|
243
|
+
setTimeout(() => {
|
|
244
|
+
for (let file of fileDaCancellare) {
|
|
245
|
+
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
246
|
+
}
|
|
247
|
+
}, 120000);
|
|
248
|
+
} else {
|
|
249
|
+
node.warn(`[warn] [hik-media-buffer:test] Nessun file multimediale generato.`);
|
|
304
250
|
}
|
|
305
251
|
updateNodeStatus();
|
|
306
252
|
}
|
|
307
253
|
|
|
308
|
-
// --- ALERT STREAM
|
|
254
|
+
// --- ALERT STREAM ---
|
|
309
255
|
function startAlertStream() {
|
|
310
256
|
if (isClosing) return;
|
|
311
257
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|