node-red-contrib-hik-media-buffer 1.1.106 → 1.1.107

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 +58 -48
  2. package/package.json +1 -1
@@ -97,25 +97,17 @@ module.exports = function(RED) {
97
97
  const referenceTime = new Date();
98
98
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
99
99
 
100
- // Costruiamo la stringa temporale RTSP Hikvision calcolando l'orario di inizio (10 secondi prima di ADESSO)
101
- // Il formato richiesto dall'RTSP Hikvision è: AAAAMMGGtHHMMSSz (tutto minuscolo)
102
- // Usiamo l'ora UTC per compatibilità nativa con i file system dei flussi video
103
- const pad = (num) => String(num).padStart(2, '0');
104
- const timeGoBack = new Date(referenceTime.getTime() - (10 * 1000)); // iniziamo 10 secondi prima dello scatto
105
-
106
- // Generiamo il timestamp con T e Z MAIUSCOLE e SENZA due punti, esattamente come piace a Hikvision
107
- const year = timeGoBack.getUTCFullYear();
108
- const month = pad(timeGoBack.getUTCMonth() + 1);
109
- const day = pad(timeGoBack.getUTCDate());
110
- const hours = pad(timeGoBack.getUTCHours());
111
- const minutes = pad(timeGoBack.getUTCMinutes());
112
- const seconds = pad(timeGoBack.getUTCSeconds());
113
-
114
- // Output pulito: 20260624T085731Z
115
- const rtspTimestamp = `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
100
+ function toXmlDate(d) {
101
+ const pad = (num) => String(num).padStart(2, '0');
102
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}Z`;
103
+ }
104
+
105
+ // Cerchiamo il record video da 2 minuti prima a 1 minuto dopo l'evento
106
+ const startTime = toXmlDate(new Date(referenceTime.getTime() - (2 * 60 * 1000)));
107
+ const endTime = toXmlDate(new Date(referenceTime.getTime() + (1 * 60 * 1000)));
116
108
 
117
- node.status({fill:"yellow", shape:"dot", text:`Estrazione media Cam ${channelID}...`});
118
- await new Promise(resolve => setTimeout(resolve, 5000));
109
+ node.status({fill:"yellow", shape:"dot", text:`Ricerca file NVR...`});
110
+ await new Promise(resolve => setTimeout(resolve, 5000)); // Aspetta che l'NVR crei il file
119
111
 
