node-red-contrib-hik-media-buffer 1.1.135 β 1.1.136
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 +40 -67
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -32,7 +32,7 @@ module.exports = function(RED) {
|
|
|
32
32
|
|
|
33
33
|
node.status({fill:"grey", shape:"ring", text:"Inizializzazione..."});
|
|
34
34
|
|
|
35
|
-
// --- FORMATTAZIONE DATA LOCALE
|
|
35
|
+
// --- FORMATTAZIONE DATA LOCALE ---
|
|
36
36
|
function toHikSearchDate(d) {
|
|
37
37
|
const pad = (num) => String(num).padStart(2, '0');
|
|
38
38
|
|
|
@@ -43,15 +43,13 @@ module.exports = function(RED) {
|
|
|
43
43
|
const minutes = pad(d.getMinutes());
|
|
44
44
|
const seconds = pad(d.getSeconds());
|
|
45
45
|
|
|
46
|
-
// Costruisce la stringa mantenendo l'ora locale del PC/Node-RED (es. 09:53:19Z)
|
|
47
46
|
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}Z`;
|
|
48
47
|
}
|
|
49
48
|
|
|
50
|
-
// --- 1. PRENDE IL NOME REALE DELLA TELECAMERA
|
|
49
|
+
// --- 1. PRENDE IL NOME REALE DELLA TELECAMERA ---
|
|
51
50
|
async function getCameraName(channelID) {
|
|
52
51
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
53
52
|
try {
|
|
54
|
-
// In accordo con la pag. 58 della documentazione, interroghiamo l'InputProxy del canale digitale
|
|
55
53
|
const res = await nvrAuth.request({
|
|
56
54
|
method: 'GET',
|
|
57
55
|
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/${channelID}`,
|
|
@@ -59,7 +57,6 @@ module.exports = function(RED) {
|
|
|
59
57
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
60
58
|
});
|
|
61
59
|
const data = res.data.toString();
|
|
62
|
-
// Estraiamo il tag <name> nativo del canale digitale
|
|
63
60
|
const match = data.match(/<name>([^<]+)<\/name>/i);
|
|
64
61
|
if (match && match[1]) return match[1].trim();
|
|
65
62
|
return `Canale ${channelID}`;
|
|
@@ -73,7 +70,6 @@ module.exports = function(RED) {
|
|
|
73
70
|
if (isClosing || !nvrOnline) return;
|
|
74
71
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
75
72
|
try {
|
|
76
|
-
// Chiamata all'endpoint di status dei canali proxy (Pag. 322)
|
|
77
73
|
const res = await nvrAuth.request({
|
|
78
74
|
method: 'GET',
|
|
79
75
|
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/InputProxy/channels/status`,
|
|
@@ -82,7 +78,6 @@ module.exports = function(RED) {
|
|
|
82
78
|
});
|
|
83
79
|
|
|
84
80
|
const xml = res.data.toString();
|
|
85
|
-
// Separiamo i singoli blocchi <InputProxyChannelStatus>
|
|
86
81
|
const channelBlocks = xml.match(/<InputProxyChannelStatus>[\s\S]*?<\/InputProxyChannelStatus>/g) || [];
|
|
87
82
|
|
|
88
83
|
for (let block of channelBlocks) {
|
|
@@ -91,21 +86,16 @@ module.exports = function(RED) {
|
|
|
91
86
|
|
|
92
87
|
if (idMatch) {
|
|
93
88
|
const ch = idMatch[1].trim();
|
|
94
|
-
// Estraiamo lo stato online nativo (true/false)
|
|
95
89
|
const isOnline = onlineMatch ? onlineMatch[1].trim().toLowerCase() === "true" : false;
|
|
96
90
|
|
|
97
91
|
if (isOnline && statoCamera[ch] === false) {
|
|
98
|
-
// π RECUPERO REALE DEL NOME: Usiamo getCameraName per avere il nome corretto OSD ("ufficio")
|
|
99
92
|
const nomeOnline = await getCameraName(ch);
|
|
100
|
-
|
|
101
93
|
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "online", nome_cliente: node.name, nome_telecamera: nomeOnline, ip_telecamera: node.host, channel: ch, msg: "Camera ripristinata" } });
|
|
102
94
|
statoCamera[ch] = true;
|
|
103
95
|
} else if (!isOnline && statoCamera[ch] === true) {
|
|
104
|
-
// Per l'offline usiamo un fallback sul canale, dato che la cam non risponderebbe a getCameraName
|
|
105
96
|
node.send({ payload: { tipo_messaggio: "status", stato_telecamera: "offline", nome_cliente: node.name, nome_telecamera: `Camera ${ch}`, ip_telecamera: node.host, channel: ch, msg: "Camera non raggiungibile" } });
|
|
106
97
|
statoCamera[ch] = false;
|
|
107
98
|
} else if (statoCamera[ch] === undefined) {
|
|
108
|
-
// Memorizzazione iniziale dello stato al boot
|
|
109
99
|
statoCamera[ch] = isOnline;
|
|
110
100
|
}
|
|
111
101
|
}
|
|
@@ -129,14 +119,14 @@ module.exports = function(RED) {
|
|
|
129
119
|
|
|
130
120
|
const heartbeatInterval = setInterval(checkCameras, 15000);
|
|
131
121
|
|
|
132
|
-
// --- 3. DOWNLOAD MEDIA
|
|
122
|
+
// --- 3. DOWNLOAD MEDIA FLUIDO (Scorciatoia Diretta da JSON) ---
|
|
133
123
|
async function downloadMedia(evento, channelID) {
|
|
134
124
|
const nowTime = Date.now();
|
|
135
125
|
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
136
126
|
if (nowTime - lastTriggerTime[channelID] < 5000) return;
|
|
137
127
|
lastTriggerTime[channelID] = nowTime;
|
|
138
128
|
|
|
139
|
-
node.status({fill:"yellow", shape:"dot", text:`Attesa
|
|
129
|
+
node.status({fill:"yellow", shape:"dot", text:`Attesa (6s)...`});
|
|
140
130
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
141
131
|
|
|
142
132
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
@@ -147,17 +137,12 @@ module.exports = function(RED) {
|
|
|
147
137
|
const nomeCamera = await getCameraName(channelID);
|
|
148
138
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
149
139
|
|
|
150
|
-
const inizioFinestra = new Date(referenceTime.getTime() - (
|
|
151
|
-
const fineFinestra = new Date(referenceTime.getTime() + (
|
|
140
|
+
const inizioFinestra = new Date(referenceTime.getTime() - (20 * 1000));
|
|
141
|
+
const fineFinestra = new Date(referenceTime.getTime() + (20 * 1000));
|
|
152
142
|
|
|
153
|
-
const startVideoSearch = toHikSearchDate(inizioFinestra);
|
|
154
|
-
const endVideoSearch = toHikSearchDate(fineFinestra);
|
|
155
|
-
|
|
156
143
|
const startFotoSearch = inizioFinestra.toISOString().split('.')[0] + "Z";
|
|
157
144
|
const endFotoSearch = fineFinestra.toISOString().split('.')[0] + "Z";
|
|
158
145
|
|
|
159
|
-
node.warn(`[DEBUG HIK] Avvio ricerca video. Canale: ${channelID}. Cerco da: ${startVideoSearch} a: ${endVideoSearch}`);
|
|
160
|
-
|
|
161
146
|
let output = {
|
|
162
147
|
tipo_messaggio: "evento",
|
|
163
148
|
nome_cliente: node.name,
|
|
@@ -174,7 +159,7 @@ module.exports = function(RED) {
|
|
|
174
159
|
let fileDaCancellare = [];
|
|
175
160
|
|
|
176
161
|
try {
|
|
177
|
-
// 1. πΈ SCARICAMENTO IMMAGINE
|
|
162
|
+
// 1. πΈ SCARICAMENTO IMMAGINE E RECUPERO METADATI ORARI
|
|
178
163
|
const payloadFoto = {
|
|
179
164
|
"EventSearchDescription": {
|
|
180
165
|
"searchID": "C5AFEE35-B1E0-4C01-83F8-47FD77892E4A",
|
|
@@ -196,55 +181,43 @@ module.exports = function(RED) {
|
|
|
196
181
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
197
182
|
});
|
|
198
183
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
<CMSearchDescription>
|
|
218
|
-
<searchID>CEC13F0F-1AEA-4474-A2DA-CFFF332C7C5B</searchID>
|
|
219
|
-
<trackList><trackID>${videoTrackID}</trackID></trackList>
|
|
220
|
-
<timeSpanList>
|
|
221
|
-
<timeSpan>
|
|
222
|
-
<startTime>${startVideoSearch}</startTime>
|
|
223
|
-
<endTime>${endVideoSearch}</endTime>
|
|
224
|
-
</timeSpan>
|
|
225
|
-
</timeSpanList>
|
|
226
|
-
<maxResults>1</maxResults>
|
|
227
|
-
<searchResultPostion>0</searchResultPostion>
|
|
228
|
-
<metadataList><metadataDescriptor>//recordType.meta.std-cgi.com</metadataDescriptor></metadataList>
|
|
229
|
-
</CMSearchDescription>`;
|
|
230
|
-
|
|
231
|
-
const resVideoSearch = await nvrAuth.request({
|
|
232
|
-
method: 'POST',
|
|
233
|
-
url: `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`,
|
|
234
|
-
data: payloadVideoSearch,
|
|
235
|
-
headers: { "Content-Type": "application/xml" },
|
|
236
|
-
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
237
|
-
});
|
|
184
|
+
let targetEvento = resFotoSearch.data?.EventSearchResult?.Targets?.[0];
|
|
185
|
+
let playbackURIGrezzo = null;
|
|
186
|
+
|
|
187
|
+
if (targetEvento) {
|
|
188
|
+
// Scarichiamo la foto normalmente
|
|
189
|
+
if (targetEvento.pictureUrl) {
|
|
190
|
+
const urlFotoGrezzo = targetEvento.pictureUrl;
|
|
191
|
+
const resDownFoto = await nvrAuth.request({
|
|
192
|
+
method: 'GET',
|
|
193
|
+
url: urlFotoGrezzo,
|
|
194
|
+
responseType: 'arraybuffer',
|
|
195
|
+
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
196
|
+
});
|
|
197
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}_ch${channelID}.jpg`);
|
|
198
|
+
fs.writeFileSync(fullImgPath, Buffer.from(resDownFoto.data));
|
|
199
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
200
|
+
fileDaCancellare.push(fullImgPath);
|
|
201
|
+
}
|
|
238
202
|
|
|
239
|
-
|
|
240
|
-
|
|
203
|
+
// 2. π₯ GENERAZIONE DINAMICA E DIRETTA PLAYBACK URI (Niente piΓΉ ricerca XML!)
|
|
204
|
+
if (targetEvento.startTime && targetEvento.endTime) {
|
|
205
|
+
// Funzione interna per ripulire i dati temporali (es. 2026-07-03T13:41:25+02:00 -> 20260703T134125Z)
|
|
206
|
+
const pulisciDataHik = (dataStr) => {
|
|
207
|
+
let pulita = dataStr.replace(/[-:]/g, '').split('+')[0];
|
|
208
|
+
return pulita;
|
|
209
|
+
};
|
|
241
210
|
|
|
242
|
-
|
|
211
|
+
const startClip = pulisciDataHik(targetEvento.startTime);
|
|
212
|
+
const endClip = pulisciDataHik(targetEvento.endTime);
|
|
243
213
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
214
|
+
playbackURIGrezzo = `rtsp://${node.host}:${node.port}/Streaming/tracks/${videoTrackID}/?starttime=${startClip}&endtime=${endClip}`;
|
|
215
|
+
node.warn(`[HIK SCORCIATOIA] URI Generato: ${playbackURIGrezzo}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
247
218
|
|
|
219
|
+
// 3. πΎ EFFETTUIAMO LA GET DI DOWNLOAD SE ABBIAMO GENERATO L'URI
|
|
220
|
+
if (playbackURIGrezzo) {
|
|
248
221
|
const payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
|
|
249
222
|
<downloadRequest>
|
|
250
223
|
<playbackURI>${playbackURIGrezzo}</playbackURI>
|