node-red-contrib-hik-media-buffer 1.1.83 → 1.1.85
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 +128 -47
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const axios = require('axios');
|
|
2
2
|
const AxiosDigestAuth = require('@mhoc/axios-digest-auth').default;
|
|
3
|
+
const http = require('http'); // Modulo nativo di Node.js per sconfiggere il 400
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const path = require('path');
|
|
5
6
|
const os = require('os');
|
|
@@ -31,9 +32,94 @@ module.exports = function(RED) {
|
|
|
31
32
|
|
|
32
33
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
33
34
|
|
|
34
|
-
//
|
|
35
|
+
// Manteniamo AxiosDigest solo per lo stream degli allarmi e lo stato dei canali (dove va da dio)
|
|
35
36
|
const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
36
37
|
|
|
38
|
+
// --- FUNZIONE AUSILIARIA PER FARE LA POST DIGEST CON HTTP NATIVO (ELIMINA IL 400) ---
|
|
39
|
+
function nativeDigestPost(urlPath, xmlBody) {
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
// Primo invio a vuoto per scatenare il 401 dell'handshake Digest
|
|
42
|
+
const options = {
|
|
43
|
+
hostname: node.host,
|
|
44
|
+
port: node.port,
|
|
45
|
+
path: urlPath,
|
|
46
|
+
method: 'POST',
|
|
47
|
+
headers: { 'Content-Type': 'application/xml', 'Content-Length': Buffer.byteLength(xmlBody) }
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const req = http.request(options, (res) => {
|
|
51
|
+
let chunks = [];
|
|
52
|
+
res.on('data', (chunk) => chunks.push(chunk));
|
|
53
|
+
res.on('end', () => {
|
|
54
|
+
const body = Buffer.concat(chunks).toString();
|
|
55
|
+
|
|
56
|
+
// Se l'NVR risponde 401 (normale al primo colpo), estraiamo i parametri Digest
|
|
57
|
+
if (res.statusCode === 401) {
|
|
58
|
+
const authHeader = res.headers['www-authenticate'];
|
|
59
|
+
if (!authHeader) return reject(new Error("Manca header WWW-Authenticate"));
|
|
60
|
+
|
|
61
|
+
// Estrazione parametri con regex
|
|
62
|
+
const realm = authHeader.match(/realm="([^"]+)"/)[1];
|
|
63
|
+
const nonce = authHeader.match(/nonce="([^"]+)"/)[1];
|
|
64
|
+
const qopMatch = authHeader.match(/qop="([^"]+)"/);
|
|
65
|
+
const qop = qopMatch ? qopMatch[1] : null;
|
|
66
|
+
|
|
67
|
+
// Calcolo degli Hash MD5 standard dell'handshake (Come fa Chrome!)
|
|
68
|
+
const crypto = require('crypto');
|
|
69
|
+
const md5 = (str) => crypto.createHash('md5').update(str).digest('hex');
|
|
70
|
+
|
|
71
|
+
const ha1 = md5(`${node.user}:${realm}:${node.pass}`);
|
|
72
|
+
const ha2 = md5(`POST:${urlPath}`);
|
|
73
|
+
|
|
74
|
+
let authString = `Digest username="${node.user}", realm="${realm}", nonce="${nonce}", uri="${urlPath}", response=""`;
|
|
75
|
+
|
|
76
|
+
if (qop) {
|
|
77
|
+
const cnonce = crypto.randomBytes(8).toString('hex');
|
|
78
|
+
const nc = "00000001";
|
|
79
|
+
const response = md5(`${ha1}:${nonce}:${nc}:${cnonce}:${qop}:${ha2}`);
|
|
80
|
+
authString = `Digest username="${node.user}", realm="${realm}", nonce="${nonce}", uri="${urlPath}", qop=${qop}, nc=${nc}, cnonce="${cnonce}", response="${response}"`;
|
|
81
|
+
} else {
|
|
82
|
+
const response = md5(`${ha1}:${nonce}:${ha2}`);
|
|
83
|
+
authString = `Digest username="${node.user}", realm="${realm}", nonce="${nonce}", uri="${urlPath}", response="${response}"`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Secondo invio definitivo con l'autenticazione calcolata al millimetro
|
|
87
|
+
const finalOptions = {
|
|
88
|
+
hostname: node.host,
|
|
89
|
+
port: node.port,
|
|
90
|
+
path: urlPath,
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/xml',
|
|
94
|
+
'Authorization': authString,
|
|
95
|
+
'Content-Length': Buffer.byteLength(xmlBody)
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const finalReq = http.request(finalOptions, (finalRes) => {
|
|
100
|
+
let finalChunks = [];
|
|
101
|
+
finalRes.on('data', (c) => finalChunks.push(c));
|
|
102
|
+
finalRes.on('end', () => {
|
|
103
|
+
resolve({
|
|
104
|
+
statusCode: finalRes.statusCode,
|
|
105
|
+
data: Buffer.concat(finalChunks)
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
finalReq.on('error', (err) => reject(err));
|
|
110
|
+
finalReq.write(xmlBody);
|
|
111
|
+
finalReq.end();
|
|
112
|
+
} else {
|
|
113
|
+
resolve({ statusCode: res.statusCode, data: body });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
req.on('error', (err) => reject(err));
|
|
118
|
+
req.write(xmlBody);
|
|
119
|
+
req.end();
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
37
123
|
// --- PRENDE IL NOME DELLA TELECAMERA DALL'NVR ---
|
|
38
124
|
async function getCameraName(channelID) {
|
|
39
125
|
try {
|
|
@@ -101,7 +187,7 @@ module.exports = function(RED) {
|
|
|
101
187
|
|
|
102
188
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
103
189
|
|
|
104
|
-
// --- DOWNLOAD MEDIA
|
|
190
|
+
// --- DOWNLOAD MEDIA RIGENERATO CON MOTORE NATIVO ---
|
|
105
191
|
async function downloadMedia(evento, channelID) {
|
|
106
192
|
const chStr = channelID.toString();
|
|
107
193
|
const nowTime = Date.now();
|
|
@@ -112,8 +198,8 @@ module.exports = function(RED) {
|
|
|
112
198
|
const nomeCamera = await getCameraName(channelID);
|
|
113
199
|
const referenceTime = new Date();
|
|
114
200
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
115
|
-
const startTime = toHikDate(new Date(referenceTime.getTime() - (
|
|
116
|
-
const endTime = toHikDate(new Date(referenceTime.getTime() + (
|
|
201
|
+
const startTime = toHikDate(new Date(referenceTime.getTime() - (15 * 1000))); // Allargato a 15s per sicurezza
|
|
202
|
+
const endTime = toHikDate(new Date(referenceTime.getTime() + (15 * 1000)));
|
|
117
203
|
|
|
118
204
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
119
205
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
@@ -139,55 +225,50 @@ module.exports = function(RED) {
|
|
|
139
225
|
searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${t.id}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${evento}</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
140
226
|
}
|
|
141
227
|
|
|
142
|
-
//
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
const resSearch = await nvrAuthAxios.request({
|
|
146
|
-
method: 'POST',
|
|
147
|
-
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
|
|
148
|
-
data: xmlBuffer,
|
|
149
|
-
headers: {
|
|
150
|
-
"Content-Type": "application/xml; charset=UTF-8",
|
|
151
|
-
"Content-Length": xmlBuffer.length
|
|
152
|
-
}
|
|
153
|
-
});
|
|
228
|
+
// USIAMO IL MOTORE NATIVO: Ritorna la risposta dell'NVR pulita senza subire il bug di Axios!
|
|
229
|
+
const resSearch = await nativeDigestPost('/ISAPI/ContentMgmt/search', searchXml);
|
|
154
230
|
|
|
155
|
-
|
|
231
|
+
if (resSearch.statusCode !== 200) {
|
|
232
|
+
node.error(`[NVR REJECT] La ricerca ha risposto con codice errore: ${resSearch.statusCode}`);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let xml = resSearch.data.toString().replace(/<(\/?)\w+:/g, "<$1");
|
|
156
237
|
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
157
238
|
|
|
158
239
|
if (uriMatch) {
|
|
159
240
|
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
241
|
+
const downXml = `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`;
|
|
160
242
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
resolve();
|
|
243
|
+
// Anche il download lo facciamo con il motore nativo per evitare sballi binari
|
|
244
|
+
const resDown = await nativeDigestPost('/ISAPI/ContentMgmt/download', downXml);
|
|
245
|
+
|
|
246
|
+
if (resDown.statusCode === 200) {
|
|
247
|
+
let buffer = resDown.data;
|
|
248
|
+
if (t.id === trackI) {
|
|
249
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
250
|
+
fs.writeFileSync(fullImgPath, buffer);
|
|
251
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
252
|
+
fileDaCancellare.push(fullImgPath);
|
|
253
|
+
} else {
|
|
254
|
+
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
255
|
+
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
256
|
+
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
257
|
+
fs.writeFileSync(rawPath, buffer);
|
|
258
|
+
|
|
259
|
+
await new Promise((resolve) => {
|
|
260
|
+
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
261
|
+
if (!err && fs.existsSync(fixedPath)) {
|
|
262
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
263
|
+
fileDaCancellare.push(fixedPath);
|
|
264
|
+
} else {
|
|
265
|
+
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
266
|
+
}
|
|
267
|
+
fileDaCancellare.push(rawPath);
|
|
268
|
+
resolve();
|
|
269
|
+
});
|
|
189
270
|
});
|
|
190
|
-
}
|
|
271
|
+
}
|
|
191
272
|
}
|
|
192
273
|
}
|
|
193
274
|
}
|
|
@@ -207,7 +288,7 @@ module.exports = function(RED) {
|
|
|
207
288
|
updateNodeStatus();
|
|
208
289
|
}
|
|
209
290
|
|
|
210
|
-
// --- ALERT STREAM CON AXIOS ---
|
|
291
|
+
// --- ALERT STREAM CON AXIOS (Super Stabile) ---
|
|
211
292
|
function startAlertStream() {
|
|
212
293
|
if (isClosing) return;
|
|
213
294
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|