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

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 +163 -118
  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,61 +35,62 @@ 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
+ const channelBlocks = xml.match(/<VideoInputChannel>[\s\S]*?<\/VideoInputChannel>/g) || [];
72
+
73
+ for (let block of channelBlocks) {
74
+ const idMatch = block.match(/<id>(\d+)<\/id>/i);
75
+ const statusMatch = block.match(/<resVerType>([^<]+)<\/resVerType>/i);
76
+
77
+ if (idMatch) {
78
+ const ch = idMatch[1];
79
+ const isOnline = statusMatch ? statusMatch[1].toLowerCase() === "online" : true;
80
+
81
+ if (isOnline && statoCamera[ch] === false) {
82
+ const nomeOnline = await getCameraNameFromNVR(ch);
83
+ 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" } });
84
+ statoCamera[ch] = true;
85
+ } else if (!isOnline && statoCamera[ch] !== false) {
86
+ 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" } });
87
+ statoCamera[ch] = false;
88
+ } else if (statoCamera[ch] === undefined) {
89
+ statoCamera[ch] = isOnline;
90
+ }
92
91
  }
93
92
  }
94
- }
93
+ } catch (e) {}
95
94
  updateNodeStatus();
96
95
  }
97
96
 
@@ -108,37 +107,32 @@ module.exports = function(RED) {
108
107
 
109
108
  const heartbeatInterval = setInterval(checkCameras, 30000);
110
109
 
111
- // --- DOWNLOAD, SALVATAGGIO, CONVERSIONE E RIMOZIONE DOPO 2 MINUTI ---
110
+ // --- DOWNLOAD CON URL E BODY SECONDO SPECIFICHE ---
112
111
  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
112
  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;
113
+ if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
114
+ if (nowTime - lastTriggerTime[channelID] < 5000) return;
115
+ lastTriggerTime[channelID] = nowTime;
124
116
 
125
- const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
117
+ const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
126
118
  const referenceTime = new Date();
127
119
  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)));
120
+
121
+ // Calcolo dinamico del Track ID per il Video (canale * 100 + 1)
122
+ const videoTrackID = (parseInt(channelID) * 100) + 1;
130
123
 
124
+ const nomeCamera = await getCameraNameFromNVR(channelID);
131
125
  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
126
 
