node-red-contrib-hik-media-buffer 1.0.0
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.html +97 -0
- package/hik-media-buffer.js +224 -0
- package/hik-snapshot.html +70 -0
- package/hik-snapshot.js +102 -0
- package/package.json +16 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('hik-media-buffer', {
|
|
3
|
+
category: 'Hikvision',
|
|
4
|
+
color: '#00b4d8',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "" },
|
|
7
|
+
host: { value: "", required: true },
|
|
8
|
+
protocol: { value: "http" },
|
|
9
|
+
port: { value: "80", required: true },
|
|
10
|
+
user: { value: "admin", required: true },
|
|
11
|
+
pass: { value: "" },
|
|
12
|
+
camPass: { value: "" },
|
|
13
|
+
cameras: { value: [] }
|
|
14
|
+
},
|
|
15
|
+
inputs: 0,
|
|
16
|
+
outputs: 1,
|
|
17
|
+
icon: "font-awesome/fa-bell",
|
|
18
|
+
label: function() { return this.name || "Hik Multi Cam"; },
|
|
19
|
+
oneditprepare: function() {
|
|
20
|
+
$("#node-input-cameras-container").css('min-height','150px').editableList({
|
|
21
|
+
addItem: function(container, i, data) {
|
|
22
|
+
var row = $('<div/>').appendTo(container);
|
|
23
|
+
$('<input/>',{class:"node-input-camera-channel",type:"text",placeholder:"Canale (es. 1)",style:"width:30%; margin-right:5px;"}).appendTo(row).val(data.channel);
|
|
24
|
+
$('<input/>',{class:"node-input-camera-ip",type:"text",placeholder:"IP Telecamera",style:"width:60%;"}).appendTo(row).val(data.ip);
|
|
25
|
+
},
|
|
26
|
+
removable: true,
|
|
27
|
+
sortable: true
|
|
28
|
+
});
|
|
29
|
+
if (this.cameras) {
|
|
30
|
+
this.cameras.forEach(function(cam) {
|
|
31
|
+
$("#node-input-cameras-container").editableList('addItem', cam);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
oneditsave: function() {
|
|
36
|
+
var cams = $("#node-input-cameras-container").editableList('items');
|
|
37
|
+
var node = this;
|
|
38
|
+
node.cameras = [];
|
|
39
|
+
cams.each(function(i) {
|
|
40
|
+
var cam = $(this);
|
|
41
|
+
var o = {
|
|
42
|
+
channel: cam.find(".node-input-camera-channel").val(),
|
|
43
|
+
ip: cam.find(".node-input-camera-ip").val()
|
|
44
|
+
};
|
|
45
|
+
if (o.channel && o.ip) node.cameras.push(o);
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
</script>
|
|
50
|
+
|
|
51
|
+
<script type="text/html" data-template-name="hik-media-buffer">
|
|
52
|
+
<div class="form-row">
|
|
53
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Nome</label>
|
|
54
|
+
<input type="text" id="node-input-name">
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<hr>
|
|
58
|
+
<h4>Configurazione NVR (Ascolto Allarmi)</h4>
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<label for="node-input-host"><i class="fa fa-globe"></i> IP NVR</label>
|
|
61
|
+
<input type="text" id="node-input-host" placeholder="Es: 192.168.1.100">
|
|
62
|
+
</div>
|
|
63
|
+
<div class="form-row">
|
|
64
|
+
<label for="node-input-user"><i class="fa fa-user"></i> Utente</label>
|
|
65
|
+
<input type="text" id="node-input-user">
|
|
66
|
+
</div>
|
|
67
|
+
<div class="form-row">
|
|
68
|
+
<label for="node-input-pass"><i class="fa fa-lock"></i> Password NVR</label>
|
|
69
|
+
<input type="password" id="node-input-pass">
|
|
70
|
+
</div>
|
|
71
|
+
<div class="form-row">
|
|
72
|
+
<label for="node-input-protocol"><i class="fa fa-shield"></i> Protocollo</label>
|
|
73
|
+
<select id="node-input-protocol" style="width: 70%">
|
|
74
|
+
<option value="http">HTTP</option>
|
|
75
|
+
<option value="https">HTTPS</option>
|
|
76
|
+
</select>
|
|
77
|
+
</div>
|
|
78
|
+
<div class="form-row">
|
|
79
|
+
<label for="node-input-port"><i class="fa fa-plug"></i> Porta</label>
|
|
80
|
+
<select id="node-input-port" style="width: 70%">
|
|
81
|
+
<option value="80">80</option>
|
|
82
|
+
<option value="443">443</option>
|
|
83
|
+
<option value="8000">8000</option>
|
|
84
|
+
<option value="8008">8008</option>
|
|
85
|
+
</select>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<hr>
|
|
89
|
+
<h4>Elenco Telecamere (Download)</h4>
|
|
90
|
+
<div class="form-row node-input-cameras-container-row">
|
|
91
|
+
<ol id="node-input-cameras-container"></ol>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="form-row">
|
|
94
|
+
<label for="node-input-camPass"><i class="fa fa-key"></i> Password Telecamera</label>
|
|
95
|
+
<input type="password" id="node-input-camPass" placeholder="Password comune alle telecamere">
|
|
96
|
+
</div>
|
|
97
|
+
</script>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
3
|
+
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');
|
|
8
|
+
|
|
9
|
+
module.exports = function(RED) {
|
|
10
|
+
function HikMediaBufferNode(config) {
|
|
11
|
+
RED.nodes.createNode(this, config);
|
|
12
|
+
const node = this;
|
|
13
|
+
|
|
14
|
+
// --- ASSEGNAZIONE PROPRIETÀ AL NODO ---
|
|
15
|
+
node.host = config.host;
|
|
16
|
+
node.port = config.port || "80";
|
|
17
|
+
node.protocol = config.protocol || "http";
|
|
18
|
+
node.user = config.user;
|
|
19
|
+
node.pass = config.pass;
|
|
20
|
+
node.camPass = config.camPass || config.pass; // Usa pass dell'NVR se quella cam specifica non c'è
|
|
21
|
+
node.cameras = config.cameras || [];
|
|
22
|
+
// --------------------------------------
|
|
23
|
+
|
|
24
|
+
let streamRequest = null;
|
|
25
|
+
let isClosing = false;
|
|
26
|
+
let lastTriggerTime = 0;
|
|
27
|
+
|
|
28
|
+
// Stati di connessione
|
|
29
|
+
let nvrOnline = true;
|
|
30
|
+
let statoCamera = {}; // Memorizza lo stato { "IP": true/false }
|
|
31
|
+
|
|
32
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
33
|
+
const tempDir = os.tmpdir();
|
|
34
|
+
const EventList = ["FieldDetection", "LineDetection"];
|
|
35
|
+
|
|
36
|
+
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
37
|
+
|
|
38
|
+
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
39
|
+
|
|
40
|
+
// --- CONTROLLO SE LE TELECAMERE SONO ONLINE ---
|
|
41
|
+
async function checkCameras() {
|
|
42
|
+
if (isClosing) return;
|
|
43
|
+
for (let cam of node.cameras) {
|
|
44
|
+
const camAuth = new AxiosDigestAuth({
|
|
45
|
+
username: node.user,
|
|
46
|
+
password: node.camPass
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
await camAuth.request({
|
|
50
|
+
method: 'GET',
|
|
51
|
+
url: `${node.protocol}://${cam.ip}:${node.port}/ISAPI/System/deviceInfo`,
|
|
52
|
+
timeout: 5000,
|
|
53
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (statoCamera[cam.ip] === false) {
|
|
57
|
+
node.send({ payload: { status: "online", ip: cam.ip, channel: cam.channel, msg: "Camera ripristinata" } });
|
|
58
|
+
statoCamera[cam.ip] = true;
|
|
59
|
+
} else if (statoCamera[cam.ip] === undefined) {
|
|
60
|
+
statoCamera[cam.ip] = true;
|
|
61
|
+
}
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (statoCamera[cam.ip] !== false) {
|
|
64
|
+
node.send({ payload: { status: "offline", ip: cam.ip, channel: cam.channel, msg: "Camera non raggiungibile" } });
|
|
65
|
+
statoCamera[cam.ip] = false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
updateNodeStatus();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function updateNodeStatus() {
|
|
73
|
+
const offlineCams = Object.values(statoCamera).filter(v => v === false).length;
|
|
74
|
+
if (!nvrOnline) {
|
|
75
|
+
node.status({fill:"red", shape:"ring", text:"NVR Offline"});
|
|
76
|
+
} else if (offlineCams > 0) {
|
|
77
|
+
node.status({fill:"yellow", shape:"dot", text: `${offlineCams} Cam Offline`});
|
|
78
|
+
} else {
|
|
79
|
+
node.status({fill:"green", shape:"ring", text:"In ascolto"});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
84
|
+
|
|
85
|
+
// --- FUNZIONE DOWNLOAD ---
|
|
86
|
+
async function downloadMedia(evento, channelID) {
|
|
87
|
+
const camera = node.cameras.find(c => c.channel == channelID);
|
|
88
|
+
if (!camera) return;
|
|
89
|
+
|
|
90
|
+
const nowTime = Date.now();
|
|
91
|
+
if (nowTime - lastTriggerTime < 10000) return;
|
|
92
|
+
lastTriggerTime = nowTime;
|
|
93
|
+
|
|
94
|
+
const camAuth = new AxiosDigestAuth({
|
|
95
|
+
username: node.user,
|
|
96
|
+
password: node.camPass
|
|
97
|
+
});
|
|
98
|
+
const referenceTime = new Date();
|
|
99
|
+
const startTime = toHikDate(new Date(referenceTime.getTime() - (10 * 1000)));
|
|
100
|
+
const endTime = toHikDate(new Date(referenceTime.getTime() + (10 * 1000)));
|
|
101
|
+
|
|
102
|
+
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
103
|
+
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
104
|
+
|
|
105
|
+
const baseUrl = `${node.protocol}://${camera.ip}:${node.port}/ISAPI/ContentMgmt`;
|
|
106
|
+
let output = { ip: camera.ip, channel: channelID, event: evento, videoPath: null, imageBuffer: null };
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const tracks = [{ name: "termicoV", id: "201" }, { name: "termico", id: "203" }];
|
|
110
|
+
for (let t of tracks) {
|
|
111
|
+
const searchXml = `<?xml version="1.0" encoding="utf-8"?>
|
|
112
|
+
<CMSearchDescription>
|
|
113
|
+
<searchID>LAST_EVENT</searchID>
|
|
114
|
+
<trackIDList><trackID>${t.id}</trackID></trackIDList>
|
|
115
|
+
<timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList>
|
|
116
|
+
<maxResults>100</maxResults>
|
|
117
|
+
<searchResultPostion>0</searchResultPostion>
|
|
118
|
+
<metadataList><metadataDescriptor>//recordType.meta.std-cgi.com/${evento}</metadataDescriptor></metadataList>
|
|
119
|
+
</CMSearchDescription>`;
|
|
120
|
+
|
|
121
|
+
const resSearch = await camAuth.request({
|
|
122
|
+
method: 'POST',
|
|
123
|
+
url: `${baseUrl}/search`,
|
|
124
|
+
data: searchXml,
|
|
125
|
+
headers: { "Content-Type": "application/xml" }
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
129
|
+
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
130
|
+
|
|
131
|
+
if (uriMatch) {
|
|
132
|
+
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
133
|
+
const resDown = await camAuth.request({
|
|
134
|
+
method: 'GET', url: `${baseUrl}/download`,
|
|
135
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
136
|
+
responseType: 'arraybuffer'
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
let buffer = Buffer.from(resDown.data);
|
|
140
|
+
if (t.id === "203") {
|
|
141
|
+
output.imageBuffer = buffer;
|
|
142
|
+
} else {
|
|
143
|
+
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
144
|
+
const rawPath = path.join(tempDir, `raw_${Date.now()}.mp4`);
|
|
145
|
+
const fixedPath = path.join(tempDir, `hik_v_${channelID}_${Date.now()}.mp4`);
|
|
146
|
+
fs.writeFileSync(rawPath, buffer);
|
|
147
|
+
await new Promise((resolve) => {
|
|
148
|
+
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
149
|
+
if (!err) {
|
|
150
|
+
output.videoPath = fixedPath;
|
|
151
|
+
try { fs.unlinkSync(rawPath); } catch(e) {}
|
|
152
|
+
} else { output.videoPath = rawPath; }
|
|
153
|
+
resolve();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
setTimeout(() => { if (output.videoPath && fs.existsSync(output.videoPath)) fs.unlinkSync(output.videoPath); }, 180000);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (output.imageBuffer || output.videoPath) node.send({ payload: output });
|
|
161
|
+
} catch (e) {
|
|
162
|
+
node.error(`Errore Download Cam ${channelID}: ${e.message}`);
|
|
163
|
+
}
|
|
164
|
+
updateNodeStatus();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// --- GESTIONE NVR ALERT STREAM ---
|
|
168
|
+
function startAlertStream() {
|
|
169
|
+
if (isClosing) return;
|
|
170
|
+
const nvrAuth = new AxiosDigestAuth({
|
|
171
|
+
username: node.user,
|
|
172
|
+
password: node.pass
|
|
173
|
+
});
|
|
174
|
+
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|
|
175
|
+
|
|
176
|
+
nvrAuth.request({
|
|
177
|
+
method: 'GET', url: url, responseType: 'stream',
|
|
178
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
179
|
+
}).then(response => {
|
|
180
|
+
streamRequest = response;
|
|
181
|
+
if (!nvrOnline) {
|
|
182
|
+
node.send({ payload: { status: "online", ip: node.host, msg: "NVR Online" } });
|
|
183
|
+
nvrOnline = true;
|
|
184
|
+
}
|
|
185
|
+
updateNodeStatus();
|
|
186
|
+
|
|
187
|
+
response.data.on('data', (chunk) => {
|
|
188
|
+
const data = chunk.toString().toLowerCase();
|
|
189
|
+
if (data.includes("active")) {
|
|
190
|
+
const chMatch = data.match(/<channelid>(\d+)<\/channelid>/i);
|
|
191
|
+
if (chMatch) {
|
|
192
|
+
let ev = "Unknown";
|
|
193
|
+
for (let e of EventList) { if (data.includes(e.toLowerCase())) { ev = e; break; } }
|
|
194
|
+
downloadMedia(ev, chMatch[1]);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
response.data.on('error', () => handleNvrError());
|
|
200
|
+
response.data.on('end', () => !isClosing && setTimeout(startAlertStream, 5000));
|
|
201
|
+
}).catch(() => handleNvrError());
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function handleNvrError() {
|
|
205
|
+
if (nvrOnline) {
|
|
206
|
+
node.send({ payload: { status: "offline", ip: node.host, msg: "NVR Offline" } });
|
|
207
|
+
nvrOnline = false;
|
|
208
|
+
}
|
|
209
|
+
updateNodeStatus();
|
|
210
|
+
if (!isClosing) setTimeout(startAlertStream, 10000);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
startAlertStream();
|
|
214
|
+
setTimeout(checkCameras, 2000);
|
|
215
|
+
|
|
216
|
+
node.on('close', (done) => {
|
|
217
|
+
isClosing = true;
|
|
218
|
+
clearInterval(heartbeatInterval);
|
|
219
|
+
if (streamRequest) streamRequest.data.destroy();
|
|
220
|
+
done();
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
RED.nodes.registerType("hik-media-buffer", HikMediaBufferNode);
|
|
224
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script type="text/javascript">
|
|
2
|
+
RED.nodes.registerType('hik-snapshot', {
|
|
3
|
+
category: 'Hikvision',
|
|
4
|
+
color: '#00b4d8',
|
|
5
|
+
defaults: {
|
|
6
|
+
name: { value: "" },
|
|
7
|
+
host: { value: "", required: true },
|
|
8
|
+
channel: {value: "", required: true},
|
|
9
|
+
protocol: { value: "http" },
|
|
10
|
+
port: { value: "80", required: true },
|
|
11
|
+
user: { value: "admin", required: true },
|
|
12
|
+
pass: { value: "" },
|
|
13
|
+
|
|
14
|
+
},
|
|
15
|
+
inputs: 1,
|
|
16
|
+
outputs: 1,
|
|
17
|
+
icon: "font-awesome/fa-picture-o",
|
|
18
|
+
label: function() { return this.name || "Hik Snapshot"; },
|
|
19
|
+
|
|
20
|
+
});
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<script type="text/html" data-template-name="hik-snapshot">
|
|
24
|
+
<div class="form-row">
|
|
25
|
+
<label for="node-input-name"><i class="fa fa-tag"></i> Nome</label>
|
|
26
|
+
<input type="text" id="node-input-name">
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<hr>
|
|
30
|
+
<h4>Configurazione NVR</h4>
|
|
31
|
+
<div class="form-row">
|
|
32
|
+
<label for="node-input-host"><i class="fa fa-globe"></i> IP NVR</label>
|
|
33
|
+
<input type="text" id="node-input-host" placeholder="Es: 192.168.1.100">
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="form-row">
|
|
37
|
+
<label for="node-input-channel"><i class="fa fa-list-ol"></i> Numero Canali</label>
|
|
38
|
+
<input type="number" id="node-input-channel" min="1" max="64">
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="form-row">
|
|
42
|
+
<label for="node-input-user"><i class="fa fa-user"></i> Utente</label>
|
|
43
|
+
<input type="text" id="node-input-user">
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="form-row">
|
|
47
|
+
<label for="node-input-pass"><i class="fa fa-lock"></i> Password NVR</label>
|
|
48
|
+
<input type="password" id="node-input-pass">
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
<div class="form-row">
|
|
52
|
+
<label for="node-input-protocol"><i class="fa fa-shield"></i> Protocollo</label>
|
|
53
|
+
<select id="node-input-protocol" style="width: 70%">
|
|
54
|
+
<option value="http">HTTP</option>
|
|
55
|
+
<option value="https">HTTPS</option>
|
|
56
|
+
</select>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<div class="form-row">
|
|
60
|
+
<label for="node-input-port"><i class="fa fa-plug"></i> Porta</label>
|
|
61
|
+
<select id="node-input-port" style="width: 70%">
|
|
62
|
+
<option value="80">80</option>
|
|
63
|
+
<option value="443">443</option>
|
|
64
|
+
<option value="85">85</option>
|
|
65
|
+
<option value="8000">8000</option>
|
|
66
|
+
<option value="8008">8008</option>
|
|
67
|
+
</select>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
</script>
|
package/hik-snapshot.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const https = require('https');
|
|
3
|
+
const mhocDigestSnapshot = require('@mhoc/axios-digest-auth');
|
|
4
|
+
const DigestAuthClass = mhocDigestSnapshot.default;
|
|
5
|
+
|
|
6
|
+
module.exports = function(RED) {
|
|
7
|
+
function HikSnapshotNode(config) {
|
|
8
|
+
RED.nodes.createNode(this, config);
|
|
9
|
+
const node = this;
|
|
10
|
+
|
|
11
|
+
node.protocol = config.protocol || "http";
|
|
12
|
+
node.host = config.host;
|
|
13
|
+
node.port = config.port || "80";
|
|
14
|
+
node.user = config.user;
|
|
15
|
+
node.pass = config.pass;
|
|
16
|
+
node.maxChannels = parseInt(config.channel) || 1;
|
|
17
|
+
|
|
18
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
19
|
+
|
|
20
|
+
node.on('input', async function(msg) {
|
|
21
|
+
if (msg.payload !== true) return;
|
|
22
|
+
|
|
23
|
+
node.status({fill: "blue", shape: "dot", text: "Verifica canali..."});
|
|
24
|
+
|
|
25
|
+
const digest = new DigestAuthClass({
|
|
26
|
+
username: node.user,
|
|
27
|
+
password: node.pass
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const data = new Date();
|
|
31
|
+
const year = data.getFullYear();
|
|
32
|
+
const month = data.getMonth() + 1;
|
|
33
|
+
const day = data.getDate();
|
|
34
|
+
|
|
35
|
+
let snapshotResults = [];
|
|
36
|
+
|
|
37
|
+
for (let i = 1; i <= node.maxChannels; i++) {
|
|
38
|
+
const chanId = i + "01";
|
|
39
|
+
const snapUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/Streaming/channels/${chanId}/picture`;
|
|
40
|
+
const recordUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/record/tracks/${chanId}/dailyDistribution`;
|
|
41
|
+
|
|
42
|
+
const recordXml = `<?xml version="1.0" encoding="utf-8"?><trackDailyParam><year>${year}</year><monthOfYear>${month}</monthOfYear><dayOfMonth>${day}</dayOfMonth></trackDailyParam>`;
|
|
43
|
+
|
|
44
|
+
let resCanale = {
|
|
45
|
+
channel: i,
|
|
46
|
+
photo: null,
|
|
47
|
+
snapOk: false,
|
|
48
|
+
isRecording: false
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
// 1. SNAPSHOT
|
|
52
|
+
try {
|
|
53
|
+
const responseSnap = await digest.request({
|
|
54
|
+
method: 'GET',
|
|
55
|
+
url: snapUrl,
|
|
56
|
+
responseType: 'arraybuffer',
|
|
57
|
+
httpsAgent: node.protocol === 'https' ? httpsAgent : undefined,
|
|
58
|
+
timeout: 5000
|
|
59
|
+
});
|
|
60
|
+
resCanale.photo = responseSnap.data;
|
|
61
|
+
resCanale.snapOk = true;
|
|
62
|
+
} catch (err) {
|
|
63
|
+
resCanale.snapError = err.message;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. RECORDING
|
|
67
|
+
try {
|
|
68
|
+
const responseRec = await digest.request({
|
|
69
|
+
method: 'POST',
|
|
70
|
+
url: recordUrl,
|
|
71
|
+
data: recordXml,
|
|
72
|
+
headers: { 'Content-Type': 'application/xml' },
|
|
73
|
+
httpsAgent: node.protocol === 'https' ? httpsAgent : undefined,
|
|
74
|
+
timeout: 5000
|
|
75
|
+
});
|
|
76
|
+
const xmlOutput = responseRec.data.toString();
|
|
77
|
+
const regex = new RegExp(`<id>${day}</id>[^]*?<record>true</record>`);
|
|
78
|
+
resCanale.isRecording = regex.test(xmlOutput);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
resCanale.recError = err.message;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
snapshotResults.push(resCanale);
|
|
84
|
+
await new Promise(resolve => setTimeout(resolve, 200));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
msg.payload = snapshotResults;
|
|
88
|
+
node.send(msg);
|
|
89
|
+
|
|
90
|
+
// Conteggi per lo stato del nodo
|
|
91
|
+
const snapCount = snapshotResults.filter(v => v.snapOk).length;
|
|
92
|
+
const recCount = snapshotResults.filter(v => v.isRecording).length;
|
|
93
|
+
|
|
94
|
+
node.status({
|
|
95
|
+
fill: (snapCount === node.maxChannels && recCount === node.maxChannels) ? "green" : "yellow",
|
|
96
|
+
shape: "dot",
|
|
97
|
+
text: `Snap: ${snapCount}/${node.maxChannels} | Rec: ${recCount}/${node.maxChannels}`
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
RED.nodes.registerType("hik-snapshot", HikSnapshotNode);
|
|
102
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "node-red-contrib-hik-media-buffer",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
|
|
5
|
+
"node-red": {
|
|
6
|
+
"nodes": {
|
|
7
|
+
"hik-media-buffer": "hik-media-buffer.js",
|
|
8
|
+
"hik-snapshot": "hik-snapshot.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.6.0",
|
|
13
|
+
"@mhoc/axios-digest-auth": "^0.8.0",
|
|
14
|
+
"xml2js": "^0.6.2"
|
|
15
|
+
}
|
|
16
|
+
}
|