node-red-contrib-hik-media-buffer 1.1.107 → 1.1.108
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 +115 -54
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -97,18 +97,17 @@ module.exports = function(RED) {
|
|
|
97
97
|
const referenceTime = new Date();
|
|
98
98
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const startTime =
|
|
107
|
-
const endTime =
|
|
108
|
-
|
|
109
|
-
node.status({fill:"yellow", shape:"dot", text:`Ricerca file NVR...`});
|
|
110
|
-
await new Promise(resolve => setTimeout(resolve, 5000)); // Aspetta che l'NVR crei il file
|
|
100
|
+
// Costruiamo le date giornaliere stile Chrome
|
|
101
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
102
|
+
const year = referenceTime.getFullYear();
|
|
103
|
+
const month = pad(referenceTime.getMonth() + 1);
|
|
104
|
+
const day = pad(referenceTime.getDate());
|
|
105
|
+
|
|
106
|
+
const startTime = `${year}-${month}-${day}T00:00:00 01:00`;
|
|
107
|
+
const endTime = `${year}-${month}-${day}T23:59:59 01:00`;
|
|
111
108
|
|
|
109
|
+
node.status({fill:"yellow", shape:"dot", text:`Generazione Sessione...`});
|
|
110
|
+
|
|
112
111
|
let output = {
|
|
113
112
|
tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
|
|
114
113
|
ip_telecamera: null, tipo_evento: evento, timestamp_epoch: timestamp,
|
|
@@ -118,68 +117,130 @@ module.exports = function(RED) {
|
|
|
118
117
|
let fileDaCancellare = [];
|
|
119
118
|
|
|
120
119
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
// --- STEP 1: CHIAMATA PRE-LOGIN PER ESTREMARE IL COOKIE WEBSESSION ---
|
|
121
|
+
// Interroghiamo un endpoint ISAPI leggero in Digest per farci rilasciare il Cookie di sessione
|
|
122
|
+
const loginRes = await nvrAuthAxios.request({
|
|
123
|
+
method: 'GET',
|
|
124
|
+
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/Security/sessionLogin/capabilities?format=json`,
|
|
125
|
+
timeout: 5000
|
|
126
|
+
});
|
|
123
127
|
|
|
124
|
-
|
|
125
|
-
|
|
128
|
+
// Estraiamo il cookie "WebSession" dagli header di risposta
|
|
129
|
+
const responseHeaders = loginRes.headers['set-cookie'] || [];
|
|
130
|
+
let webSessionValue = '';
|
|
131
|
+
let cookieKeyName = 'WebSession';
|
|
132
|
+
|
|
133
|
+
for (let cookie of responseHeaders) {
|
|
134
|
+
if (cookie.includes('WebSession')) {
|
|
135
|
+
const matchCookie = cookie.match(/(WebSession_[a-f0-9]+|WebSession)=([^;]+)/i);
|
|
136
|
+
if (matchCookie) {
|
|
137
|
+
cookieKeyName = matchCookie[1];
|
|
138
|
+
webSessionValue = matchCookie[2];
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Se l'NVR non ha sputato il cookie, ne generiamo uno fittizio in MD5 basandoci sul timestamp
|
|
144
|
+
// perché in molti firmware basta che l'header esista ed abbia lo stesso valore del cookie!
|
|
145
|
+
if (!webSessionValue) {
|
|
146
|
+
webSessionValue = require('crypto').createHash('md5').update(String(Date.now())).digest('hex');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
node.warn(`[DEBUG SESSION] Clonazione Cookie -> ${cookieKeyName}=${webSessionValue}`);
|
|
150
|
+
|
|
151
|
+
// --- STEP 2: COSTRUZIONE QUERY DIRETTA CON DATI DI CHROME ---
|
|
152
|
+
const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
153
|
+
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
154
|
+
return v.toString(16).toUpperCase();
|
|
155
|
+
});
|
|
126
156
|
|
|
127
|
-
const
|
|
128
|
-
|
|
157
|
+
const searchPayload = {
|
|
158
|
+
"EventSearchDescription": {
|
|
159
|
+
"searchID": dynamicSearchId,
|
|
160
|
+
"searchResultPosition": 0,
|
|
161
|
+
"maxResults": 30,
|
|
162
|
+
"timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
|
|
163
|
+
"type": "all",
|
|
164
|
+
"channels": [ parseInt(channelID) ],
|
|
165
|
+
"eventType": "behavior",
|
|
166
|
+
"behavior": { "behaviorEventType": evento.toLowerCase() }
|
|
167
|
+
}
|
|
168
|
+
};
|
|
129
169
|
|
|
130
|
-
|
|
131
|
-
|
|
170
|
+
const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
|
|
171
|
+
fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
|
|
132
172
|
|
|
133
|
-
|
|
173
|
+
// --- STEP 3: LANCE DIRETTO VIA CURL CON I TRE ASSI NELLA MANICA ---
|
|
174
|
+
// 1. Content-Type form-urlencoded (anche se mandiamo un JSON, come fa Chrome!)
|
|
175
|
+
// 2. Cookie WebSession inserito a mano
|
|
176
|
+
// 3. sessiontag identico al valore del cookie
|
|
177
|
+
const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
|
|
178
|
+
|
|
179
|
+
const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" ` +
|
|
180
|
+
`-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" ` +
|
|
181
|
+
`-H "X-Requested-With: XMLHttpRequest" ` +
|
|
182
|
+
`-H "sessiontag: ${webSessionValue}" ` +
|
|
183
|
+
`-b "${cookieKeyName}=${webSessionValue}" ` +
|
|
184
|
+
`-d "@${tempJsonPath}" "${targetUrl}"`;
|
|
185
|
+
|
|
186
|
+
node.warn(`[DEBUG JSON] Invio query protetta con token di sessione...`);
|
|
187
|
+
let jsonResponseRaw = await new Promise((resolve) => { exec(curlCommand, (err, stdout) => resolve(err ? null : stdout)); });
|
|
134
188
|
|
|
135
|
-
if (
|
|
136
|
-
|
|
189
|
+
try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
|
|
190
|
+
|
|
191
|
+
if (!jsonResponseRaw) {
|
|
192
|
+
node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
|
|
137
193
|
updateNodeStatus();
|
|
138
194
|
return;
|
|
139
195
|
}
|
|
140
196
|
|
|
141
|
-
const
|
|
142
|
-
|
|
197
|
+
const searchResult = JSON.parse(jsonResponseRaw);
|
|
198
|
+
const matches = searchResult?.EventSearchResult?.matchList || [];
|
|
143
199
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
200
|
+
if (matches.length === 0) {
|
|
201
|
+
node.warn(`[DEBUG JSON] Zero match trovati. La sessione non è bastata o i parametri differiscono.`);
|
|
202
|
+
updateNodeStatus();
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
147
205
|
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
206
|
+
// Se arriviamo qui, l'array si è sbloccato!
|
|
207
|
+
const matchItem = matches[0];
|
|
208
|
+
const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
|
|
209
|
+
|
|
210
|
+
if (playbackUri) {
|
|
211
|
+
node.warn(`[DEBUG JSON] Match sbloccato con successo! URL: ${playbackUri}`);
|
|
212
|
+
|
|
213
|
+
// --- DOWNLOAD FOTO NATIVA REGISTRATA SU HDD ---
|
|
214
|
+
let pictureUrlPath = null;
|
|
215
|
+
const urlParamsMatch = playbackUri.match(/\?(.*)/);
|
|
216
|
+
if (urlParamsMatch) {
|
|
217
|
+
const trackI = (channelID * 100 + 3).toString();
|
|
218
|
+
pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
|
|
219
|
+
}
|
|
153
220
|
|
|
154
|
-
|
|
221
|
+
if (pictureUrlPath) {
|
|
222
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
223
|
+
const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" -b "${cookieKeyName}=${webSessionValue}" -H "sessiontag: ${webSessionValue}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
|
|
224
|
+
await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
|
|
225
|
+
if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
|
|
226
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
227
|
+
fileDaCancellare.push(fullImgPath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
155
230
|
|
|
156
|
-
|
|
157
|
-
|
|
231
|
+
// --- DOWNLOAD VIDEO NATIVO REGISTRATO SU HDD VIA FFMPEG ---
|
|
232
|
+
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
233
|
+
const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
|
|
234
|
+
const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
|
|
158
235
|
|
|
159
|
-
|
|
160
|
-
// Visto che il file è sul tuo PC, FFmpeg lavora in locale (velocità istantanea, zero timeout di rete!)
|
|
161
|
-
const ffmpegCutCmd = `ffmpeg -y -i "${rawVideoPath}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
|
|
162
|
-
await new Promise((resolve) => { exec(ffmpegCutCmd, () => resolve()); });
|
|
236
|
+
await new Promise((resolve) => { exec(ffmpegRtspCmd, { timeout: 25000 }, () => resolve()); });
|
|
163
237
|
|
|
164
|
-
if (fs.existsSync(fixedPath)) {
|
|
238
|
+
if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
|
|
165
239
|
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
166
240
|
fileDaCancellare.push(fixedPath);
|
|
167
|
-
|
|
168
|
-
// --- 3. ESTRAZIONE FOTO DAL VIDEO REGISTRATO ---
|
|
169
|
-
const ffmpegImgCmd = `ffmpeg -y -i "${fixedPath}" -vframes 1 -q:v 2 "${fullImgPath}"`;
|
|
170
|
-
await new Promise((resolve) => { exec(ffmpegImgCmd, () => resolve()); });
|
|
171
|
-
|
|
172
|
-
if (fs.existsSync(fullImgPath)) {
|
|
173
|
-
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
174
|
-
fileDaCancellare.push(fullImgPath);
|
|
175
|
-
}
|
|
176
241
|
}
|
|
177
|
-
fileDaCancellare.push(rawVideoPath);
|
|
178
|
-
} else {
|
|
179
|
-
node.warn(`[DEBUG HTTP] Il download del file da NVR è fallito o il file è vuoto.`);
|
|
180
242
|
}
|
|
181
243
|
|
|
182
|
-
// --- SPEDIZIONE PAYLOAD ---
|
|
183
244
|
if (output.foto_base64 || output.video_base64) {
|
|
184
245
|
node.send({ payload: output });
|
|
185
246
|
setTimeout(() => {
|
|
@@ -190,7 +251,7 @@ module.exports = function(RED) {
|
|
|
190
251
|
}
|
|
191
252
|
|
|
192
253
|
} catch (e) {
|
|
193
|
-
node.error(`Errore
|
|
254
|
+
node.error(`Errore session-cloner: ${e.message}`);
|
|
194
255
|
}
|
|
195
256
|
updateNodeStatus();
|
|
196
257
|
}
|