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

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 +87 -54
  2. package/package.json +1 -1
@@ -97,18 +97,17 @@ module.exports = function(RED) {
97
97
  const referenceTime = new Date();
98
98
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
99
99
 
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)));
108
-
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
100
+ // Costruiamo le date giornaliere identiche a quelle viste su Chrome
101
+ const pad = (num) => String(num).padStart(2, '0');
102
+ const year = referenceTime.getFullYear();
103
+ const month = pad(referenceTime.getMonth() + 1);
104
+ const day = pad(referenceTime.getDate());
105
+
106
+ const startTime = `${year}-${month}-${day}T00:00:00 01:00`;
107
+ const endTime = `${year}-${month}-${day}T23:59:59 01:00`;
111
108
 
109
+ node.status({fill:"yellow", shape:"dot", text:`Invio query protetta...`});
110
+
112
111
  let output = {
113
112
  tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
114
113
  ip_telecamera: null, tipo_evento: evento, timestamp_epoch: timestamp,
@@ -118,68 +117,102 @@ module.exports = function(RED) {
118
117
  let fileDaCancellare = [];
119
118
 
120
119
  try {
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();
120
+ // --- GENERAZIONE HASH DI SESSIONE LOCALE ---
121
+ // Generiamo un MD5 casuale per bypassare il controllo di uguaglianza tra Cookie e Sessiontag
122
+ const fakeSessionToken = require('crypto').createHash('md5').update(String(Math.random() + Date.now())).digest('hex');
123
+ const cookieKeyName = `WebSession_${require('crypto').createHash('md5').update(node.host).digest('hex').substring(0, 10)}`;
124
+
125
+ node.warn(`[DEBUG SESSION] Simulazione Header -> ${cookieKeyName}=${fakeSessionToken}`);
123
126
 
124
- const tempXmlPath = path.join(baseStorage, `search_xml_${channelID}_${timestamp}.xml`);
125
- fs.writeFileSync(tempXmlPath, searchXml, 'utf8');
127
+ const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
128
+ var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
129
+ return v.toString(16).toUpperCase();
130
+ });
126
131
 
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}"`;
132
+ const searchPayload = {
133
+ "EventSearchDescription": {
134
+ "searchID": dynamicSearchId,
135
+ "searchResultPosition": 0,
136
+ "maxResults": 30,
137
+ "timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
138
+ "type": "all",
139
+ "channels": [ parseInt(channelID) ],
140
+ "eventType": "behavior",
141
+ "behavior": { "behaviorEventType": evento.toLowerCase() }
142
+ }
143
+ };
129
144
 
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){}
145
+ const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
146
+ fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
132
147
 
133
- const playbackUriMatch = xmlResponseRaw ? xmlResponseRaw.match(/<playbackURI>([^<]+)<\/playbackURI>/i) : null;
148
+ const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
149
+
150
+ // Compiliamo il comando cURL inserendo gli header emersi dall'analisi di Chrome
151
+ // Usiamo sia il content-type form-urlencoded sia la coppia speculare cookie/sessiontag
152
+ const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" ` +
153
+ `-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" ` +
154
+ `-H "X-Requested-With: XMLHttpRequest" ` +
155
+ `-H "sessiontag: ${fakeSessionToken}" ` +
156
+ `-b "${cookieKeyName}=${fakeSessionToken}; updatePlugin=false" ` +
157
+ `-d "@${tempJsonPath}" "${targetUrl}"`;
158
+
159
+ let jsonResponseRaw = await new Promise((resolve) => { exec(curlCommand, (err, stdout) => resolve(err ? null : stdout)); });
134
160
 
135
- if (!playbackUriMatch) {
136
- node.warn(`[DEBUG HTTP] Nessun file trovato nell'archivio per la cam ${channelID}.`);
161
+ try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
162
+
163
+ if (!jsonResponseRaw) {
164
+ node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
137
165
  updateNodeStatus();
138
166
  return;
139
167
  }
140
168
 
141
- const playbackUri = playbackUriMatch[1].trim();
142
- node.warn(`[DEBUG HTTP] Trovato URI d'archivio: ${playbackUri}`);
169
+ const searchResult = JSON.parse(jsonResponseRaw);
170
+ const matches = searchResult?.EventSearchResult?.matchList || [];
143
171
 
144
- const rawVideoPath = path.join(baseStorage, `raw_${channelID}_${timestamp}.mp4`);
145
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
146
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
172
+ if (matches.length === 0) {
173
+ node.warn(`[DEBUG JSON] Zero match trovati nell'archivio protetto.`);
174
+ updateNodeStatus();
175
+ return;
176
+ }
147
177
 
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}"`;
178
+ // Se superiamo il blocco, estraiamo i file storici
179
+ const matchItem = matches[0];
180
+ const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
181
+
182
+ if (playbackUri) {
183
+ node.warn(`[DEBUG JSON] File individuato: ${playbackUri}`);
184
+
185
+ // --- DOWNLOAD FOTO REGISTRATA ---
186
+ let pictureUrlPath = null;
187
+ const urlParamsMatch = playbackUri.match(/\?(.*)/);
188
+ if (urlParamsMatch) {
189
+ const trackI = (channelID * 100 + 3).toString();
190
+ pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
191
+ }
153
192
 
154
- await new Promise((resolve) => { exec(curlDownloadCmd, () => resolve()); });
193
+ if (pictureUrlPath) {
194
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
195
+ const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" -b "${cookieKeyName}=${fakeSessionToken}" -H "sessiontag: ${fakeSessionToken}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
196
+ await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
197
+ if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
198
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
199
+ fileDaCancellare.push(fullImgPath);
200
+ }
201
+ }
155
202
 
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...`);
203
+ // --- DOWNLOAD VIDEO REGISTRATO VIA FFMPEG ---
204
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
205
+ const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
206
+ const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
158
207
 
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()); });
208
+ await new Promise((resolve) => { exec(ffmpegRtspCmd, { timeout: 25000 }, () => resolve()); });
163
209
 
164
- if (fs.existsSync(fixedPath)) {
210
+ if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
165
211
  output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
166
212
  fileDaCancellare.push(fixedPath);
167
-
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()); });
171
-
172
- if (fs.existsSync(fullImgPath)) {
173
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
174
- fileDaCancellare.push(fullImgPath);
175
- }
176
213
  }
177
- fileDaCancellare.push(rawVideoPath);
178
- } else {
179
- node.warn(`[DEBUG HTTP] Il download del file da NVR è fallito o il file è vuoto.`);
180
214
  }
181
215
 
182
- // --- SPEDIZIONE PAYLOAD ---
183
216
  if (output.foto_base64 || output.video_base64) {
184
217
  node.send({ payload: output });
185
218
  setTimeout(() => {
@@ -190,7 +223,7 @@ module.exports = function(RED) {
190
223
  }
191
224
 
192
225
  } catch (e) {
193
- node.error(`Errore nel download HTTP d'archivio: ${e.message}`);
226
+ node.error(`Errore query speculare: ${e.message}`);
194
227
  }
195
228
  updateNodeStatus();
196
229
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.107",
3
+ "version": "1.1.109",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",