node-red-contrib-hik-media-buffer 1.1.63 → 1.1.65
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 +100 -65
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -173,84 +173,119 @@ module.exports = function(RED) {
|
|
|
173
173
|
let fileDaCancellare = [];
|
|
174
174
|
|
|
175
175
|
try {
|
|
176
|
-
|
|
176
|
+
for (let t of tracks) {
|
|
177
177
|
node.warn(`[warn] [hik-media-buffer:test] [STEP 4] Elaborazione traccia ${t.label} (ID: ${t.id})`);
|
|
178
|
+
|
|
179
|
+
const evMinuscolo = eventoNVR.toLowerCase();
|
|
178
180
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// Struttura rigida sequenziale approvata dall'NVR (con l'errore di battitura nativo searchResultPostion)
|
|
183
|
-
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><contentTypeList><contentType>${contentTypeValue}</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>${descriptorTarget}</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
184
|
-
|
|
185
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
|
|
186
|
-
try {
|
|
187
|
-
const resSearch = await camAuth.request({
|
|
188
|
-
method: 'POST',
|
|
189
|
-
url: `${baseUrl}/search`,
|
|
190
|
-
data: searchXml,
|
|
191
|
-
headers: { "Content-Type": "application/xml" }
|
|
192
|
-
});
|
|
181
|
+
if (t.isFoto) {
|
|
182
|
+
// --- SE È LA FOTO: USA IL TUO XML ORIGINALE IDENTICO ---
|
|
183
|
+
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/${evMinuscolo}</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
193
184
|
|
|
194
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 4.
|
|
185
|
+
node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST XML /search per FOTO. XML:\n${searchXml}`);
|
|
195
186
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
187
|
+
try {
|
|
188
|
+
const resSearch = await camAuth.request({
|
|
189
|
+
method: 'POST',
|
|
190
|
+
url: `${baseUrl}/search`,
|
|
191
|
+
data: searchXml,
|
|
192
|
+
headers: { "Content-Type": "application/xml" }
|
|
193
|
+
});
|
|
202
194
|
|
|
203
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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" }
|
|
209
237
|
});
|
|
210
238
|
|
|
211
|
-
|
|
212
|
-
|
|
239
|
+
const resDataStr = JSON.stringify(resSearchVideo.data);
|
|
240
|
+
const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
|
|
213
241
|
|
|
214
|
-
if (
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
218
|
-
fileDaCancellare.push(fullImgPath);
|
|
219
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 5.3] Immagine salvata e convertita in Base64.`);
|
|
220
|
-
} else {
|
|
221
|
-
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
222
|
-
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
223
|
-
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
224
|
-
fs.writeFileSync(rawPath, buffer);
|
|
225
|
-
|
|
226
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 5.3] Video grezzo salvato. Avvio conversione FFMPEG...`);
|
|
227
|
-
await new Promise((resolve) => {
|
|
228
|
-
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
229
|
-
if (!err && fs.existsSync(fixedPath)) {
|
|
230
|
-
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
231
|
-
fileDaCancellare.push(fixedPath);
|
|
232
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 5.4] Conversione FFMPEG completata con successo.`);
|
|
233
|
-
} else {
|
|
234
|
-
node.warn(`[warn] [hik-media-buffer:test] [STEP 5.4] Errore FFMPEG (uso il video grezzo di backup): ${err ? err.message : 'File mancante'}`);
|
|
235
|
-
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
236
|
-
}
|
|
237
|
-
fileDaCancellare.push(rawPath);
|
|
238
|
-
resolve();
|
|
239
|
-
});
|
|
240
|
-
});
|
|
242
|
+
if (videoUriMatch) {
|
|
243
|
+
let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
|
|
244
|
+
await scaricaIlFile(rawVideoUri, t.isFoto);
|
|
241
245
|
}
|
|
242
|
-
}
|
|
243
|
-
node.
|
|
244
|
-
}
|
|
245
|
-
} catch (searchError) {
|
|
246
|
-
let errorDetails = searchError.message;
|
|
247
|
-
if (searchError.response) {
|
|
248
|
-
errorDetails += ` | Status: ${searchError.response.status} | Dati Risposta: ${JSON.stringify(searchError.response.data)}`;
|
|
246
|
+
} catch (err) {
|
|
247
|
+
node.error(`[error] [hik-media-buffer:test] Errore VIDEO JSON: ${err.message}`);
|
|
249
248
|
}
|
|
250
|
-
node.error(`[error] [hik-media-buffer:test] [CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
|
|
251
249
|
}
|
|
252
250
|
}
|
|
253
251
|
|
|
252
|
+
// Funzione interna di supporto per evitare duplicazione di codice di download
|
|
253
|
+
async function scaricaIlFile(playbackUri, isFoto) {
|
|
254
|
+
node.warn(`[warn] [hik-media-buffer:test] [STEP 5] Scarico traccia. URI: ${playbackUri}`);
|
|
255
|
+
const resDown = await camAuth.request({
|
|
256
|
+
method: 'GET',
|
|
257
|
+
url: `${baseUrl}/download`,
|
|
258
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${playbackUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
259
|
+
responseType: 'arraybuffer'
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
let buffer = Buffer.from(resDown.data);
|
|
263
|
+
if (isFoto) {
|
|
264
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
265
|
+
fs.writeFileSync(fullImgPath, buffer);
|
|
266
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
267
|
+
fileDaCancellare.push(fullImgPath);
|
|
268
|
+
} else {
|
|
269
|
+
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
270
|
+
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
271
|
+
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
272
|
+
fs.writeFileSync(rawPath, buffer);
|
|
273
|
+
|
|
274
|
+
await new Promise((resolve) => {
|
|
275
|
+
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
276
|
+
if (!err && fs.existsSync(fixedPath)) {
|
|
277
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
278
|
+
fileDaCancellare.push(fixedPath);
|
|
279
|
+
} else {
|
|
280
|
+
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
281
|
+
}
|
|
282
|
+
fileDaCancellare.push(rawPath);
|
|
283
|
+
resolve();
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
254
289
|
if (output.foto_base64 || output.video_base64) {
|
|
255
290
|
node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
|
|
256
291
|
node.send({ payload: output });
|