node-red-contrib-hik-media-buffer 1.1.98 → 1.1.100
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 +36 -34
- 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
|
+
// CLONE PERFETTO DEL FORMATO DATA DI CHROME (Es: 2026-06-23T17:40:00 02:00)
|
|
32
32
|
function toJsonDate(d) {
|
|
33
33
|
const pad = (num) => String(num).padStart(2, '0');
|
|
34
34
|
const year = d.getFullYear();
|
|
@@ -39,17 +39,18 @@ module.exports = function(RED) {
|
|
|
39
39
|
const seconds = pad(d.getSeconds());
|
|
40
40
|
|
|
41
41
|
const offsetMinutes = d.getTimezoneOffset();
|
|
42
|
-
|
|
42
|
+
// Chiariamo l'offset senza inserire il '+' che fa incazzare l'NVR
|
|
43
|
+
const offsetSign = offsetMinutes <= 0 ? '' : '-';
|
|
43
44
|
const absOffsetMinutes = Math.abs(offsetMinutes);
|
|
44
45
|
const offsetHours = pad(Math.floor(absOffsetMinutes / 60));
|
|
45
46
|
const offsetMins = pad(absOffsetMinutes % 60);
|
|
46
47
|
|
|
47
|
-
|
|
48
|
+
// NOTA: C'è uno spazio intenzionale prima di offsetSign per replicare il log dell'NVR
|
|
49
|
+
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds} ${offsetSign}${offsetHours}:${offsetMins}`;
|
|
48
50
|
}
|
|
49
51
|
|
|
50
52
|
const nvrAuthAxios = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
51
53
|
|
|
52
|
-
// --- RECUPERA IL NOME DELLA TELECAMERA ---
|
|
53
54
|
async function getCameraName(channelID) {
|
|
54
55
|
try {
|
|
55
56
|
const res = await nvrAuthAxios.request({
|
|
@@ -63,7 +64,6 @@ module.exports = function(RED) {
|
|
|
63
64
|
} catch (e) { return `Camera_${channelID}`; }
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
// --- CONTROLLO STATUS TELECAMERE CONNESSE ALL'NVR ---
|
|
67
67
|
async function checkCameras() {
|
|
68
68
|
if (isClosing || !nvrOnline) return;
|
|
69
69
|
try {
|
|
@@ -106,7 +106,7 @@ module.exports = function(RED) {
|
|
|
106
106
|
|
|
107
107
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
108
108
|
|
|
109
|
-
// ---
|
|
109
|
+
// --- SISTEMA DI SCARICAMENTO SBLOCCATO CON DATA DI CHROME ---
|
|
110
110
|
async function downloadMedia(evento, channelID) {
|
|
111
111
|
const chStr = channelID.toString();
|
|
112
112
|
const nowTime = Date.now();
|
|
@@ -118,14 +118,19 @@ module.exports = function(RED) {
|
|
|
118
118
|
const referenceTime = new Date();
|
|
119
119
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
120
120
|
|
|
121
|
-
//
|
|
122
|
-
const
|
|
123
|
-
|
|
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
|
+
|
|
127
|
+
const startTime = toJsonDate(dStart);
|
|
128
|
+
const endTime = toJsonDate(dEnd);
|
|
124
129
|
|
|
125
130
|
node.status({fill:"yellow", shape:"dot", text:`Ricerca Evento Cam ${channelID}...`});
|
|
126
131
|
|
|
127
|
-
// Aspettiamo
|
|
128
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
132
|
+
// Aspettiamo 4 secondi per essere certi che l'NVR abbia scritto il log a database
|
|
133
|
+
await new Promise(resolve => setTimeout(resolve, 4000));
|
|
129
134
|
|
|
130
135
|
let output = {
|
|
131
136
|
tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
|
|
@@ -136,13 +141,13 @@ module.exports = function(RED) {
|
|
|
136
141
|
let fileDaCancellare = [];
|
|
137
142
|
|
|
138
143
|
try {
|
|
139
|
-
//
|
|
144
|
+
// Generazione dinamica di un SearchID UUID valido per la sessione
|
|
140
145
|
const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
141
146
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
142
147
|
return v.toString(16).toUpperCase();
|
|
143
148
|
});
|
|
144
149
|
|
|
145
|
-
//
|
|
150
|
+
// Payload clonato identico a quello funzionante di Chrome
|
|
146
151
|
const searchPayload = {
|
|
147
152
|
"EventSearchDescription": {
|
|
148
153
|
"searchID": dynamicSearchId,
|
|
@@ -150,17 +155,16 @@ module.exports = function(RED) {
|
|
|
150
155
|
"maxResults": 30,
|
|
151
156
|
"timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
|
|
152
157
|
"type": "all",
|
|
153
|
-
"channels": [ parseInt(channelID) ],
|
|
158
|
+
"channels": [ parseInt(channelID) ],
|
|
154
159
|
"eventType": "behavior",
|
|
155
|
-
"behavior": {
|
|
156
|
-
"behaviorEventType": evento.toLowerCase() // Spara "linedetection" o "fielddetection"
|
|
157
|
-
}
|
|
160
|
+
"behavior": { "behaviorEventType": evento.toLowerCase() }
|
|
158
161
|
}
|
|
159
162
|
};
|
|
163
|
+
|
|
160
164
|
const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
|
|
161
165
|
fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
|
|
162
166
|
|
|
163
|
-
node.warn(`[DEBUG JSON]
|
|
167
|
+
node.warn(`[DEBUG JSON] Invio query con data stile Chrome...`);
|
|
164
168
|
const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
|
|
165
169
|
const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/json" -d "@${tempJsonPath}" "${targetUrl}"`;
|
|
166
170
|
|
|
@@ -171,7 +175,7 @@ module.exports = function(RED) {
|
|
|
171
175
|
try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
|
|
172
176
|
|
|
173
177
|
if (!jsonResponseRaw) {
|
|
174
|
-
node.error("[DEBUG JSON] Nessuna risposta
|
|
178
|
+
node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
|
|
175
179
|
updateNodeStatus();
|
|
176
180
|
return;
|
|
177
181
|
}
|
|
@@ -180,27 +184,27 @@ module.exports = function(RED) {
|
|
|
180
184
|
const matches = searchResult?.EventSearchResult?.matchList || [];
|
|
181
185
|
|
|
182
186
|
if (matches.length === 0) {
|
|
183
|
-
node.warn(`[DEBUG JSON]
|
|
187
|
+
node.warn(`[DEBUG JSON] Zero match trovati nell'intervallo orario con sintassi Chrome.`);
|
|
184
188
|
updateNodeStatus();
|
|
185
189
|
return;
|
|
186
190
|
}
|
|
187
191
|
|
|
188
|
-
//
|
|
192
|
+
// Prendiamo il record più fresco (il primo restituito)
|
|
189
193
|
const matchItem = matches[0];
|
|
190
194
|
const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
|
|
191
195
|
|
|
192
|
-
// --- 1.
|
|
196
|
+
// --- 1. DOWNLOAD FOTO ORIGINALE DAL PATH NATIVO GET ---
|
|
193
197
|
let pictureUrlPath = null;
|
|
194
198
|
if (playbackUri) {
|
|
195
199
|
const urlParamsMatch = playbackUri.match(/\?(.*)/);
|
|
196
200
|
if (urlParamsMatch) {
|
|
197
|
-
const trackI = (channelID * 100 + 3).toString(); //
|
|
201
|
+
const trackI = (channelID * 100 + 3).toString(); // Traccia 203 per le foto
|
|
198
202
|
pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
if (pictureUrlPath) {
|
|
203
|
-
node.warn(`[DEBUG FOTO] Scarico la foto
|
|
207
|
+
node.warn(`[DEBUG FOTO] Scarico la foto originale dell'evento via HTTP GET...`);
|
|
204
208
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
205
209
|
const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
|
|
206
210
|
|
|
@@ -213,15 +217,15 @@ module.exports = function(RED) {
|
|
|
213
217
|
}
|
|
214
218
|
}
|
|
215
219
|
|
|
216
|
-
// --- 2.
|
|
220
|
+
// --- 2. DOWNLOAD VIDEO CHIRURGICO VIA RTSP (Taglio forzato a 15 secondi) ---
|
|
217
221
|
if (playbackUri) {
|
|
218
|
-
node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP dell'evento
|
|
222
|
+
node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP temporizzato dell'evento...`);
|
|
219
223
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
220
224
|
|
|
221
|
-
//
|
|
225
|
+
// Integriamo utente e password nell'URL RTSP
|
|
222
226
|
const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
|
|
223
227
|
|
|
224
|
-
// -t 15
|
|
228
|
+
// -t 15 ferma Ffmpeg dopo 15 secondi esatti dall'inizio del record, evitando i file da 1GB
|
|
225
229
|
const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
|
|
226
230
|
|
|
227
231
|
await new Promise((resolve) => {
|
|
@@ -234,20 +238,18 @@ module.exports = function(RED) {
|
|
|
234
238
|
if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
|
|
235
239
|
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
236
240
|
fileDaCancellare.push(fixedPath);
|
|
237
|
-
node.warn(`[DEBUG VIDEO]
|
|
238
|
-
} else {
|
|
239
|
-
node.warn(`[DEBUG VIDEO] Flusso RTSP non catturato o spezzone non pronto.`);
|
|
241
|
+
node.warn(`[DEBUG VIDEO] Video catturato con successo (${fs.statSync(fixedPath).size} byte).`);
|
|
240
242
|
}
|
|
241
243
|
}
|
|
242
244
|
|
|
243
|
-
// ---
|
|
245
|
+
// --- SPEDIZIONE PAYLOAD ---
|
|
244
246
|
if (output.foto_base64 || output.video_base64) {
|
|
245
247
|
node.send({ payload: output });
|
|
246
248
|
setTimeout(() => {
|
|
247
249
|
for (let file of fileDaCancellare) {
|
|
248
250
|
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
249
251
|
}
|
|
250
|
-
}, 60000);
|
|
252
|
+
}, 60000);
|
|
251
253
|
}
|
|
252
254
|
|
|
253
255
|
} catch (e) {
|
|
@@ -256,7 +258,7 @@ module.exports = function(RED) {
|
|
|
256
258
|
updateNodeStatus();
|
|
257
259
|
}
|
|
258
260
|
|
|
259
|
-
// ---
|
|
261
|
+
// --- APERTURA ALERT STREAM (Axios Stream continuo) ---
|
|
260
262
|
function startAlertStream() {
|
|
261
263
|
if (isClosing) return;
|
|
262
264
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|