node-red-contrib-hik-media-buffer 1.1.111 → 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.
- package/hik-media-buffer.js +166 -117
- package/hik-snapshot.js +4 -0
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -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
|
|
42
|
-
const
|
|
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
|
|
42
|
+
const res = await nvrAuth.request({
|
|
49
43
|
method: 'GET',
|
|
50
|
-
url: `${node.protocol}://${
|
|
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
|
-
|
|
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_${
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
if (
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
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[
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
method: 'GET',
|
|
169
|
-
url: `${baseUrl}/download`,
|
|
170
|
-
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
//
|
|
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
|
|
270
|
+
node.error(`Errore pulizia file temporaneo ${file}: ${err.message}`);
|
|
222
271
|
}
|
|
223
272
|
}
|
|
224
273
|
}, 120000);
|
package/hik-snapshot.js
CHANGED
|
@@ -8,6 +8,9 @@ module.exports = function(RED) {
|
|
|
8
8
|
RED.nodes.createNode(this, config);
|
|
9
9
|
const node = this;
|
|
10
10
|
|
|
11
|
+
// Recuperiamo il nome del nodo dalla configurazione (se vuoto usa l'host)
|
|
12
|
+
node.nodeName = config.name || `NVR_${config.host}`;
|
|
13
|
+
|
|
11
14
|
node.protocol = config.protocol || "http";
|
|
12
15
|
node.host = config.host;
|
|
13
16
|
node.port = config.port || "80";
|
|
@@ -42,6 +45,7 @@ module.exports = function(RED) {
|
|
|
42
45
|
const recordXml = `<?xml version="1.0" encoding="utf-8"?><trackDailyParam><year>${year}</year><monthOfYear>${month}</monthOfYear><dayOfMonth>${day}</dayOfMonth></trackDailyParam>`;
|
|
43
46
|
|
|
44
47
|
let resCanale = {
|
|
48
|
+
name: node.nodeName, // <--- INIEZIONE RIGIDA DEL NOME DEL CONDOMINIO
|
|
45
49
|
channel: i,
|
|
46
50
|
photo: null,
|
|
47
51
|
snapOk: false,
|