node-red-contrib-hik-media-buffer 1.1.64 → 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 -63
  2. package/package.json +1 -1
@@ -173,82 +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
- // Questo è il TUO XML ORIGINALE, non è stato toccato un singolo tag.
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>`;
182
-
183
- node.warn(`[warn] [hik-media-buffer:test] [STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
184
- try {
185
- const resSearch = await camAuth.request({
186
- method: 'POST',
187
- url: `${baseUrl}/search`,
188
- data: searchXml,
189
- headers: { "Content-Type": "application/xml" }
190
- });
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>`;
191
184
 
192
- 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}`);
193
186
 
194
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
195
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
196
-
197
- if (uriMatch) {
198
- const rawUri = uriMatch[1].replace(/&amp;/g, '&');
199
- 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
+ });
200
194
 
201
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.1] Invio richiesta GET /download per traccia ${t.id}`);
202
- const resDown = await camAuth.request({
203
- method: 'GET',
204
- url: `${baseUrl}/download`,
205
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
206
- 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" }
207
237
  });
208
238
 
209
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.2] Scaricati ${resDown.data.byteLength} byte per traccia ${t.id}`);
210
- 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);
211
241
 
212
- if (t.isFoto) {
213
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
214
- fs.writeFileSync(fullImgPath, buffer);
215
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
216
- fileDaCancellare.push(fullImgPath);
217
- node.warn(`[warn] [hik-media-buffer:test] [STEP 5.3] Immagine salvata e convertita in Base64.`);
218
- } else {
219
- if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
220
- const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
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
- });
242
+ if (videoUriMatch) {
243
+ let rawVideoUri = videoUriMatch[0].replace(/\\/g, '');
244
+ await scaricaIlFile(rawVideoUri, t.isFoto);
239
245
  }
240
- } else {
241
- 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}`);
246
+ } catch (err) {
247
+ node.error(`[error] [hik-media-buffer:test] Errore VIDEO JSON: ${err.message}`);
242
248
  }
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
249
  }
250
250
  }
251
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
+
252
289
  if (output.foto_base64 || output.video_base64) {
253
290
  node.warn(`[warn] [hik-media-buffer:test] [STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
254
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.64",
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",