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.
- package/hik-media-buffer.js +163 -118
- 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,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
|
|
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
|
-
|
|
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
|
+
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
|
|
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[
|
|
120
|
-
|
|
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
|
|
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
|
-
|
|
129
|
-
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
});
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
266
|
+
node.error(`Errore pulizia file temporaneo ${file}: ${err.message}`);
|
|
222
267
|
}
|
|
223
268
|
}
|
|
224
269
|
}, 120000);
|