node-red-contrib-hik-media-buffer 1.1.49 → 1.1.51
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 +84 -59
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -12,7 +12,7 @@ module.exports = function(RED) {
|
|
|
12
12
|
const node = this;
|
|
13
13
|
|
|
14
14
|
node.name = config.name || "TEST";
|
|
15
|
-
node.host = config.host;
|
|
15
|
+
node.host = config.host;
|
|
16
16
|
node.port = config.port || "80";
|
|
17
17
|
node.protocol = config.protocol || "http";
|
|
18
18
|
node.user = config.user;
|
|
@@ -25,11 +25,7 @@ module.exports = function(RED) {
|
|
|
25
25
|
let statoCanale = {};
|
|
26
26
|
|
|
27
27
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
28
|
-
|
|
29
|
-
// La tua EventList originale per l'alertStream
|
|
30
28
|
const EventList = ["FieldDetection", "LineDetection"];
|
|
31
|
-
|
|
32
|
-
// CARTELLA TEMPORANEA
|
|
33
29
|
const baseStorage = path.join(os.tmpdir(), "hik_temp_media");
|
|
34
30
|
if (!fs.existsSync(baseStorage)) fs.mkdirSync(baseStorage, { recursive: true });
|
|
35
31
|
|
|
@@ -37,7 +33,6 @@ module.exports = function(RED) {
|
|
|
37
33
|
|
|
38
34
|
function toHikDate(d) { return d.toISOString().split('.')[0] + "Z"; }
|
|
39
35
|
|
|
40
|
-
// --- PRENDE IL NOME DEL CANALE DIRETTAMENTE DALL'NVR ---
|
|
41
36
|
async function getCameraName(channelID) {
|
|
42
37
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
43
38
|
try {
|
|
@@ -47,7 +42,6 @@ module.exports = function(RED) {
|
|
|
47
42
|
timeout: 5000,
|
|
48
43
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
49
44
|
});
|
|
50
|
-
|
|
51
45
|
const data = res.data.toString();
|
|
52
46
|
const match = data.match(/<name>([^<]+)<\/name>/i);
|
|
53
47
|
return match && match[1] ? match[1].trim() : `Canale_${channelID}`;
|
|
@@ -56,11 +50,9 @@ module.exports = function(RED) {
|
|
|
56
50
|
}
|
|
57
51
|
}
|
|
58
52
|
|
|
59
|
-
// --- CONTROLLO STATUS DINAMICO DEI CANALI DIGITALI DALL'NVR ---
|
|
60
53
|
async function checkCameras() {
|
|
61
54
|
if (isClosing) return;
|
|
62
55
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
63
|
-
|
|
64
56
|
try {
|
|
65
57
|
const res = await nvrAuth.request({
|
|
66
58
|
method: 'GET',
|
|
@@ -68,24 +60,19 @@ module.exports = function(RED) {
|
|
|
68
60
|
timeout: 5000,
|
|
69
61
|
httpsAgent: node.protocol === "https" ? httpsAgent : undefined
|
|
70
62
|
});
|
|
71
|
-
|
|
72
63
|
nvrOnline = true;
|
|
73
64
|
const xmlData = res.data.toString();
|
|
74
|
-
|
|
75
65
|
const bloccoRegex = /<InputProxyChannelStatus[\s\\S]*?>([\s\S]*?)<\/InputProxyChannelStatus>/gi;
|
|
76
66
|
let matchBlocco;
|
|
77
67
|
|
|
78
68
|
while ((matchBlocco = bloccoRegex.exec(xmlData)) !== null) {
|
|
79
69
|
const bloccoContenuto = matchBlocco[1];
|
|
80
|
-
|
|
81
|
-
// FISSO: Corretto il nome della variabile bloccoContenuto (prima c'era un typo che rompeva il nodo)
|
|
82
70
|
const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
|
|
83
71
|
const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i);
|
|
84
72
|
|
|
85
73
|
if (idMatch && statusMatch) {
|
|
86
74
|
const ch = idMatch[1];
|
|
87
75
|
const statusStr = statusMatch[1].toLowerCase();
|
|
88
|
-
|
|
89
76
|
const isOnline = statusStr.includes("online") || statusStr.includes("recording");
|
|
90
77
|
|
|
91
78
|
if (isOnline && statoCanale[ch] === false) {
|
|
@@ -119,25 +106,32 @@ module.exports = function(RED) {
|
|
|
119
106
|
|
|
120
107
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
121
108
|
|
|
122
|
-
// --- DOWNLOAD
|
|
109
|
+
// --- DOWNLOAD CON DEBUG COPIOSO ---
|
|
123
110
|
async function downloadMedia(evento, channelID) {
|
|
111
|
+
node.warn(`[STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
|
|
112
|
+
|
|
124
113
|
const nowTime = Date.now();
|
|
125
114
|
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
126
|
-
if (nowTime - lastTriggerTime[channelID] < 5000)
|
|
115
|
+
if (nowTime - lastTriggerTime[channelID] < 5000) {
|
|
116
|
+
node.warn(`[STEP 1] Richiesta bloccata dall'antirimbalzo (meno di 5s dall'ultimo trigger)`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
127
119
|
lastTriggerTime[channelID] = nowTime;
|
|
128
120
|
|
|
129
121
|
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
130
|
-
|
|
131
122
|
const referenceTime = new Date();
|
|
132
123
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
133
124
|
|
|
134
125
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
|
|
135
126
|
const endTime = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
|
|
127
|
+
node.warn(`[STEP 2] Finestra temporale impostata: ${startTime} -> ${endTime}`);
|
|
136
128
|
|
|
137
129
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
138
130
|
const nomeCamera = await getCameraName(channelID);
|
|
139
131
|
|
|
132
|
+
node.warn(`[STEP 3] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
|
|
140
133
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
134
|
+
|
|
141
135
|
const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
|
|
142
136
|
|
|
143
137
|
let eventoNVR = "FieldDetection";
|
|
@@ -162,65 +156,95 @@ module.exports = function(RED) {
|
|
|
162
156
|
};
|
|
163
157
|
|
|
164
158
|
const parsedChannel = parseInt(channelID, 10);
|
|
165
|
-
const trackVideo = (parsedChannel * 100) + 1;
|
|
166
|
-
const trackFoto = (parsedChannel * 100) + 3;
|
|
159
|
+
const trackVideo = (parsedChannel * 100) + 1;
|
|
160
|
+
const trackFoto = (parsedChannel * 100) + 3;
|
|
167
161
|
|
|
168
162
|
const tracks = [
|
|
169
|
-
{ id: trackVideo.toString(), isFoto: false },
|
|
170
|
-
{ id: trackFoto.toString(), isFoto: true }
|
|
163
|
+
{ id: trackVideo.toString(), isFoto: false, label: "VIDEO" },
|
|
164
|
+
{ id: trackFoto.toString(), isFoto: true, label: "FOTO" }
|
|
171
165
|
];
|
|
172
166
|
let fileDaCancellare = [];
|
|
173
167
|
|
|
174
168
|
try {
|
|
175
169
|
for (let t of tracks) {
|
|
176
|
-
|
|
170
|
+
node.warn(`[STEP 4] Elaborazione traccia ${t.label} (ID: ${t.id})`);
|
|
171
|
+
|
|
172
|
+
// Ripristiniamo l'XML puro clonato dal tuo ispeziona immagini per traccia 203
|
|
177
173
|
const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>LAST_EVENT</searchID><trackIDList><trackID>${t.id}</trackID></trackIDList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><contentTypeList><contentType>metadata</contentType></contentTypeList><maxResults>30</maxResults><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
178
174
|
|
|
179
|
-
|
|
180
|
-
method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
184
|
-
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
175
|
+
node.warn(`[STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
|
|
185
176
|
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
responseType: 'arraybuffer'
|
|
177
|
+
try {
|
|
178
|
+
const resSearch = await camAuth.request({
|
|
179
|
+
method: 'POST',
|
|
180
|
+
url: `${baseUrl}/search`,
|
|
181
|
+
data: searchXml,
|
|
182
|
+
headers: { "Content-Type": "application/xml" }
|
|
193
183
|
});
|
|
184
|
+
|
|
185
|
+
node.warn(`[STEP 4.2] Risposta ricevuta da /search per traccia ${t.id}: Riposta Ricevuta (Status ${resSearch.status})`);
|
|
186
|
+
|
|
187
|
+
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
188
|
+
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
194
189
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
fs.writeFileSync(fullImgPath, buffer);
|
|
199
|
-
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
200
|
-
fileDaCancellare.push(fullImgPath);
|
|
201
|
-
} else {
|
|
202
|
-
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
203
|
-
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
204
|
-
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
205
|
-
fs.writeFileSync(rawPath, buffer);
|
|
190
|
+
if (uriMatch) {
|
|
191
|
+
const rawUri = uriMatch[1].replace(/&/g, '&');
|
|
192
|
+
node.warn(`[STEP 5] Trovato playbackURI per traccia ${t.id}: ${rawUri}`);
|
|
206
193
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
214
|
-
}
|
|
215
|
-
fileDaCancellare.push(rawPath);
|
|
216
|
-
resolve();
|
|
217
|
-
});
|
|
194
|
+
node.warn(`[STEP 5.1] Invio richiesta GET /download per traccia ${t.id}`);
|
|
195
|
+
const resDown = await camAuth.request({
|
|
196
|
+
method: 'GET',
|
|
197
|
+
url: `${baseUrl}/download`,
|
|
198
|
+
data: `<?xml version="1.0" encoding="UTF-8"?><downloadRequest><playbackURI>${rawUri.replace(/&/g, '&')}</playbackURI></downloadRequest>`,
|
|
199
|
+
responseType: 'arraybuffer'
|
|
218
200
|
});
|
|
201
|
+
|
|
202
|
+
node.warn(`[STEP 5.2] Scaricati ${resDown.data.byteLength} byte per traccia ${t.id}`);
|
|
203
|
+
let buffer = Buffer.from(resDown.data);
|
|
204
|
+
|
|
205
|
+
if (t.isFoto) {
|
|
206
|
+
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
207
|
+
fs.writeFileSync(fullImgPath, buffer);
|
|
208
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
209
|
+
fileDaCancellare.push(fullImgPath);
|
|
210
|
+
node.warn(`[STEP 5.3] Immagine salvata e convertita in Base64.`);
|
|
211
|
+
} else {
|
|
212
|
+
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
213
|
+
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
214
|
+
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
215
|
+
fs.writeFileSync(rawPath, buffer);
|
|
216
|
+
|
|
217
|
+
node.warn(`[STEP 5.3] Video grezzo salvato. Avvio conversione FFMPEG...`);
|
|
218
|
+
await new Promise((resolve) => {
|
|
219
|
+
exec(`ffmpeg -y -i "${rawPath}" -c copy -movflags +faststart "${fixedPath}"`, (err) => {
|
|
220
|
+
if (!err && fs.existsSync(fixedPath)) {
|
|
221
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
222
|
+
fileDaCancellare.push(fixedPath);
|
|
223
|
+
node.warn(`[STEP 5.4] Conversione FFMPEG completata con successo.`);
|
|
224
|
+
} else {
|
|
225
|
+
node.warn(`[STEP 5.4] Errore FFMPEG (uso il video grezzo di backup): ${err ? err.message : 'File mancante'}`);
|
|
226
|
+
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
227
|
+
}
|
|
228
|
+
fileDaCancellare.push(rawPath);
|
|
229
|
+
resolve();
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
node.warn(`[STEP 4.3] Nessun playbackURI trovato nel documento di risposta XML per traccia ${t.id}. XML di risposta:\n${resSearch.data}`);
|
|
235
|
+
}
|
|
236
|
+
} catch (searchError) {
|
|
237
|
+
// CATTURA DETTAGLIATA DELL'ERRORE 400
|
|
238
|
+
let errorDetails = searchError.message;
|
|
239
|
+
if (searchError.response) {
|
|
240
|
+
errorDetails += ` | Status: ${searchError.response.status} | Dati Risposta: ${JSON.stringify(searchError.response.data)}`;
|
|
219
241
|
}
|
|
242
|
+
node.error(`[CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
|
|
220
243
|
}
|
|
221
244
|
}
|
|
222
245
|
|
|
223
246
|
if (output.foto_base64 || output.video_base64) {
|
|
247
|
+
node.warn(`[STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
|
|
224
248
|
node.send({ payload: output });
|
|
225
249
|
|
|
226
250
|
setTimeout(() => {
|
|
@@ -228,15 +252,16 @@ module.exports = function(RED) {
|
|
|
228
252
|
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
229
253
|
}
|
|
230
254
|
}, 120000);
|
|
255
|
+
} else {
|
|
256
|
+
node.warn(`[STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
|
|
231
257
|
}
|
|
232
258
|
|
|
233
259
|
} catch (e) {
|
|
234
|
-
node.error(`
|
|
260
|
+
node.error(`[FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
|
|
235
261
|
}
|
|
236
262
|
updateNodeStatus();
|
|
237
263
|
}
|
|
238
264
|
|
|
239
|
-
// --- ALERT STREAM ---
|
|
240
265
|
function startAlertStream() {
|
|
241
266
|
if (isClosing) return;
|
|
242
267
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|