node-red-contrib-hik-media-buffer 1.1.100 → 1.1.101
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 +53 -49
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -28,7 +28,7 @@ module.exports = function(RED) {
|
|
|
28
28
|
|
|
29
29
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
30
30
|
|
|
31
|
-
//
|
|
31
|
+
// Formatta la data simulando il fuso orario senza il segno '+'
|
|
32
32
|
function toJsonDate(d) {
|
|
33
33
|
const pad = (num) => String(num).padStart(2, '0');
|
|
34
34
|
const year = d.getFullYear();
|
|
@@ -39,14 +39,12 @@ module.exports = function(RED) {
|
|
|
39
39
|
const seconds = pad(d.getSeconds());
|
|
40
40
|
|
|
41
41
|
const offsetMinutes = d.getTimezoneOffset();
|
|
42
|
-
// Chiariamo l'offset senza inserire il '+' che fa incazzare l'NVR
|
|
43
42
|
const offsetSign = offsetMinutes <= 0 ? '' : '-';
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
const
|
|
43
|
+
const offsetHours = pad(Math.floor(Math.abs(offsetMinutes) / 60));
|
|
44
|
+
const offsetMins = pad(Math.abs(offsetMinutes) % 60);
|
|
45
|
+
const fusoOrario = `${offsetSign}${offsetHours}:${offsetMins}`;
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds} ${offsetSign}${offsetHours}:${offsetMins}`;
|
|
47
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds} ${fusoOrario}`;
|
|
50
48
|
}
|
|
51
49
|
|
|
52
50
|
const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
@@ -106,7 +104,7 @@ module.exports = function(RED) {
|
|
|
106
104
|
|
|
107
105
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
108
106
|
|
|
109
|
-
// ---
|
|
107
|
+
// --- CORE: RICERCA ARCHIVIO GIORNALIERO SIMULANDO CHROME ---
|
|
110
108
|
async function downloadMedia(evento, channelID) {
|
|
111
109
|
const chStr = channelID.toString();
|
|
112
110
|
const nowTime = Date.now();
|
|
@@ -117,19 +115,25 @@ module.exports = function(RED) {
|
|
|
117
115
|
const nomeCamera = await getCameraName(channelID);
|
|
118
116
|
const referenceTime = new Date();
|
|
119
117
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
120
|
-
|
|
121
|
-
// Cerchiamo nell'ora corrente (da inizio ora a fine ora) per non fare tutto il giorno
|
|
122
|
-
const dStart = new Date(referenceTime);
|
|
123
|
-
dStart.setMinutes(0, 0, 0);
|
|
124
|
-
const dEnd = new Date(referenceTime);
|
|
125
|
-
dEnd.setMinutes(59, 59, 999);
|
|
126
118
|
|
|
127
|
-
|
|
128
|
-
const
|
|
119
|
+
// Generazione dell'intervallo per l'intera giornata corrente
|
|
120
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
121
|
+
const year = referenceTime.getFullYear();
|
|
122
|
+
const month = pad(referenceTime.getMonth() + 1);
|
|
123
|
+
const day = pad(referenceTime.getDate());
|
|
124
|
+
|
|
125
|
+
const offsetMinutes = referenceTime.getTimezoneOffset();
|
|
126
|
+
const offsetSign = offsetMinutes <= 0 ? '' : '-';
|
|
127
|
+
const offsetHours = pad(Math.floor(Math.abs(offsetMinutes) / 60));
|
|
128
|
+
const offsetMins = pad(Math.abs(offsetMinutes) % 60);
|
|
129
|
+
const fusoOrario = `${offsetSign}${offsetHours}:${offsetMins}`;
|
|
130
|
+
|
|
131
|
+
const startTime = `${year}-${month}-${day}T00:00:00 ${fusoOrario}`;
|
|
132
|
+
const endTime = `${year}-${month}-${day}T23:59:59 ${fusoOrario}`;
|
|
129
133
|
|
|
130
|
-
node.status({fill:"yellow", shape:"dot", text:`Ricerca
|
|
134
|
+
node.status({fill:"yellow", shape:"dot", text:`Ricerca archivio Cam ${channelID}...`});
|
|
131
135
|
|
|
132
|
-
// Aspettiamo 4 secondi per
|
|
136
|
+
// Aspettiamo 4 secondi per dare tempo all'NVR di indicizzare il log su disco
|
|
133
137
|
await new Promise(resolve => setTimeout(resolve, 4000));
|
|
134
138
|
|
|
135
139
|
let output = {
|
|
@@ -141,13 +145,13 @@ module.exports = function(RED) {
|
|
|
141
145
|
let fileDaCancellare = [];
|
|
142
146
|
|
|
143
147
|
try {
|
|
144
|
-
//
|
|
148
|
+
// SearchID UUID Dinamico ad ogni sessione
|
|
145
149
|
const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
146
150
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
147
151
|
return v.toString(16).toUpperCase();
|
|
148
152
|
});
|
|
149
153
|
|
|
150
|
-
// Payload clonato
|
|
154
|
+
// Payload clonato dal Form Data di Chrome
|
|
151
155
|
const searchPayload = {
|
|
152
156
|
"EventSearchDescription": {
|
|
153
157
|
"searchID": dynamicSearchId,
|
|
@@ -164,9 +168,11 @@ module.exports = function(RED) {
|
|
|
164
168
|
const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
|
|
165
169
|
fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
|
|
166
170
|
|
|
167
|
-
node.warn(`[DEBUG JSON] Invio query con data stile Chrome...`);
|
|
171
|
+
node.warn(`[DEBUG JSON] Invio query giornaliera con data stile Chrome...`);
|
|
168
172
|
const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
|
|
169
|
-
|
|
173
|
+
|
|
174
|
+
// Forziamo gli header completi ricavati dall'analisi del client web
|
|
175
|
+
const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/json; charset=UTF-8" -H "X-Requested-With: XMLHttpRequest" -d "@${tempJsonPath}" "${targetUrl}"`;
|
|
170
176
|
|
|
171
177
|
let jsonResponseRaw = await new Promise((resolve) => {
|
|
172
178
|
exec(curlCommand, (err, stdout) => { resolve(err ? null : stdout); });
|
|
@@ -184,48 +190,46 @@ module.exports = function(RED) {
|
|
|
184
190
|
const matches = searchResult?.EventSearchResult?.matchList || [];
|
|
185
191
|
|
|
186
192
|
if (matches.length === 0) {
|
|
187
|
-
node.warn(`[DEBUG JSON] Zero match trovati nell'
|
|
193
|
+
node.warn(`[DEBUG JSON] Zero match trovati nell'archivio giornaliero.`);
|
|
188
194
|
updateNodeStatus();
|
|
189
195
|
return;
|
|
190
196
|
}
|
|
191
197
|
|
|
192
|
-
// Prendiamo il record più fresco (
|
|
198
|
+
// Prendiamo il record d'archivio più fresco (l'ultimo registrato)
|
|
193
199
|
const matchItem = matches[0];
|
|
194
200
|
const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
|
|
195
201
|
|
|
196
|
-
// --- 1. DOWNLOAD FOTO ORIGINALE DAL PATH NATIVO GET ---
|
|
197
|
-
let pictureUrlPath = null;
|
|
198
202
|
if (playbackUri) {
|
|
203
|
+
node.warn(`[DEBUG JSON] Trovato PlaybackURI: ${playbackUri}`);
|
|
204
|
+
|
|
205
|
+
// --- 1. DOWNLOAD FOTO HISTORIC (GET diretta sulla traccia foto 203) ---
|
|
206
|
+
let pictureUrlPath = null;
|
|
199
207
|
const urlParamsMatch = playbackUri.match(/\?(.*)/);
|
|
200
208
|
if (urlParamsMatch) {
|
|
201
|
-
const trackI = (channelID * 100 + 3).toString();
|
|
209
|
+
const trackI = (channelID * 100 + 3).toString();
|
|
202
210
|
pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
|
|
203
211
|
}
|
|
204
|
-
}
|
|
205
212
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
if (pictureUrlPath) {
|
|
214
|
+
node.warn(`[DEBUG FOTO] Scarico lo snapshot nativo d'archivio via GET...`);
|
|
215
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
216
|
+
const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
|
|
217
|
+
|
|
218
|
+
await new Promise((resolve) => { exec(curlImgCmd, () => resolve()); });
|
|
219
|
+
|
|
220
|
+
if (fs.existsSync(fullImgPath) && fs.statSync(fullImgPath).size > 100) {
|
|
221
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
222
|
+
fileDaCancellare.push(fullImgPath);
|
|
223
|
+
node.warn(`[DEBUG FOTO] Foto scaricata con successo.`);
|
|
224
|
+
}
|
|
217
225
|
}
|
|
218
|
-
}
|
|
219
226
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP temporizzato dell'evento...`);
|
|
227
|
+
// --- 2. DOWNLOAD VIDEO HISTORIC VIA RTSP (Taglio a 15 secondi) ---
|
|
228
|
+
node.warn(`[DEBUG VIDEO] Recupero lo spezzone video registrato via RTSP...`);
|
|
223
229
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
224
|
-
|
|
225
|
-
// Integriamo utente e password nell'URL RTSP
|
|
226
230
|
const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
|
|
227
231
|
|
|
228
|
-
//
|
|
232
|
+
// Connessione all'RTSP storico e salvataggio locale dei primi 15 secondi dell'evento
|
|
229
233
|
const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
|
|
230
234
|
|
|
231
235
|
await new Promise((resolve) => {
|
|
@@ -238,11 +242,11 @@ module.exports = function(RED) {
|
|
|
238
242
|
if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
|
|
239
243
|
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
240
244
|
fileDaCancellare.push(fixedPath);
|
|
241
|
-
node.warn(`[DEBUG VIDEO]
|
|
245
|
+
node.warn(`[DEBUG VIDEO] Spezzone video generato con successo.`);
|
|
242
246
|
}
|
|
243
247
|
}
|
|
244
248
|
|
|
245
|
-
// --- SPEDIZIONE
|
|
249
|
+
// --- SPEDIZIONE MESSAGGIO FINALE ---
|
|
246
250
|
if (output.foto_base64 || output.video_base64) {
|
|
247
251
|
node.send({ payload: output });
|
|
248
252
|
setTimeout(() => {
|
|
@@ -258,7 +262,7 @@ module.exports = function(RED) {
|
|
|
258
262
|
updateNodeStatus();
|
|
259
263
|
}
|
|
260
264
|
|
|
261
|
-
// ---
|
|
265
|
+
// --- ASCOLTO SULL'ALERT STREAM DELL'NVR ---
|
|
262
266
|
function startAlertStream() {
|
|
263
267
|
if (isClosing) return;
|
|
264
268
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|