node-red-contrib-hik-media-buffer 1.1.64 → 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 +102 -119
- 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,113 +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
|
-
|
|
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
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
};
|
|
178
172
|
|
|
179
|
-
|
|
180
|
-
// Cambia solo il contenuto testuale del descriptor per puntare all'NVR in minuscolo e senza i doppi slash che facevano scattare il "two root tags"
|
|
181
|
-
const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${t.id}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>100</maxResults><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
173
|
+
node.warn(`[warn] [hik-media-buffer:test] Invio query JSON a eventRecordSearch per canale ${parsedChannel}...`);
|
|
182
174
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 4.2] Risposta ricevuta da /search per traccia ${t.id}: Status ${resSearch.status}`);
|
|
193
|
-
|
|
194
|
-
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
195
|
-
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
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" }
|
|
181
|
+
});
|
|
196
182
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
+
});
|
|
208
198
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
199
|
+
let videoBuffer = Buffer.from(resDownVideo.data);
|
|
200
|
+
if (videoBuffer.slice(0, 4).toString() === 'IMKH') videoBuffer = videoBuffer.slice(40);
|
|
201
|
+
|
|
202
|
+
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
203
|
+
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
204
|
+
const extractedImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
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)...`);
|
|
210
|
+
await new Promise((resolve) => {
|
|
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
|
+
}
|
|
218
224
|
} else {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
222
|
-
fs.writeFileSync(rawPath, buffer);
|
|
223
|
-
|
|
224
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 5.3] Video grezzo salvato. Avvio conversione FFMPEG...`);
|
|
225
|
-
await new Promise((resolve) => {
|
|
226
|
-
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
227
|
-
if (!err && fs.existsSync(fixedPath)) {
|
|
228
|
-
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
229
|
-
fileDaCancellare.push(fixedPath);
|
|
230
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 5.4] Conversione FFMPEG completata con successo.`);
|
|
231
|
-
} else {
|
|
232
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 5.4] Errore FFMPEG (uso il video grezzo di backup): ${err ? err.message : 'File mancante'}`);
|
|
233
|
-
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
234
|
-
}
|
|
235
|
-
fileDaCancellare.push(rawPath);
|
|
236
|
-
resolve();
|
|
237
|
-
});
|
|
238
|
-
});
|
|
225
|
+
node.warn(`[warn] [hik-media-buffer:test] Errore FFMPEG, invio video originale grezzo: ${err.message}`);
|
|
226
|
+
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
239
227
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
} catch (searchError) {
|
|
244
|
-
let errorDetails = searchError.message;
|
|
245
|
-
if (searchError.response) {
|
|
246
|
-
errorDetails += ` | Status: ${searchError.response.status} | Dati Risposta: ${JSON.stringify(searchError.response.data)}`;
|
|
247
|
-
}
|
|
248
|
-
node.error(`[error] [hik-media-buffer:test] [CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (output.foto_base64 || output.video_base64) {
|
|
253
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
|
|
254
|
-
node.send({ payload: output });
|
|
255
|
-
|
|
256
|
-
setTimeout(() => {
|
|
257
|
-
for (let file of fileDaCancellare) {
|
|
258
|
-
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
259
|
-
}
|
|
260
|
-
}, 120000);
|
|
228
|
+
resolve();
|
|
229
|
+
});
|
|
230
|
+
});
|
|
261
231
|
} else {
|
|
262
|
-
node.warn(`[warn] [hik-media-buffer:test]
|
|
232
|
+
node.warn(`[warn] [hik-media-buffer:test] Nessun file video trovato nel database JSON dell'NVR.`);
|
|
263
233
|
}
|
|
234
|
+
} catch (videoErr) {
|
|
235
|
+
node.error(`[error] [hik-media-buffer:test] Errore critico durante la ricerca JSON: ${videoErr.message}`);
|
|
236
|
+
}
|
|
264
237
|
|
|
265
|
-
|
|
266
|
-
|
|
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.`);
|
|
267
250
|
}
|
|
268
251
|
updateNodeStatus();
|
|
269
252
|
}
|
|
270
253
|
|
|
271
|
-
// --- ALERT STREAM
|
|
254
|
+
// --- ALERT STREAM ---
|
|
272
255
|
function startAlertStream() {
|
|
273
256
|
if (isClosing) return;
|
|
274
257
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|