node-red-contrib-hik-media-buffer 1.1.115 → 1.1.117

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.
@@ -35,27 +35,28 @@ module.exports = function(RED) {
35
35
 
36
36
  function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
37
37
 
38
- // --- PRENDE IL NOME DELLA TELECAMERA DALL'NVR ---
38
+ // --- PRENDE IL NOME REALE DELLA TELECAMERA DALL'NVR ---
39
39
  async function getCameraNameFromNVR(channelID) {
40
40
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
41
41
  try {
42
42
  const res = await nvrAuth.request({
43
43
  method: 'GET',
44
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}/overlays/channelNameOverlay`,
44
+ url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}`,
45
45
  timeout: 5000,
46
46
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
47
47
  });
48
48
 
49
49
  const data = res.data.toString();
50
- const match = data.match(/<name>([^<]+)<\/name>/i);
50
+ // Cerchiamo il tag channelName che contiene il nome impostato sull'NVR
51
+ const match = data.match(/<channelName>([^<]+)<\/channelName>/i);
51
52
  if (match && match[1]) return match[1].trim();
52
- return `Canale_${channelID}`;
53
+ return `Canale ${channelID}`;
53
54
  } catch (e) {
54
- return `Camera_${channelID}`;
55
+ return `Canale ${channelID}`;
55
56
  }
56
57
  }
57
58
 
58
- // --- CONTROLLO STATUS CAMERE INTERROGANDO DIRETTAMENTE L'NVR ---
59
+ // --- CONTROLLO STATUS CAMERE (ONLINE/OFFLINE) INTERROGANDO L'NVR ---
59
60
  async function checkCameras() {
60
61
  if (isClosing || !nvrOnline) return;
61
62
  const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
@@ -72,25 +73,29 @@ module.exports = function(RED) {
72
73
 
73
74
  for (let block of channelBlocks) {
74
75
  const idMatch = block.match(/<id>(\d+)<\/id>/i);
75
- const statusMatch = block.match(/<resVerType>([^<]+)<\/resVerType>/i);
76
+ // Hikvision usa il tag <online> true/false per lo stato della telecamera IP accoppiata
77
+ const onlineMatch = block.match(/<online>([^<]+)<\/online>/i) || block.match(/<onLine>([^<]+)<\/onLine>/i);
76
78
 
77
79
  if (idMatch) {
78
80
  const ch = idMatch[1];
79
- const isOnline = statusMatch ? statusMatch[1].toLowerCase() === "online" : true;
81
+ // Se il tag dice true è online, altrimenti se dice false (o manca) è offline
82
+ const isOnline = onlineMatch ? onlineMatch[1].trim().toLowerCase() === "true" : true;
80
83
 
81
84
  if (isOnline && statoCamera[ch] === false) {
82
85
  const nomeOnline = await getCameraNameFromNVR(ch);
83
86
  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
87
  statoCamera[ch] = true;
85
88
  } 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" } });
89
+ 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
90
  statoCamera[ch] = false;
88
91
  } else if (statoCamera[ch] === undefined) {
89
92
  statoCamera[ch] = isOnline;
90
93
  }
91
94
  }
92
95
  }
93
- } catch (e) {}
96
+ } catch (e) {
97
+ node.error(`Errore durante il check delle camere: ${e.message}`);
98
+ }
94
99
  updateNodeStatus();
95
100
  }
96
101
 