136
- // struttura del payload per Python
127
+ // Finestra temporale per la ricerca foto JSON
128
+ const startSearchStr = referenceTime.toISOString().split('T')[0] + "T00:00:00 01:00";
129
+ const endSearchStr = referenceTime.toISOString().split('T')[0] + "T23:59:59 01:00";
130
+
137
131
  let output = {
138
132
  tipo_messaggio: "evento",
139
133
  nome_cliente: node.name,
140
134
  nome_telecamera: nomeCamera,
141
- ip_telecamera: camera.ip,
135
+ ip_telecamera: node.host,
142
136
  tipo_evento: evento,
143
137
  timestamp_epoch: timestamp,
144
138
  stato_telecamera: "ONLINE",
@@ -147,78 +141,129 @@ module.exports = function(RED) {
147
141
  video_base64: null
148
142
  };
149
143
 
150
-
151
144
  let fileDaCancellare = [];
152
145
 
153
146
  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>`;
147
+ // 1. SCARICAMENTO IMMAGINE (POST in JSON a eventRecordSearch)
148
+ const payloadFoto = {
149
+ "EventSearchDescription": {
150
+ "searchID": "C5AFEE35-B1E0-4C01-83F8-47FD77892E4A",
151
+ "searchResultPosition": 0,
152
+ "maxResults": 30,
153
+ "timeSpanList": [{
154
+ "startTime": startSearchStr,
155
+ "endTime": endSearchStr
156
+ }],
157
+ "type": "all",
158
+ "channels": [parseInt(channelID)],
159
+ "eventType": "behavior",
160
+ "behavior": { "behaviorEventType": "linedetection" }
161
+ }
162
+ };
157
163
 
158
- const resSearch = await camAuth.request({
159
- method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
160
- });
164
+ const resFotoSearch = await nvrAuth.request({
165
+ method: 'POST',
166
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`,
167
+ data: payloadFoto,
168
+ headers: { "Content-Type": "application/json" },
169
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
170
+ });
171
+
172
+ // CORRETTO: Adesso cerca dentro Targets e prende il pictureUrl grezzo come su Postman
173
+ if (resFotoSearch.data?.EventSearchResult?.Targets?.[0]?.pictureUrl) {
174
+ const urlFotoGrezzo = resFotoSearch.data.EventSearchResult.Targets[0].pictureUrl;
161
175
 
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
- });
176
+ const resDownFoto = await nvrAuth.request({
177
+ method: 'GET',
178
+ url: urlFotoGrezzo,
179
+ responseType: 'arraybuffer',
180
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
181
+ });
173
182
 
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
- }
183
+ const fullImgPath = path.join(baseStorage, `img_${timestamp}_ch${channelID}.jpg`);
184
+ fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
185
+ output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
186
+ fileDaCancellare.push(fullImgPath);
187
+ }
188
+
189
+ // 2. SCARICAMENTO VIDEO (POST XML a search -> GET XML a download)
190
+ const startVideoSearch = referenceTime.toISOString().split('T')[0] + "T00:00:00Z";
191
+ const endVideoSearch = referenceTime.toISOString().split('T')[0] + "T23:59:59Z";
192
+
193
+ const payloadVideoSearch = `<?xml version="1.0" encoding="utf-8"?>
194
+ <CMSearchDescription>
195
+ <searchID>CEC13F0F-1AEA-4474-A2DA-CFFF332C7C5B</searchID>
196
+ <trackList><trackID>${videoTrackID}</trackID></trackList>
197
+ <timeSpanList>
198
+ <timeSpan>
199
+ <startTime>${startVideoSearch}</startTime>
200
+ <endTime>${endVideoSearch}</endTime>
201
+ </timeSpan>
202
+ </timeSpanList>
203
+ <maxResults>100</maxResults>
204
+ <searchResultPostion>0</searchResultPostion>
205
+ <metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
206
+ </CMSearchDescription>`;
207
+
208
+ const resVideoSearch = await nvrAuth.request({
209
+ method: 'POST',
210
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
211
+ data: payloadVideoSearch,
212
+ headers: { "Content-Type": "application/xml" },
213
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
214
+ });
215
+
216
+ const xmlString = resVideoSearch.data.toString();
217
+ const uriMatch = xmlString.match(/<playbackURI>([\s\S]*?)<\/playbackURI>/i);
218
+
219
+ if (uriMatch && uriMatch[1]) {
220
+ const playbackURIGrezzo = uriMatch[1].trim();
221
+
222
+ const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
223
+ <downloadRequest>
224
+ <playbackURI>${playbackURIGrezzo}</playbackURI>
225
+ </downloadRequest>`;
226
+
227
+ const resDownVideo = await nvrAuth.request({
228
+ method: 'GET',
229
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download`,
230
+ data: payloadDownload,
231
+ headers: { "Content-Type": "application/xml" },
232
+ responseType: 'arraybuffer',
233
+ httpsAgent: node.protocol === "https" ? httpsAgent : undefined
234
+ });
235
+
236
+ let bufferVideo = Buffer.from(resDownVideo.data);
237
+ if (bufferVideo.slice(0, 4).toString() === 'IMKH') bufferVideo = bufferVideo.slice(40);
238
+
239
+ const rawPath = path.join(baseStorage, `raw_${timestamp}_ch${channelID}.mp4`);
240
+ const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
241
+ fs.writeFileSync(rawPath, bufferVideo);
242
+
243
+ await new Promise((resolve) => {
244
+ exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
245
+ if (!err && fs.existsSync(fixedPath)) {
246
+ output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
247
+ fileDaCancellare.push(fixedPath);
248
+ } else {
249
+ output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
250
+ }
251
+ fileDaCancellare.push(rawPath);
252
+ resolve();
253
+ });
254
+ });
207
255
  }
208
256
 
209
- // Spediamo il pacchetto completo verso l'HTTP Request tramite Node-RED
257
+ // Invio dei media elaborati
210
258
  if (output.foto_base64 || output.video_base64) {
211
259
  node.send({ payload: output });
212
260
 
213
- // TIMER A 2 MINUTI PER LA PULIZIA DEL DISCO
214
261
  setTimeout(() => {
215
262
  for (let file of fileDaCancellare) {
216
263
  try {
217
- if (fs.existsSync(file)) {
218
- fs.unlinkSync(file);
219
- }
264
+ if (fs.existsSync(file)) fs.unlinkSync(file);
220
265
  } catch (err) {
221
- node.error(`Errore durante la pulizia del file temporaneo ${file}: ${err.message}`);
266
+ node.error(`Errore pulizia file temporaneo ${file}: ${err.message}`);
222
267
  }
223
268
  }
224
269
  }, 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.114",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",