node-red-contrib-hik-media-buffer 1.1.105 → 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 -45
  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,43 +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
- const archiveRtspUrl = `rtsp://${node.user}:${node.pass}@${node.host}:554/Streaming/tracks/${trackId}/?starttime=${rtspTimestamp}`;
134
- node.warn(`[DEBUG RTSP] Chiamata finale: ${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}"`;
135
153
 
136
- // Via il flag -rtsp_transport tcp per sbloccare il timeout UDP
137
- const ffmpegRtspCmd = `ffmpeg -y -i "${archiveRtspUrl}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
154
+ await new Promise((resolve) => { exec(curlDownloadCmd, () => resolve()); });
138
155
 
139
- await new Promise((resolve) => {
140
- exec(ffmpegRtspCmd, { timeout: 25000 }, (err) => {
141
- if (err) node.error(`[DEBUG FFMPEG VIDEO ERRORE]: ${err.message}`);
142
- resolve();
143
- });
144
- });
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...`);
145
158
 
146
- if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
147
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
148
- fileDaCancellare.push(fixedPath);
149
- 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()); });
150
163
 
151
- // --- 2. GENERAZIONE FOTO DAL VIDEO APPENA SCARICATO ---
152
- // Estraiamo il primo fotogramma utile del video reale registrato su disco dell'evento. Infallibile!
153
- node.warn(`[DEBUG RTSP] Estraggo il fotogramma dell'evento dal video registrato...`);
154
- 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);
155
167
 
156
- await new Promise((resolve) => {
157
- exec(ffmpegImgCmd, () => resolve());
158
- });
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()); });
159
171
 
160
- if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
161
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
162
- fileDaCancellare.push(fullImgPath);
163
- 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
+ }
164
176
  }
177
+ fileDaCancellare.push(rawVideoPath);
165
178
  } else {
166
- 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.`);
167
180
  }
168
181
 
169
182
  // --- SPEDIZIONE PAYLOAD ---
@@ -177,7 +190,7 @@ module.exports = function(RED) {
177
190
  }
178
191
 
179
192
  } catch (e) {
180
- node.error(`Errore Critico nel recupero RTSP diretto: ${e.message}`);
193
+ node.error(`Errore nel download HTTP d'archivio: ${e.message}`);
181
194
  }
182
195
  updateNodeStatus();
183
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.105",
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",