120
112
  let output = {
121
113
  tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
@@ -127,46 +119,64 @@ module.exports = function(RED) {
127
119
 
128
120
  try {
129
121
  const trackId = parseInt(channelID) * 100 + 1;
122
+ const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>00000000-0000-0000-0000-000000000000</searchID><trackList><trackID>${trackId}</trackID></trackList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>1</maxResults></CMSearchDescription>`.trim();
123
+
124
+ const tempXmlPath = path.join(baseStorage, `search_xml_${channelID}_${timestamp}.xml`);
125
+ fs.writeFileSync(tempXmlPath, searchXml, 'utf8');
126
+
127
+ const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`;
128
+ const curlSearch = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/xml" -d "@${tempXmlPath}" "${targetUrl}"`;
129
+
130
+ let xmlResponseRaw = await new Promise((resolve) => { exec(curlSearch, (err, stdout) => resolve(err ? null : stdout)); });
131
+ try { if (fs.existsSync(tempXmlPath)) fs.unlinkSync(tempXmlPath); } catch(e){}
132
+
133
+ const playbackUriMatch = xmlResponseRaw ? xmlResponseRaw.match(/<playbackURI>([^<]+)<\/playbackURI>/i) : null;
134
+
135
+ if (!playbackUriMatch) {
136
+ node.warn(`[DEBUG HTTP] Nessun file trovato nell'archivio per la cam ${channelID}.`);
137
+ updateNodeStatus();
138
+ return;
139
+ }
140
+
141
+ const playbackUri = playbackUriMatch[1].trim();
142
+ node.warn(`[DEBUG HTTP] Trovato URI d'archivio: ${playbackUri}`);
143
+
144
+ const rawVideoPath = path.join(baseStorage, `raw_${channelID}_${timestamp}.mp4`);
130
145
  const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
131
146
  const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
132
147
 
133
- // Cambiamo leggermente l'endpoint usando /channels/ che è più tollerante sul TCP d'archivio
134
- const archiveRtspUrl = `rtsp://${node.user}:${node.pass}@${node.host}:554/Streaming/channels/${channelID}01/?starttime=${rtspTimestamp}`;
135
- node.warn(`[DEBUG RTSP] Tentativo TCP blindato su: ${archiveRtspUrl}`);
148
+ // --- 1. DOWNLOAD DIRETTO VIA HTTP (ISAPI DOWNLOAD) ---
149
+ // Scarichiamo il file video originale memorizzato sull'NVR usando l'API di download nativa
150
+ node.warn(`[DEBUG HTTP] Avvio download del file originale dall'hard disk...`);
151
+ const downloadUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(playbackUri)}`;
152
+ const curlDownloadCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${downloadUrl}" -o "${rawVideoPath}"`;
136
153
 
137
- // -rtsp_transport tcp forza il tunnel stabile senza pacchetti persi
138
- // -stimeout imposta il timeout interno della connessione a 20 secondi (in microsecondi)
139
- // -fflags +genpts forza FFmpeg a ricostruire i timestamp mancanti ignorando i drop iniziali dell'NVR
140
- const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -stimeout 20000000 -i "${archiveRtspUrl}" -t 15 -fflags +genpts -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
154
+ await new Promise((resolve) => { exec(curlDownloadCmd, () => resolve()); });
141
155
 
142
- await new Promise((resolve) => {
143
- exec(ffmpegRtspCmd, { timeout: 25000 }, (err) => {
144
- if (err) node.error(`[DEBUG FFMPEG VIDEO ERRORE]: ${err.message}`);
145
- resolve();
146
- });
147
- });
156
+ if (fs.existsSync(rawVideoPath) && fs.statSync(rawVideoPath).size > 5000) {
157
+ node.warn(`[DEBUG HTTP] File scaricato (${fs.statSync(rawVideoPath).size} byte). Lo ritaglio in locale...`);
148
158
 
149
- if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
150
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
151
- fileDaCancellare.push(fixedPath);
152
- node.warn(`[DEBUG RTSP] Spezzone video d'archivio estratto con successo (${fs.statSync(fixedPath).size} byte).`);
159
+ // --- 2. TAGLIO LOCALE E CONVERSIONE AUDIO CON FFMPEG ---
160
+ // Visto che il file è sul tuo PC, FFmpeg lavora in locale (velocità istantanea, zero timeout di rete!)
161
+ const ffmpegCutCmd = `ffmpeg -y -i "${rawVideoPath}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
162
+ await new Promise((resolve) => { exec(ffmpegCutCmd, () => resolve()); });
153
163
 
154
- // --- 2. GENERAZIONE FOTO DAL VIDEO APPENA SCARICATO ---
155
- // Estraiamo il primo fotogramma utile del video reale registrato su disco dell'evento. Infallibile!
156
- node.warn(`[DEBUG RTSP] Estraggo il fotogramma dell'evento dal video registrato...`);
157
- const ffmpegImgCmd = `ffmpeg -y -i "${fixedPath}" -vframes 1 -q:v 2 "${fullImgPath}"`;
164
+ if (fs.existsSync(fixedPath)) {
165
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
166
+ fileDaCancellare.push(fixedPath);
158
167
 
159
- await new Promise((resolve) => {
160
- exec(ffmpegImgCmd, () => resolve());
161
- });
168
+ // --- 3. ESTRAZIONE FOTO DAL VIDEO REGISTRATO ---
169
+ const ffmpegImgCmd = `ffmpeg -y -i "${fixedPath}" -vframes 1 -q:v 2 "${fullImgPath}"`;
170
+ await new Promise((resolve) => { exec(ffmpegImgCmd, () => resolve()); });
162
171
 
163
- if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
164
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
165
- fileDaCancellare.push(fullImgPath);
166
- node.warn(`[DEBUG RTSP] Foto estratta con successo dal video dell'evento.`);
172
+ if (fs.existsSync(fullImgPath)) {
173
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
174
+ fileDaCancellare.push(fullImgPath);
175
+ }
167
176
  }
177
+ fileDaCancellare.push(rawVideoPath);
168
178
  } else {
169
- node.warn(`[DEBUG RTSP] Impossibile recuperare lo stream video dall'archivio.`);
179
+ node.warn(`[DEBUG HTTP] Il download del file da NVR è fallito o il file è vuoto.`);
170
180
  }
171
181
 
172
182
  // --- SPEDIZIONE PAYLOAD ---
@@ -180,7 +190,7 @@ module.exports = function(RED) {
180
190
  }
181
191
 
182
192
  } catch (e) {
183
- node.error(`Errore Critico nel recupero RTSP diretto: ${e.message}`);
193
+ node.error(`Errore nel download HTTP d'archivio: ${e.message}`);
184
194
  }
185
195
  updateNodeStatus();
186
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.106",
3
+ "version": "1.1.107",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",