node-red-contrib-hik-media-buffer 1.1.107 → 1.1.109
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 +87 -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 identiche a quelle viste su 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:`Invio query protetta...`});
|
|
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,102 @@ module.exports = function(RED) {
|
|
|
118
117
|
let fileDaCancellare = [];
|
|
119
118
|
|
|
120
119
|
try {
|
|
121
|
-
|
|
122
|
-
|
|
120
|
+
// --- GENERAZIONE HASH DI SESSIONE LOCALE ---
|
|
121
|
+
// Generiamo un MD5 casuale per bypassare il controllo di uguaglianza tra Cookie e Sessiontag
|
|
122
|
+
const fakeSessionToken = require('crypto').createHash('md5').update(String(Math.random() + Date.now())).digest('hex');
|
|
123
|
+
const cookieKeyName = `WebSession_${require('crypto').createHash('md5').update(node.host).digest('hex').substring(0, 10)}`;
|
|
124
|
+
|
|
125
|
+
node.warn(`[DEBUG SESSION] Simulazione Header -> ${cookieKeyName}=${fakeSessionToken}`);
|
|
123
126
|
|
|
124
|
-
const
|
|
125
|
-
|
|
127
|
+
const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
128
|
+
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
129
|
+
return v.toString(16).toUpperCase();
|
|
130
|
+
});
|
|
126
131
|
|
|
127
|
-
const
|
|
128
|
-
|
|
132
|
+
const searchPayload = {
|
|
133
|
+
"EventSearchDescription": {
|
|
134
|
+
"searchID": dynamicSearchId,
|
|
135
|
+
"searchResultPosition": 0,
|
|
136
|
+
"maxResults": 30,
|
|
137
|
+
"timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
|
|
138
|
+
"type": "all",
|
|
139
|
+
"channels": [ parseInt(channelID) ],
|
|
140
|
+
"eventType": "behavior",
|
|
141
|
+
"behavior": { "behaviorEventType": evento.toLowerCase() }
|
|
142
|
+
}
|
|
143
|
+
};
|
|
129
144
|
|
|
130
|
-
|
|
131
|
-
|
|
145
|
+
const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
|
|
146
|
+
fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
|
|
132
147
|
|
|
133
|
-
const
|
|
148
|
+
const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
|
|
149
|
+
|
|
150
|
+
// Compiliamo il comando cURL inserendo gli header emersi dall'analisi di Chrome
|
|
151
|
+
// Usiamo sia il content-type form-urlencoded sia la coppia speculare cookie/sessiontag
|
|
152
|
+
const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" ` +
|
|
153
|
+
`-H "Content-Type: application/x-www-form-urlencoded; charset=UTF-8" ` +
|
|
154
|
+
`-H "X-Requested-With: XMLHttpRequest" ` +
|
|
155
|
+
`-H "sessiontag: ${fakeSessionToken}" ` +
|
|
156
|
+
`-b "${cookieKeyName}=${fakeSessionToken}; updatePlugin=false" ` +
|
|
157
|
+
`-d "@${tempJsonPath}" "${targetUrl}"`;
|
|
158
|
+
|
|
159
|
+
let jsonResponseRaw = await new Promise((resolve) => { exec(curlCommand, (err, stdout) => resolve(err ? null : stdout)); });
|
|
134
160
|
|
|
135
|
-
if (
|
|
136
|
-
|
|
161
|
+
try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
|
|
162
|
+
|
|
163
|
+
if (!jsonResponseRaw) {
|
|
164
|
+
node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
|
|
137
165
|
updateNodeStatus();
|
|
138
166
|
return;
|
|
139
167
|
}
|
|
140
168
|
|
|
141
|
-
const
|
|
142
|
-
|
|
169
|
+
const searchResult = JSON.parse(jsonResponseRaw);
|
|
170
|
+
const matches = searchResult?.EventSearchResult?.matchList || [];
|
|
143
171
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
172
|
+
if (matches.length === 0) {
|
|
173
|
+
node.warn(`[DEBUG JSON] Zero match trovati nell'archivio protetto.`);
|
|
174
|
+
updateNodeStatus();
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
147
177
|
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
178
|
+
// Se superiamo il blocco, estraiamo i file storici
|
|
179
|
+
const matchItem = matches[0];
|
|
180
|
+
const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
|
|
181
|
+
|
|
182
|
+
if (playbackUri) {
|
|
183
|
+
node.warn(`[DEBUG JSON] File individuato: ${playbackUri}`);
|
|
184
|
+
|
|
185
|
+
// --- DOWNLOAD FOTO REGISTRATA ---
|
|
186
|
+
let pictureUrlPath = null;
|
|
187
|
+
const urlParamsMatch = playbackUri.match(/\?(.*)/);
|
|
188
|
+
if (urlParamsMatch) {
|
|
189
|
+
const trackI = (channelID * 100 + 3).toString();
|
|
190
|
+
pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
|
|
191
|
+
}
|
|
153
192
|
|
|
154
|
-
|
|
193
|
+
if (pictureUrlPath) {
|
|
194
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
195
|
+
const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" -b "${cookieKeyName}=${fakeSessionToken}" -H "sessiontag: ${fakeSessionToken}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
|
|
196
|
+
await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
|
|
197
|
+
if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
|
|
198
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
199
|
+
fileDaCancellare.push(fullImgPath);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
155
202
|
|
|
156
|
-
|
|
157
|
-
|
|
203
|
+
// --- DOWNLOAD VIDEO REGISTRATO VIA FFMPEG ---
|
|
204
|
+
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
205
|
+
const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
|
|
206
|
+
const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
|
|
158
207
|
|
|
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()); });
|
|
208
|
+
await new Promise((resolve) => { exec(ffmpegRtspCmd, { timeout: 25000 }, () => resolve()); });
|
|
163
209
|
|
|
164
|
-
if (fs.existsSync(fixedPath)) {
|
|
210
|
+
if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
|
|
165
211
|
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
166
212
|
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
213
|
}
|
|
177
|
-
fileDaCancellare.push(rawVideoPath);
|
|
178
|
-
} else {
|
|
179
|
-
node.warn(`[DEBUG HTTP] Il download del file da NVR è fallito o il file è vuoto.`);
|
|
180
214
|
}
|
|
181
215
|
|
|
182
|
-
// --- SPEDIZIONE PAYLOAD ---
|
|
183
216
|
if (output.foto_base64 || output.video_base64) {
|
|
184
217
|
node.send({ payload: output });
|
|
185
218
|
setTimeout(() => {
|
|
@@ -190,7 +223,7 @@ module.exports = function(RED) {
|
|
|
190
223
|
}
|
|
191
224
|
|
|
192
225
|
} catch (e) {
|
|
193
|
-
node.error(`Errore
|
|
226
|
+
node.error(`Errore query speculare: ${e.message}`);
|
|
194
227
|
}
|
|
195
228
|
updateNodeStatus();
|
|
196
229
|
}
|