node-red-contrib-hik-media-buffer 1.1.134 β†’ 1.1.136

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 +42 -68
  2. package/package.json +1 -1
@@ -32,7 +32,7 @@ module.exports = function(RED) {
32
32
 
33
33
  node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
34
34
 
35
- // --- FORMATTAZIONE DATA LOCALE (Risolve il bug delle 2 ore di differenza dell'NVR) ---
35
+ // --- FORMATTAZIONE DATA LOCALE ---
36
36
  function toHikSearchDate(d) {
37
37
  const pad = (num) => String(num).padStart(2, '0');
38
38
 
@@ -43,15 +43,13 @@ module.exports = function(RED) {
43
43
  const minutes = pad(d.getMinutes());
44
44
  const seconds = pad(d.getSeconds());
45
45
 
46
- // Costruisce la stringa mantenendo l'ora locale del PC/Node-RED (es. 09:53:19Z)
47
46
  return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`;
48
47
  }
49
48
 
50
- // --- 1. PRENDE IL NOME REALE DELLA TELECAMERA DAL DIGITAL PROXY ---
49
+ // --- 1. PRENDE IL NOME REALE DELLA TELECAMERA ---
51
50
  async function getCameraName(channelID) {
52
51
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
53
52
  try {
54
- // In accordo con la pag. 58 della documentazione, interroghiamo l'InputProxy del canale digitale
55
53
  const res = await nvrAuth.request({
56
54
  method: 'GET',
57
55
  url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/${channelID}`,
@@ -59,7 +57,6 @@ module.exports = function(RED) {
59
57
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
60
58
  });
61
59
  const data = res.data.toString();
62
- // Estraiamo il tag <name> nativo del canale digitale
63
60
  const match = data.match(/<name>([^<]+)<\/name>/i);
64
61
  if (match && match[1]) return match[1].trim();
65
62
  return `Canale ${channelID}`;
@@ -73,7 +70,6 @@ module.exports = function(RED) {
73
70
  if (isClosing || !nvrOnline) return;
74
71
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
75
72
  try {
76
- // Chiamata all'endpoint di status dei canali proxy (Pag. 322)
77
73
  const res = await nvrAuth.request({
78
74
  method: 'GET',
79
75
  url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
@@ -82,7 +78,6 @@ module.exports = function(RED) {
82
78
  });
83
79
 
84
80
  const xml = res.data.toString();
85
- // Separiamo i singoli blocchi <InputProxyChannelStatus>
86
81
  const channelBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/g) || [];
87
82
 
88
83
  for (let block of channelBlocks) {
@@ -91,21 +86,16 @@ module.exports = function(RED) {
91
86
 
92
87
  if (idMatch) {
93
88
  const ch = idMatch[1].trim();
94
- // Estraiamo lo stato online nativo (true/false)
95
89
  const isOnline = onlineMatch ? onlineMatch[1].trim().toLowerCase() === "true" : false;
96
90
 
97
91
  if (isOnline && statoCamera[ch] === false) {
98
- // 🌟 RECUPERO REALE DEL NOME: Usiamo getCameraName per avere il nome corretto OSD ("ufficio")
99
92
  const nomeOnline = await getCameraName(ch);
100
-
101
93
  node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: node.host, channel: ch, msg: "Camera ripristinata" } });
102
94
  statoCamera[ch] = true;
103
95
  } else if (!isOnline && statoCamera[ch] === true) {
104
- // Per l'offline usiamo un fallback sul canale, dato che la cam non risponderebbe a getCameraName
105
96
  node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera ${ch}`, ip_telecamera: node.host, channel: ch, msg: "Camera non raggiungibile" } });
106
97
  statoCamera[ch] = false;
107
98
  } else if (statoCamera[ch] === undefined) {
108
- // Memorizzazione iniziale dello stato al boot
109
99
  statoCamera[ch] = isOnline;
110
100
  }
111
101
  }
@@ -129,14 +119,14 @@ module.exports = function(RED) {
129
119
 
130
120
  const heartbeatInterval = setInterval(checkCameras, 15000);
131
121
 
132
- // --- 3. DOWNLOAD MEDIA CON DELAY E FORMATO STRINGA CORRETTO ---
122
+ // --- 3. DOWNLOAD MEDIA FLUIDO (Scorciatoia Diretta da JSON) ---
133
123
  async function downloadMedia(evento, channelID) {
134
124
  const nowTime = Date.now();
135
125
  if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
136
126
  if (nowTime - lastTriggerTime[channelID] < 5000) return;
137
127
  lastTriggerTime[channelID] = nowTime;
138
128
 
139
- node.status({fill:"yellow", shape:"dot", text:`Attesa scrittura NVR (6s)...`});
129
+ node.status({fill:"yellow", shape:"dot", text:`Attesa (6s)...`});
140
130
  await new Promise(resolve => setTimeout(resolve, 6000));
141
131
 
142
132
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
@@ -147,17 +137,12 @@ module.exports = function(RED) {
147
137
  const nomeCamera = await getCameraName(channelID);
148
138
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
149
139
 
150
- const inizioFinestra = new Date(referenceTime.getTime() - (10 * 1000));
151
- const fineFinestra = new Date(referenceTime.getTime() + (10 * 1000));
140
+ const inizioFinestra = new Date(referenceTime.getTime() - (20 * 1000));
141
+ const fineFinestra = new Date(referenceTime.getTime() + (20 * 1000));
152
142
 
153
- const startVideoSearch = toHikSearchDate(inizioFinestra);
154
- const endVideoSearch = toHikSearchDate(fineFinestra);
155
-
156
143
  const startFotoSearch = inizioFinestra.toISOString().split('.')[0] + "Z";
157
144
  const endFotoSearch = fineFinestra.toISOString().split('.')[0] + "Z";
158
145
 
159
- node.warn(`[DEBUG HIK] Avvio ricerca video. Canale: ${channelID}. Cerco da: ${startVideoSearch} a: ${endVideoSearch}`);
160
-
161
146
  let output = {
162
147
  tipo_messaggio: "evento",
163
148
  nome_cliente: node.name,
@@ -174,7 +159,7 @@ module.exports = function(RED) {
174
159
  let fileDaCancellare = [];
175
160
 
176
161
  try {
177
- // 1. πŸ“Έ SCARICAMENTO IMMAGINE
162
+ // 1. πŸ“Έ SCARICAMENTO IMMAGINE E RECUPERO METADATI ORARI
178
163
  const payloadFoto = {
179
164
  "EventSearchDescription": {
180
165
  "searchID": "C5AFEE35-B1E0-4C01-83F8-47FD77892E4A",
@@ -196,54 +181,43 @@ module.exports = function(RED) {
196
181
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
197
182
  });
198
183
 
199
- if (resFotoSearch.data?.EventSearchResult?.Targets?.[0]?.pictureUrl) {
200
- const urlFotoGrezzo = resFotoSearch.data.EventSearchResult.Targets[0].pictureUrl;
201
-
202
- const resDownFoto = await nvrAuth.request({
203
- method: 'GET',
204
- url: urlFotoGrezzo,
205
- responseType: 'arraybuffer',
206
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
207
- });
208
-
209
- const fullImgPath = path.join(baseStorage, `img_${timestamp}_ch${channelID}.jpg`);
210
- fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
211
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
212
- fileDaCancellare.push(fullImgPath);
213
- }
214
-
215
- // 2. πŸŽ₯ SCARICAMENTO VIDEO
216
- const payloadVideoSearch = `<?xml version="1.0" encoding="utf-8"?>
217
- <CMSearchDescription>
218
- <searchID>CEC13F0F-1AEA-4474-A2DA-CFFF332C7C5B</searchID>
219
- <trackList><trackID>${videoTrackID}</trackID></trackList>
220
- <timeSpanList>
221
- <timeSpan>
222
- <startTime>${startVideoSearch}</startTime>
223
- <endTime>${endVideoSearch}</endTime>
224
- </timeSpan>
225
- </timeSpanList>
226
- <maxResults>1</maxResults>
227
- <searchResultPostion>0</searchResultPostion>
228
- <metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
229
- </CMSearchDescription>`;
230
-
231
- const resVideoSearch = await nvrAuth.request({
232
- method: 'POST',
233
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
234
- data: payloadVideoSearch,
235
- headers: { "Content-Type": "application/xml" },
236
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
237
- });
184
+ let targetEvento = resFotoSearch.data?.EventSearchResult?.Targets?.[0];
185
+ let playbackURIGrezzo = null;
186
+
187
+ if (targetEvento) {
188
+ // Scarichiamo la foto normalmente
189
+ if (targetEvento.pictureUrl) {
190
+ const urlFotoGrezzo = targetEvento.pictureUrl;
191
+ const resDownFoto = await nvrAuth.request({
192
+ method: 'GET',
193
+ url: urlFotoGrezzo,
194
+ responseType: 'arraybuffer',
195
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
196
+ });
197
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}_ch${channelID}.jpg`);
198
+ fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
199
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
200
+ fileDaCancellare.push(fullImgPath);
201
+ }
238
202
 
239
- const xmlString = resVideoSearch.data.toString();
240
- node.warn(`[DEBUG HIK] Risposta XML dell'NVR:\n${xmlString}`);
203
+ // 2. πŸŽ₯ GENERAZIONE DINAMICA E DIRETTA PLAYBACK URI (Niente piΓΉ ricerca XML!)
204
+ if (targetEvento.startTime && targetEvento.endTime) {
205
+ // Funzione interna per ripulire i dati temporali (es. 2026-07-03T13:41:25+02:00 -> 20260703T134125Z)
206
+ const pulisciDataHik = (dataStr) => {
207
+ let pulita = dataStr.replace(/[-:]/g, '').split('+')[0];
208
+ return pulita;
209
+ };
241
210
 
242
- const uriMatch = xmlString.match(/<playbackURI>([\s\S]*?)<\/playbackURI>/i);
211
+ const startClip = pulisciDataHik(targetEvento.startTime);
212
+ const endClip = pulisciDataHik(targetEvento.endTime);
243
213
 
244
- if (uriMatch && uriMatch[1]) {
245
- const playbackURIGrezzo = uriMatch[1].trim();
214
+ playbackURIGrezzo = `rtsp://${node.host}:${node.port}/Streaming/tracks/${videoTrackID}/?starttime=${startClip}&endtime=${endClip}`;
215
+ node.warn(`[HIK SCORCIATOIA] URI Generato: ${playbackURIGrezzo}`);
216
+ }
217
+ }
246
218
 
219
+ // 3. πŸ’Ύ EFFETTUIAMO LA GET DI DOWNLOAD SE ABBIAMO GENERATO L'URI
220
+ if (playbackURIGrezzo) {
247
221
  const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
248
222
  <downloadRequest>
249
223
  <playbackURI>${playbackURIGrezzo}</playbackURI>
@@ -251,8 +225,8 @@ module.exports = function(RED) {
251
225
 
252
226
  const resDownVideo = await nvrAuth.request({
253
227
  method: 'GET',
254
- url: `rtsp://192.168.62.131/Streaming/tracks/201/?starttime=2026-06-29 13:40:06&amp;endTime=2026-06-29 13:40:16Z`,
255
- //data: payloadDownload,
228
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download`,
229
+ data: payloadDownload,
256
230
  headers: { "Content-Type": "application/xml" },
257
231
  responseType: 'arraybuffer',
258
232
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.134",
3
+ "version": "1.1.136",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",