@@ -107,7 +112,12 @@ module.exports = function(RED) {
107
112
 
108
113
  const heartbeatInterval = setInterval(checkCameras, 30000);
109
114
 
110
- // --- DOWNLOAD CON URL E BODY SECONDO SPECIFICHE ---
115
+ // --- FORMATTATORE PER IL BODY DI RICERCA (Con i trattini: 2026-07-07T17:24:00Z) ---
116
+ function toHikSearchDate(d) {
117
+ return d.toISOString().split('.')[0] + "Z";
118
+ }
119
+
120
+ // --- DOWNLOAD CON ORARIO DINAMICO STRETTO SULL'ULTIMO EVENTO ---
111
121
  async function downloadMedia(evento, channelID) {
112
122
  const nowTime = Date.now();
113
123
  if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
@@ -118,15 +128,16 @@ module.exports = function(RED) {
118
128
  const referenceTime = new Date();
119
129
  const timestamp = Math.floor(referenceTime.getTime() / 1000);
120
130
 
121
- // Calcolo dinamico del Track ID per il Video (canale * 100 + 1)
122
131
  const videoTrackID = (parseInt(channelID) * 100) + 1;
123
-
124
132
  const nomeCamera = await getCameraNameFromNVR(channelID);
125
133
  node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
126
134
 
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";
135
+ // FINESTRA TEMPORALE REALE: Cerchiamo l'evento da 2 minuti fa a 1 minuto nel futuro
136
+ const inizioFinestra = new Date(referenceTime.getTime() - (2 * 60 * 1000));
137
+ const fineFinestra = new Date(referenceTime.getTime() + (1 * 60 * 1000));
138
+
139
+ const startSearchStr = toHikSearchDate(inizioFinestra);
140
+ const endSearchStr = toHikSearchDate(fineFinestra);
130
141
 
131
142
  let output = {
132
143
  tipo_messaggio: "evento",
@@ -144,16 +155,13 @@ module.exports = function(RED) {
144
155
  let fileDaCancellare = [];
145
156
 
146
157
  try {
147
- // 1. SCARICAMENTO IMMAGINE (POST in JSON a eventRecordSearch)
158
+ // 1. 📸 RICERCA E SCARICAMENTO IMMAGINE (Formato con i trattini nel body)
148
159
  const payloadFoto = {
149
160
  "EventSearchDescription": {
150
161
  "searchID": "C5AFEE35-B1E0-4C01-83F8-47FD77892E4A",
151
162
  "searchResultPosition": 0,
152
- "maxResults": 30,
153
- "timeSpanList": [{
154
- "startTime": startSearchStr,
155
- "endTime": endSearchStr
156
- }],
163
+ "maxResults": 1, // Prendiamo solo il più vicino all'evento attuale
164
+ "timeSpanList": [{ "startTime": startSearchStr, "endTime": endSearchStr }],
157
165
  "type": "all",
158
166
  "channels": [parseInt(channelID)],
159
167
  "eventType": "behavior",
@@ -169,7 +177,6 @@ module.exports = function(RED) {
169
177
  httpsAgent: node.protocol === "https" ? httpsAgent : undefined
170
178
  });
171
179
 
172
- // CORRETTO: Adesso cerca dentro Targets e prende il pictureUrl grezzo come su Postman
173
180
  if (resFotoSearch.data?.EventSearchResult?.Targets?.[0]?.pictureUrl) {
174
181
  const urlFotoGrezzo = resFotoSearch.data.EventSearchResult.Targets[0].pictureUrl;
175
182
 
@@ -186,21 +193,18 @@ module.exports = function(RED) {
186
193
  fileDaCancellare.push(fullImgPath);
187
194
  }
188
195
 
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
-
196
+ // 2. 🎥 RICERCA E SCARICAMENTO VIDEO (Formato con i trattini nel body)
193
197
  const payloadVideoSearch = `<?xml version="1.0" encoding="utf-8"?>
194
198
  <CMSearchDescription>
195
199
  <searchID>CEC13F0F-1AEA-4474-A2DA-CFFF332C7C5B</searchID>
196
200
  <trackList><trackID>${videoTrackID}</trackID></trackList>
197
201
  <timeSpanList>
198
202
  <timeSpan>
199
- <startTime>${startVideoSearch}</startTime>
200
- <endTime>${endVideoSearch}</endTime>
203
+ <startTime>${startSearchStr}</startTime>
204
+ <endTime>${endSearchStr}</endTime>
201
205
  </timeSpan>
202
206
  </timeSpanList>
203
- <maxResults>100</maxResults>
207
+ <maxResults>1</maxResults>
204
208
  <searchResultPostion>0</searchResultPostion>
205
209
  <metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
206
210
  </CMSearchDescription>`;
@@ -217,8 +221,10 @@ module.exports = function(RED) {
217
221
  const uriMatch = xmlString.match(/<playbackURI>([\s\S]*?)<\/playbackURI>/i);
218
222
 
219
223
  if (uriMatch && uriMatch[1]) {
224
+ // Estrae il playbackURI grezzo che contiene GIÀ la data compressa senza trattini generata dall'NVR
220
225
  const playbackURIGrezzo = uriMatch[1].trim();
221
226
 
227
+ // Inviamo il downloadRequest passando l'URI nativo fornito dall'NVR (che rispetta il formato compresso)
222
228
  const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
223
229
  <downloadRequest>
224
230
  <playbackURI>${playbackURIGrezzo}</playbackURI>
@@ -254,17 +260,11 @@ module.exports = function(RED) {
254
260
  });
255
261
  }
256
262
 
257
- // Invio dei media elaborati
258
263
  if (output.foto_base64 || output.video_base64) {
259
264
  node.send({ payload: output });
260
-
261
265
  setTimeout(() => {
262
266
  for (let file of fileDaCancellare) {
263
- try {
264
- if (fs.existsSync(file)) fs.unlinkSync(file);
265
- } catch (err) {
266
- node.error(`Errore pulizia file temporaneo ${file}: ${err.message}`);
267
- }
267
+ try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
268
268
  }
269
269
  }, 120000);
270
270
  }
package/hik-snapshot.js CHANGED
@@ -1,320 +1,106 @@
1
1
  const axios = require('axios');
2
- const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
3
2
  const https = require('https');
4
- const fs = require('fs');
5
- const path = require('path');
6
- const os = require('os');
7
- const { exec } = require('child_process');
3
+ const mhocDigestSnapshot = require('@mhoc/axios-digest-auth');
4
+ const DigestAuthClass = mhocDigestSnapshot.default;
8
5
 
9
6
  module.exports = function(RED) {
10
- function HikMediaBufferNode(config) {
7
+ function HikSnapshotNode(config) {
11
8
  RED.nodes.createNode(this, config);
12
9
  const node = this;
13
-
14
- node.name = config.name || "TEST";
10
+
11
+ // Recuperiamo il nome del nodo dalla configurazione (se vuoto usa l'host)
12
+ node.nodeName = config.name || `NVR_${config.host}`;
13
+
14
+ node.protocol = config.protocol || "http";
15
15
  node.host = config.host;
16
16
  node.port = config.port || "80";
17
- node.protocol = config.protocol || "http";
18
17
  node.user = config.user;
19
18
  node.pass = config.pass;
20
-
21
- let streamRequest = null;
22
- let isClosing = false;
23
- let lastTriggerTime = {};
24
- let nvrOnline = true;
25
- let statoCamera = {};
19
+ node.maxChannels = parseInt(config.channel) || 1;
26
20
 
27
21
  const httpsAgent = new https.Agent({ rejectUnauthorized: false });
28
- const EventList = ["FieldDetection", "LineDetection"];
29
-
30
- // CARTELLA TEMPORANEA
31
- const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
32
- if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
33
-
34
- node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
35
-
36
- function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
37
-
38
- // --- PRENDE IL NOME REALE DELLA TELECAMERA DALL'NVR ---
39
- async function getCameraNameFromNVR(channelID) {
40
- const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
41
- try {
42
- const res = await nvrAuth.request({
43
- method: 'GET',
44
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels/${channelID}`,
45
- timeout: 5000,
46
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
47
- });
48
-
49
- const data = res.data.toString();
50
- // Cerchiamo il tag channelName che contiene il nome impostato sull'NVR
51
- const match = data.match(/<channelName>([^<]+)<\/channelName>/i);
52
- if (match && match[1]) return match[1].trim();
53
- return `Canale ${channelID}`;
54
- } catch (e) {
55
- return `Canale ${channelID}`;
56
- }
57
- }
58
-
59
- // --- CONTROLLO STATUS CAMERE (ONLINE/OFFLINE) INTERROGANDO L'NVR ---
60
- async function checkCameras() {
61
- if (isClosing || !nvrOnline) return;
62
- const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
63
- try {
64
- const res = await nvrAuth.request({
65
- method: 'GET',
66
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/System/Video/inputs/channels`,
67
- timeout: 8000,
68
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
69
- });
70
-
71
- const xml = res.data.toString();
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
- // Hikvision usa il tag <online> true/false per lo stato della telecamera IP accoppiata
77
- const onlineMatch = block.match(/<online>([^<]+)<\/online>/i) || block.match(/<onLine>([^<]+)<\/onLine>/i);
78
-
79
- if (idMatch) {
80
- const ch = idMatch[1];
81
- // Se il tag dice true è online, altrimenti se dice false (o manca) è offline
82
- const isOnline = onlineMatch ? onlineMatch[1].trim().toLowerCase() === "true" : true;
83
-
84
- if (isOnline && statoCamera[ch] === false) {
85
- const nomeOnline = await getCameraNameFromNVR(ch);
86
- 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" } });
87
- statoCamera[ch] = true;
88
- } else if (!isOnline && statoCamera[ch] !== false) {
89
- 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" } });
90
- statoCamera[ch] = false;
91
- } else if (statoCamera[ch] === undefined) {
92
- statoCamera[ch] = isOnline;
93
- }
94
- }
95
- }
96
- } catch (e) {
97
- node.error(`Errore durante il check delle camere: ${e.message}`);
98
- }
99
- updateNodeStatus();
100
- }
101
-
102
- function updateNodeStatus() {
103
- const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
104
- if (!nvrOnline) {
105
- node.status({fill:"red", shape:"ring", text:"NVR Offline"});
106
- } else if (offlineCams > 0) {
107
- node.status({fill:"yellow", shape:"dot", text: `${offlineCams} Cam Offline`});
108
- } else {
109
- node.status({fill:"green", shape:"ring", text:"In ascolto"});
110
- }
111
- }
112
22
 
113
- const heartbeatInterval = setInterval(checkCameras, 30000);
23
+ node.on('input', async function(msg) {
24
+ if (msg.payload !== true) return;
114
25
 
115
- // --- DOWNLOAD CON URL E BODY SECONDO SPECIFICHE ---
116
- async function downloadMedia(evento, channelID) {
117
- const nowTime = Date.now();
118
- if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
119
- if (nowTime - lastTriggerTime[channelID] < 5000) return;
120
- lastTriggerTime[channelID] = nowTime;
121
-
122
- const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
123
- const referenceTime = new Date();
124
- const timestamp = Math.floor(referenceTime.getTime() / 1000);
125
-
126
- const videoTrackID = (parseInt(channelID) * 100) + 1;
26
+ node.status({fill: "blue", shape: "dot", text: "Verifica canali..."});
127
27
 
128
- // Recupera il nome reale prima di inviare l'allarme
129
- const nomeCamera = await getCameraNameFromNVR(channelID);
130
- node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
28
+ const digest = new DigestAuthClass({
29
+ username: node.user,
30
+ password: node.pass
31
+ });
131
32
 
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";
33
+ const data = new Date();
34
+ const year = data.getFullYear();
35
+ const month = data.getMonth() + 1;
36
+ const day = data.getDate();
134
37
 
135
- let output = {
136
- tipo_messaggio: "evento",
137
- nome_cliente: node.name,
138
- nome_telecamera: nomeCamera,
139
- ip_telecamera: node.host,
140
- tipo_evento: evento,
141
- timestamp_epoch: timestamp,
142
- stato_telecamera: "ONLINE",
143
- channel: channelID.toString(),
144
- foto_base64: null,
145
- video_base64: null
146
- };
38
+ let snapshotResults = [];
147
39
 
148
- let fileDaCancellare = [];
149
-
150
- try {
151
- // 1. SCARICAMENTO IMMAGINE
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
- }
40
+ for (let i = 1; i <= node.maxChannels; i++) {
41
+ const chanId = i + "01";
42
+ const snapUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/Streaming/channels/${chanId}/picture`;
43
+ const recordUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/record/tracks/${chanId}/dailyDistribution`;
44
+
45
+ const recordXml = `<?xml version="1.0" encoding="utf-8"?><trackDailyParam><year>${year}</year><monthOfYear>${month}</monthOfYear><dayOfMonth>${day}</dayOfMonth></trackDailyParam>`;
46
+
47
+ let resCanale = {
48
+ name: node.nodeName, // <--- INIEZIONE RIGIDA DEL NOME DEL CONDOMINIO
49
+ channel: i,
50
+ photo: null,
51
+ snapOk: false,
52
+ isRecording: false
166
53
  };
167
54
 
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?.Targets?.[0]?.pictureUrl) {
177
- const urlFotoGrezzo = resFotoSearch.data.EventSearchResult.Targets[0].pictureUrl;
178
-
179
- const resDownFoto = await nvrAuth.request({
55
+ // 1. SNAPSHOT
56
+ try {
57
+ const responseSnap = await digest.request({
180
58
  method: 'GET',
181
- url: urlFotoGrezzo,
59
+ url: snapUrl,
182
60
  responseType: 'arraybuffer',
183
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
61
+ httpsAgent: node.protocol === 'https' ? httpsAgent : undefined,
62
+ timeout: 5000
184
63
  });
185
-
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);
64
+ resCanale.photo = responseSnap.data;
65
+ resCanale.snapOk = true;
66
+ } catch (err) {
67
+ resCanale.snapError = err.message;
190
68
  }
191
69
 
192
- // 2. SCARICAMENTO VIDEO
193
- const startVideoSearch = referenceTime.toISOString().split('T')[0] + "T00:00:00Z";
194
- const endVideoSearch = referenceTime.toISOString().split('T')[0] + "T23:59:59Z";
195
-
196
- const payloadVideoSearch = `<?xml version="1.0" encoding="utf-8"?>
197
- <CMSearchDescription>
198
- <searchID>CEC13F0F-1AEA-4474-A2DA-CFFF332C7C5B</searchID>
199
- <trackList><trackID>${videoTrackID}</trackID></trackList>
200
- <timeSpanList>
201
- <timeSpan>
202
- <startTime>${startVideoSearch}</startTime>
203
- <endTime>${endVideoSearch}</endTime>
204
- </timeSpan>
205
- </timeSpanList>
206
- <maxResults>100</maxResults>
207
- <searchResultPostion>0</searchResultPostion>
208
- <metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
209
- </CMSearchDescription>`;
210
-
211
- const resVideoSearch = await nvrAuth.request({
212
- method: 'POST',
213
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
214
- data: payloadVideoSearch,
215
- headers: { "Content-Type": "application/xml" },
216
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
217
- });
218
-
219
- const xmlString = resVideoSearch.data.toString();
220
- const uriMatch = xmlString.match(/<playbackURI>([\s\S]*?)<\/playbackURI>/i);
221
-
222
- if (uriMatch && uriMatch[1]) {
223
- const playbackURIGrezzo = uriMatch[1].trim();
224
-
225
- const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
226
- <downloadRequest>
227
- <playbackURI>${playbackURIGrezzo}</playbackURI>
228
- </downloadRequest>`;
229
-
230
- const resDownVideo = await nvrAuth.request({
231
- method: 'GET',
232
- url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download`,
233
- data: payloadDownload,
234
- headers: { "Content-Type": "application/xml" },
235
- responseType: 'arraybuffer',
236
- httpsAgent: node.protocol === "https" ? httpsAgent : undefined
237
- });
238
-
239
- let bufferVideo = Buffer.from(resDownVideo.data);
240
- if (bufferVideo.slice(0, 4).toString() === 'IMKH') bufferVideo = bufferVideo.slice(40);
241
-
242
- const rawPath = path.join(baseStorage, `raw_${timestamp}_ch${channelID}.mp4`);
243
- const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
244
- fs.writeFileSync(rawPath, bufferVideo);
245
-
246
- await new Promise((resolve) => {
247
- exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
248
- if (!err && fs.existsSync(fixedPath)) {
249
- output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
250
- fileDaCancellare.push(fixedPath);
251
- } else {
252
- output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
253
- }
254
- fileDaCancellare.push(rawPath);
255
- resolve();
256
- });
70
+ // 2. RECORDING
71
+ try {
72
+ const responseRec = await digest.request({
73
+ method: 'POST',
74
+ url: recordUrl,
75
+ data: recordXml,
76
+ headers: { 'Content-Type': 'application/xml' },
77
+ httpsAgent: node.protocol === 'https' ? httpsAgent : undefined,
78
+ timeout: 5000
257
79
  });
80
+ const xmlOutput = responseRec.data.toString();
81
+ const regex = new RegExp(`<id>${day}</id>[^]*?<record>true</record>`);
82
+ resCanale.isRecording = regex.test(xmlOutput);
83
+ } catch (err) {
84
+ resCanale.recError = err.message;
258
85
  }
259
86
 
260
- if (output.foto_base64 || output.video_base64) {
261
- node.send({ payload: output });
262
-
263
- setTimeout(() => {
264
- for (let file of fileDaCancellare) {
265
- try {
266
- if (fs.existsSync(file)) fs.unlinkSync(file);
267
- } catch (err) {}
268
- }
269
- }, 120000);
270
- }
271
-
272
- } catch (e) {
273
- node.error(`Errore Download Cam ${channelID}: ${e.message}`);
87
+ snapshotResults.push(resCanale);
88
+ await new Promise(resolve => setTimeout(resolve, 200));
274
89
  }
275
- updateNodeStatus();
276
- }
277
90
 
278
- // --- ALERT STREAM ---
279
- function startAlertStream() {
280
- if (isClosing) return;
281
- const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
282
- const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
283
- nvrAuth.request({ method: 'GET', url: url, responseType: 'stream', httpsAgent: node.protocol === "https" ? httpsAgent : undefined })
284
- .then(response => {
285
- streamRequest = response;
286
- if (!nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", ip: node.host, msg: "NVR Online", nome_cliente: node.name } }); nvrOnline = true; }
287
- updateNodeStatus();
288
- response.data.on('data', (chunk) => {
289
- const data = chunk.toString().toLowerCase();
290
- if (data.includes("active")) {
291
- const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
292
- if (chMatch) {
293
- let ev = "Unknown";
294
- for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
295
- downloadMedia(ev, chMatch[1]);
296
- }
297
- }
298
- });
299
- response.data.on('error', () => handleNvrError());
300
- response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
301
- }).catch(() => handleNvrError());
302
- }
91
+ msg.payload = snapshotResults;
92
+ node.send(msg);
303
93
 
304
- function handleNvrError() {
305
- if (nvrOnline) { node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", ip: node.host, msg: "NVR Offline", nome_cliente: node.name } }); nvrOnline = false; }
306
- updateNodeStatus();
307
- if (!isClosing) setTimeout(startAlertStream, 10000);
308
- }
309
-
310
- startAlertStream();
311
- setTimeout(checkCameras, 2000);
312
- node.on('close', (done) => {
313
- isClosing = true;
314
- clearInterval(heartbeatInterval);
315
- if (streamRequest) streamRequest.data.destroy();
316
- done();
94
+ // Conteggi per lo stato del nodo
95
+ const snapCount = snapshotResults.filter(v => v.snapOk).length;
96
+ const recCount = snapshotResults.filter(v => v.isRecording).length;
97
+
98
+ node.status({
99
+ fill: (snapCount === node.maxChannels && recCount === node.maxChannels) ? "green" : "yellow",
100
+ shape: "dot",
101
+ text: `Snap: ${snapCount}/${node.maxChannels} | Rec: ${recCount}/${node.maxChannels}`
102
+ });
317
103
  });
318
104
  }
319
- RED.nodes.registerType("hik-media-buffer", HikMediaBufferNode);
105
+ RED.nodes.registerType("hik-snapshot", HikSnapshotNode);
320
106
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.1.115",
3
+ "version": "1.1.117",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",