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.
Files changed (2) hide show
  1. package/hik-media-buffer.js +100 -65
  2. package/package.json +1 -1
@@ -173,84 +173,119 @@ module.exports = function(RED) {
173
173
  let fileDaCancellare = [];
174
174
 
175
175
  try {
176
- for (let t of tracks) {
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
- const contentTypeValue = t.isFoto ? "metadata" : "video";
180
- const descriptorTarget = t.isFoto ? `recordType.meta.hikvision.com/${eventoNVR.toLowerCase()}` : `recordType.meta.hikvision.com/timing`;
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.2] Risposta ricevuta da /search per traccia ${t.id}: Status ${resSearch.status}`);
185
+ node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST XML /search per FOTO. XML:\n${searchXml}`);
195
186
 
196
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
197
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
198
-
199
- if (uriMatch) {
200
- const rawUri = uriMatch[1].replace(/&amp;/g, '&');
201
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5] Trovato playbackURI per traccia ${t.id}: ${rawUri}`);
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
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.1] Invio richiesta GET /download per traccia ${t.id}`);
204
- const resDown = await camAuth.request({
205
- method: 'GET',
206
- url: `${baseUrl}/download`,
207
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
208
- responseType: 'arraybuffer'
195
+ let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
196
+ const uriMatch = xml.match(/<playbackURI>([^<]+)</);
197
+ if (uriMatch) {
198
+ const rawUri = uriMatch[1].replace(/&amp;/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
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.2] Scaricati ${resDown.data.byteLength} byte per traccia ${t.id}`);
212
- let buffer = Buffer.from(resDown.data);
239
+ const resDataStr = JSON.stringify(resSearchVideo.data);
240
+ const videoUriMatch = resDataStr.match(/rtsp:\/\/[^"]+/i) || resDataStr.match(/\/ISAPI\/ContentMgmt\/download[^"]+/i);
213
241
 
214
- if (t.isFoto) {
215
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
216
- fs.writeFileSync(fullImgPath, buffer);
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
- } else {
243
- node.warn(`[warn] [hik-media-buffer:test] [STEP 4.3] Nessun playbackURI trovato nel documento di risposta XML per traccia ${t.id}. XML di risposta:\n${resSearch.data}`);
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, '&amp;')}</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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.63",
3
+ "version": "1.1.65",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",