node-red-contrib-hik-media-buffer 1.1.112 → 1.1.113

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 +166 -117
  2. package/package.json +1 -1
@@ -17,8 +17,6 @@ module.exports = function(RED) {
17
17
  node.protocol = config.protocol || "http";
18
18
  node.user = config.user;
19
19
  node.pass = config.pass;
20
- node.camPass = config.camPass || config.pass;
21
- node.cameras = config.cameras || [];
22
20
 
23
21
  let streamRequest = null;
24
22
  let isClosing = false;
@@ -37,60 +35,65 @@ module.exports = function(RED) {
37
35
 
38
36
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
39
37
 
40
- // --- PRENDE IL NOME DELLA TELECAMERA ---
41
- async function getCameraName(cam) {
42
- const camAuth = new AxiosDigestAuth({
43
- username: node.user,
44
- password: node.camPass
45
- });
46
-
38
+ // --- PRENDE IL NOME DELLA TELECAMERA DALL'NVR ---
39
+ async function getCameraNameFromNVR(channelID) {
40
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
47
41
  try {
48
- const res = await camAuth.request({
42
+ const res = await nvrAuth.request({
49
43
  method: 'GET',
50
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/Video/inputs/channels/${cam.channel}/overlays/channelNameOverlay`,
44
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
51
45
  timeout: 5000,
52
46
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
53
47
  });
54
48
 
55
49
  const data = res.data.toString();
56
50
  const match = data.match(/<name>([^<]+)<\/name>/i);
57
-
58
- if (match && match[1]) {
59
- return match[1].trim();
60
- } else {
61
- return `Canale_${cam.channel}`;
62
- }
63
-
51
+ if (match && match[1]) return match[1].trim();
52
+ return `Canale_${channelID}`;
64
53
  } catch (e) {
65
- return `Camera_${cam.channel}`;
54
+ return `Camera_${channelID}`;
66
55
  }
67
56
  }
68
57
 
69
- // --- CONTROLLO STATUS CAMERE ---
58
+ // --- CONTROLLO STATUS CAMERE INTERROGANDO DIRETTAMENTE L'NVR ---
70
59
  async function checkCameras() {
71
- if (isClosing) return;
72
- for (let cam of node.cameras) {
73
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.camPass });
74
- try {
75
- await camAuth.request({
76
- method: 'GET',
77
- url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
78
- timeout: 5000,
79
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
80
- });
81
- if (statoCamera[cam.ip] === false) {
82
- const nomeOnline = await getCameraName(cam);
83
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
84
- statoCamera[cam.ip] = true;
85
- } else if (statoCamera[cam.ip] === undefined) {
86
- statoCamera[cam.ip] = true;
87
- }
88
- } catch (e) {
89
- if (statoCamera[cam.ip] !== false) {
90
- node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera_${cam.channel}`, ip_telecamera: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
91
- statoCamera[cam.ip] = false;
60
+ if (isClosing || !nvrOnline) return;
61
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
62
+ try {
63
+ const res = await nvrAuth.request({
64
+ method: 'GET',
65
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels`,
66
+ timeout: 8000,
67
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
68
+ });
69
+
70
+ const xml = res.data.toString();
71
+ // Regex per estrarre ogni blocco VideoInputChannel
72
+ const channelBlocks = xml.match(/<VideoInputChannel>[\s\S]*?<\/VideoInputChannel>/g) || [];
73
+
74
+ for (let block of channelBlocks) {
75
+ const idMatch = block.match(/<id>(\d+)<\/id>/i);
76
+ const statusMatch = block.match(/<resVerType>([^<]+)<\/resVerType>/i); // online / offline o simile in base al firmware
77
+
78
+ if (idMatch) {
79
+ const ch = idMatch[1];
80
+ // Se non trova il tag esplicito o dice "online", assumiamo sia attiva
81
+ const isOnline = statusMatch ? statusMatch[1].toLowerCase() === "online" : true;
82
+
83
+ if (isOnline && statoCamera[ch] === false) {
84
+ const nomeOnline = await getCameraNameFromNVR(ch);
85
+ 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" } });
86
+ statoCamera[ch] = true;
87
+ } else if (!isOnline && statoCamera[ch] !== false) {
88
+ 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" } });
89
+ statoCamera[ch] = false;
90
+ } else if (statoCamera[ch] === undefined) {
91
+ statoCamera[ch] = isOnline;
92
+ }
92
93
  }
93
94
  }
95
+ } catch (e) {
96
+ // Se l'NVR fallisce la chiamata generale, lo gestisce l'alert stream o lasciamo lo stato immutato
94
97
  }
95
98
  updateNodeStatus();
96
99
  }
@@ -108,37 +111,32 @@ module.exports = function(RED) {
108
111
 
109
112
  const heartbeatInterval = setInterval(checkCameras, 30000);
110
113
 
111
- // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
114
+ // --- DOWNLOAD CON URL E BODY SECONDO SPECIFICHE ---
112
115
  async function downloadMedia(evento, channelID) {
113
- const camera = node.cameras.find(c => c.channel == channelID);
114
- if (!camera) return;
115
-
116
- const nomeCamera = await getCameraName(camera);
117
-
118
116
  const nowTime = Date.now();
119
- if(!lastTriggerTime[camera.ip]){
120
- lastTriggerTime[camera.ip] = 0;
121
- }
122
- if (nowTime - lastTriggerTime[camera.ip] < 5000) return;
123
- lastTriggerTime[camera.ip] = nowTime;
117
+ if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
118
+ if (nowTime - lastTriggerTime[channelID] < 5000) return;
119
+ lastTriggerTime[channelID] = nowTime;
124
120
 
125
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
121
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
126
122
  const referenceTime = new Date();
127
123
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
128
- const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
129
- const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
124
+
125
+ // Calcolo dinamico del Track ID per il Video (canale * 100 + 1)
126
+ const videoTrackID = (parseInt(channelID) * 100) + 1;
130
127
 
128
+ const nomeCamera = await getCameraNameFromNVR(channelID);
131
129
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
132
- await new Promise(resolve => setTimeout(resolve, 6000));
133
-
134
- const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
135
130
 
136
- // struttura del payload per Python
131
+ // Finestra temporale di 14 ore per la ricerca (adeguata al tuo esempio)
132
+ const startSearchStr = referenceTime.toISOString().split('T')[0] + "T00:00:00 01:00";
133
+ const endSearchStr = referenceTime.toISOString().split('T')[0] + "T23:59:59 01:00";
134
+
137
135
  let output = {
138
136
  tipo_messaggio: "evento",
139
137
  nome_cliente: node.name,
140
138
  nome_telecamera: nomeCamera,
141
- ip_telecamera: camera.ip,
139
+ ip_telecamera: node.host,
142
140
  tipo_evento: evento,
143
141
  timestamp_epoch: timestamp,
144
142
  stato_telecamera: "ONLINE",
@@ -147,78 +145,129 @@ module.exports = function(RED) {
147
145
  video_base64: null
148
146
  };
149
147
 
150
-
151
148
  let fileDaCancellare = [];
152
149
 
153
150
  try {
154
- const tracks = [{ id: "203" }, { id: "201" }];
155
- for (let t of tracks) {
156
- const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${t.id}</trackID></trackIDList><timeSpanList><timeSpan><startTime>2026-06-25T00:00:00Z</startTime><endTime>2026-06-26T23:59:59Z</endTime></timeSpan></timeSpanList><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>recordType.meta.hikvision.com/LineDetection</metadataDescriptor></metadataList></CMSearchDescription>`;
151
+ // 1. SCARICAMENTO IMMAGINE (POST in JSON a eventRecordSearch)
152
+ const payloadFoto = {
153
+ "EventSearchDescription": {
154
+ "searchID": "C5AFEE35-B1E0-4C01-83F8-47FD77892E4A",
155
+ "searchResultPosition": 0,
156
+ "maxResults": 30,
157
+ "timeSpanList": [{
158
+ "startTime": startSearchStr,
159
+ "endTime": endSearchStr
160
+ }],
161
+ "type": "all",
162
+ "channels": [parseInt(channelID)],
163
+ "eventType": "behavior",
164
+ "behavior": { "behaviorEventType": "linedetection" }
165
+ }
166
+ };
157
167
 
158
- const resSearch = await camAuth.request({
159
- method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
160
- });
168
+ const resFotoSearch = await nvrAuth.request({
169
+ method: 'POST',
170
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`,
171
+ data: payloadFoto,
172
+ headers: { "Content-Type": "application/json" },
173
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
174
+ });
175
+
176
+ if (resFotoSearch.data?.EventSearchResult?.matchList?.[0]?.pictureUrl) {
177
+ const urlFotoGrezzo = resFotoSearch.data.EventSearchResult.matchList[0].pictureUrl;
161
178
 
162
- let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
163
- const uriMatch = xml.match(/<playbackURI>([^<]+)</);
164
-
165
- if (uriMatch) {
166
- const rawUri = uriMatch[1].replace(/&amp;/g, '&');
167
- const resDown = await camAuth.request({
168
- method: 'GET',
169
- url: `${baseUrl}/download`,
170
- data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&amp;')}</playbackURI></downloadRequest>`,
171
- responseType: 'arraybuffer'
172
- });
179
+ const resDownFoto = await nvrAuth.request({
180
+ method: 'GET',
181
+ url: urlFotoGrezzo,
182
+ responseType: 'arraybuffer',
183
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
184
+ });
173
185
 
174
- let buffer = Buffer.from(resDown.data);
175
- if (t.id === "203") {
176
- // SALVA FOTO IN LOCALE
177
- const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
178
- fs.writeFileSync(fullImgPath, buffer);
179
-
180
- // La convertiamo subito in testo per il payload
181
- output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
182
-
183
- // Registriamo il file per la distruzione futura
184
- fileDaCancellare.push(fullImgPath);
185
- } else {
186
- // SALVA VIDEO IN LOCALE-
187
- if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
188
- const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
189
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
190
- fs.writeFileSync(rawPath, buffer);
191
-
192
- // Eseguiamo ffmpeg localmente
193
- await new Promise((resolve) => {
194
- exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
195
- if (!err && fs.existsSync(fixedPath)) {
196
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
197
- fileDaCancellare.push(fixedPath);
198
- } else {
199
- output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
200
- }
201
- fileDaCancellare.push(rawPath);
202
- resolve();
203
- });
204
- });
205
- }
206
- }
186
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}_ch${channelID}.jpg`);
187
+ fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
188
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
189
+ fileDaCancellare.push(fullImgPath);
190
+ }
191
+
192
+ // 2. SCARICAMENTO VIDEO (POST XML a search -> GET XML a download)
193
+ // Usiamo una finestra temporale estesa in formato Z come richiesto dall'NVR
194
+ const startVideoSearch = referenceTime.toISOString().split('T')[0] + "T00:00:00Z";
195
+ const endVideoSearch = referenceTime.toISOString().split('T')[0] + "T23:59:59Z";
196
+
197
+ const payloadVideoSearch = `<?xml version="1.0" encoding="utf-8"?>
198
+ <CMSearchDescription>
199
+ <searchID>CEC13F0F-1AEA-4474-A2DA-CFFF332C7C5B</searchID>
200
+ <trackList><trackID>${videoTrackID}</trackID></trackList>
201
+ <timeSpanList>
202
+ <timeSpan>
203
+ <startTime>${startVideoSearch}</startTime>
204
+ <endTime>${endVideoSearch}</endTime>
205
+ </timeSpan>
206
+ </timeSpanList>
207
+ <maxResults>100</maxResults>
208
+ <searchResultPostion>0</searchResultPostion>
209
+ <metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
210
+ </CMSearchDescription>`;
211
+
212
+ const resVideoSearch = await nvrAuth.request({
213
+ method: 'POST',
214
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
215
+ data: payloadVideoSearch,
216
+ headers: { "Content-Type": "application/xml" },
217
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
218
+ });
219
+
220
+ const xmlString = resVideoSearch.data.toString();
221
+ const uriMatch = xmlString.match(/<playbackURI>([\s\S]*?)<\/playbackURI>/i);
222
+
223
+ if (uriMatch && uriMatch[1]) {
224
+ const playbackURIGrezzo = uriMatch[1].trim();
225
+
226
+ const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
227
+ <downloadRequest>
228
+ <playbackURI>${playbackURIGrezzo}</playbackURI>
229
+ </downloadRequest>`;
230
+
231
+ const resDownVideo = await nvrAuth.request({
232
+ method: 'GET',
233
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download`,
234
+ data: payloadDownload,
235
+ headers: { "Content-Type": "application/xml" },
236
+ responseType: 'arraybuffer',
237
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
238
+ });
239
+
240
+ let bufferVideo = Buffer.from(resDownVideo.data);
241
+ if (bufferVideo.slice(0, 4).toString() === 'IMKH') bufferVideo = bufferVideo.slice(40);
242
+
243
+ const rawPath = path.join(baseStorage, `raw_${timestamp}_ch${channelID}.mp4`);
244
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
245
+ fs.writeFileSync(rawPath, bufferVideo);
246
+
247
+ await new Promise((resolve) => {
248
+ exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
249
+ if (!err && fs.existsSync(fixedPath)) {
250
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
251
+ fileDaCancellare.push(fixedPath);
252
+ } else {
253
+ output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
254
+ }
255
+ fileDaCancellare.push(rawPath);
256
+ resolve();
257
+ });
258
+ });
207
259
  }
208
260
 
209
- // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
261
+ // Invio dei media elaborati
210
262
  if (output.foto_base64 || output.video_base64) {
211
263
  node.send({ payload: output });
212
264
 
213
- // TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
214
265
  setTimeout(() => {
215
266
  for (let file of fileDaCancellare) {
216
267
  try {
217
- if (fs.existsSync(file)) {
218
- fs.unlinkSync(file);
219
- }
268
+ if (fs.existsSync(file)) fs.unlinkSync(file);
220
269
  } catch (err) {
221
- node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
270
+ node.error(`Errore pulizia file temporaneo ${file}: ${err.message}`);
222
271
  }
223
272
  }
224
273
  }, 120000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.112",
3
+ "version": "1.1.113",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",