node-red-contrib-hik-media-buffer 1.1.99 → 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 +33 -39
- 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,22 +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 dStart = new Date(referenceTime
|
|
123
|
-
dStart.
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
dEnd.setSeconds(59, 999); // Forza :59 secondi
|
|
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);
|
|
127
126
|
|
|
128
127
|
const startTime = toJsonDate(dStart);
|
|
129
128
|
const endTime = toJsonDate(dEnd);
|
|
130
129
|
|
|
131
|
-
node.warn(`[DEBUG TEMPO SANIFICATO] Cerco da: ${startTime} a: ${endTime}`);
|
|
132
|
-
|
|
133
130
|
node.status({fill:"yellow", shape:"dot", text:`Ricerca Evento Cam ${channelID}...`});
|
|
134
131
|
|
|
135
|
-
// Aspettiamo
|
|
136
|
-
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));
|
|
137
134
|
|
|
138
135
|
let output = {
|
|
139
136
|
tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
|
|
@@ -144,13 +141,13 @@ module.exports = function(RED) {
|
|
|
144
141
|
let fileDaCancellare = [];
|
|
145
142
|
|
|
146
143
|
try {
|
|
147
|
-
//
|
|
144
|
+
// Generazione dinamica di un SearchID UUID valido per la sessione
|
|
148
145
|
const dynamicSearchId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
149
146
|
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
150
147
|
return v.toString(16).toUpperCase();
|
|
151
148
|
});
|
|
152
149
|
|
|
153
|
-
//
|
|
150
|
+
// Payload clonato identico a quello funzionante di Chrome
|
|
154
151
|
const searchPayload = {
|
|
155
152
|
"EventSearchDescription": {
|
|
156
153
|
"searchID": dynamicSearchId,
|
|
@@ -158,17 +155,16 @@ module.exports = function(RED) {
|
|
|
158
155
|
"maxResults": 30,
|
|
159
156
|
"timeSpanList": [{ "startTime": startTime, "endTime": endTime }],
|
|
160
157
|
"type": "all",
|
|
161
|
-
"channels": [ parseInt(channelID) ],
|
|
158
|
+
"channels": [ parseInt(channelID) ],
|
|
162
159
|
"eventType": "behavior",
|
|
163
|
-
"behavior": {
|
|
164
|
-
"behaviorEventType": evento.toLowerCase() // Spara "linedetection" o "fielddetection"
|
|
165
|
-
}
|
|
160
|
+
"behavior": { "behaviorEventType": evento.toLowerCase() }
|
|
166
161
|
}
|
|
167
162
|
};
|
|
163
|
+
|
|
168
164
|
const tempJsonPath = path.join(baseStorage, `search_${channelID}_${timestamp}.json`);
|
|
169
165
|
fs.writeFileSync(tempJsonPath, JSON.stringify(searchPayload), 'utf8');
|
|
170
166
|
|
|
171
|
-
node.warn(`[DEBUG JSON]
|
|
167
|
+
node.warn(`[DEBUG JSON] Invio query con data stile Chrome...`);
|
|
172
168
|
const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/eventRecordSearch?format=json`;
|
|
173
169
|
const curlCommand = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/json" -d "@${tempJsonPath}" "${targetUrl}"`;
|
|
174
170
|
|
|
@@ -179,7 +175,7 @@ module.exports = function(RED) {
|
|
|
179
175
|
try { if (fs.existsSync(tempJsonPath)) fs.unlinkSync(tempJsonPath); } catch(e){}
|
|
180
176
|
|
|
181
177
|
if (!jsonResponseRaw) {
|
|
182
|
-
node.error("[DEBUG JSON] Nessuna risposta
|
|
178
|
+
node.error("[DEBUG JSON] Nessuna risposta dall'NVR.");
|
|
183
179
|
updateNodeStatus();
|
|
184
180
|
return;
|
|
185
181
|
}
|
|
@@ -188,27 +184,27 @@ module.exports = function(RED) {
|
|
|
188
184
|
const matches = searchResult?.EventSearchResult?.matchList || [];
|
|
189
185
|
|
|
190
186
|
if (matches.length === 0) {
|
|
191
|
-
node.warn(`[DEBUG JSON]
|
|
187
|
+
node.warn(`[DEBUG JSON] Zero match trovati nell'intervallo orario con sintassi Chrome.`);
|
|
192
188
|
updateNodeStatus();
|
|
193
189
|
return;
|
|
194
190
|
}
|
|
195
191
|
|
|
196
|
-
//
|
|
192
|
+
// Prendiamo il record più fresco (il primo restituito)
|
|
197
193
|
const matchItem = matches[0];
|
|
198
194
|
const playbackUri = matchItem?.mediaSegmentDescriptor?.playbackURI;
|
|
199
195
|
|
|
200
|
-
// --- 1.
|
|
196
|
+
// --- 1. DOWNLOAD FOTO ORIGINALE DAL PATH NATIVO GET ---
|
|
201
197
|
let pictureUrlPath = null;
|
|
202
198
|
if (playbackUri) {
|
|
203
199
|
const urlParamsMatch = playbackUri.match(/\?(.*)/);
|
|
204
200
|
if (urlParamsMatch) {
|
|
205
|
-
const trackI = (channelID * 100 + 3).toString(); //
|
|
201
|
+
const trackI = (channelID * 100 + 3).toString(); // Traccia 203 per le foto
|
|
206
202
|
pictureUrlPath = `/picture/Streaming/tracks/${trackI}/?${urlParamsMatch[1]}`;
|
|
207
203
|
}
|
|
208
204
|
}
|
|
209
205
|
|
|
210
206
|
if (pictureUrlPath) {
|
|
211
|
-
node.warn(`[DEBUG FOTO] Scarico la foto
|
|
207
|
+
node.warn(`[DEBUG FOTO] Scarico la foto originale dell'evento via HTTP GET...`);
|
|
212
208
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
213
209
|
const curlImgCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${node.protocol}://${node.host}:${node.port}${pictureUrlPath}" -o "${fullImgPath}"`;
|
|
214
210
|
|
|
@@ -221,15 +217,15 @@ module.exports = function(RED) {
|
|
|
221
217
|
}
|
|
222
218
|
}
|
|
223
219
|
|
|
224
|
-
// --- 2.
|
|
220
|
+
// --- 2. DOWNLOAD VIDEO CHIRURGICO VIA RTSP (Taglio forzato a 15 secondi) ---
|
|
225
221
|
if (playbackUri) {
|
|
226
|
-
node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP dell'evento
|
|
222
|
+
node.warn(`[DEBUG VIDEO] Aggancio il flusso RTSP temporizzato dell'evento...`);
|
|
227
223
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
228
224
|
|
|
229
|
-
//
|
|
225
|
+
// Integriamo utente e password nell'URL RTSP
|
|
230
226
|
const authenticatedRtspUrl = playbackUri.replace("rtsp://", `rtsp://${node.user}:${node.pass}@`);
|
|
231
227
|
|
|
232
|
-
// -t 15
|
|
228
|
+
// -t 15 ferma Ffmpeg dopo 15 secondi esatti dall'inizio del record, evitando i file da 1GB
|
|
233
229
|
const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -i "${authenticatedRtspUrl}" -t 15 -c copy -movflags +faststart "${fixedPath}"`;
|
|
234
230
|
|
|
235
231
|
await new Promise((resolve) => {
|
|
@@ -242,20 +238,18 @@ module.exports = function(RED) {
|
|
|
242
238
|
if (fs.existsSync(fixedPath) && fs.statSync(fixedPath).size > 1000) {
|
|
243
239
|
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
244
240
|
fileDaCancellare.push(fixedPath);
|
|
245
|
-
node.warn(`[DEBUG VIDEO]
|
|
246
|
-
} else {
|
|
247
|
-
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).`);
|
|
248
242
|
}
|
|
249
243
|
}
|
|
250
244
|
|
|
251
|
-
// ---
|
|
245
|
+
// --- SPEDIZIONE PAYLOAD ---
|
|
252
246
|
if (output.foto_base64 || output.video_base64) {
|
|
253
247
|
node.send({ payload: output });
|
|
254
248
|
setTimeout(() => {
|
|
255
249
|
for (let file of fileDaCancellare) {
|
|
256
250
|
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
257
251
|
}
|
|
258
|
-
}, 60000);
|
|
252
|
+
}, 60000);
|
|
259
253
|
}
|
|
260
254
|
|
|
261
255
|
} catch (e) {
|
|
@@ -264,7 +258,7 @@ module.exports = function(RED) {
|
|
|
264
258
|
updateNodeStatus();
|
|
265
259
|
}
|
|
266
260
|
|
|
267
|
-
// ---
|
|
261
|
+
// --- APERTURA ALERT STREAM (Axios Stream continuo) ---
|
|
268
262
|
function startAlertStream() {
|
|
269
263
|
if (isClosing) return;
|
|
270
264
|
const url = `${node.protocol}://${node.host}:${node.port}/ISAPI/Event/notification/alertStream`;
|