node-red-contrib-hik-media-buffer 1.1.50 → 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 -65
- 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,23 +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
70
|
const idMatch = bloccoContenuto.match(/<id>(\d+)<\/id>/i);
|
|
82
71
|
const statusMatch = bloccoContenuto.match(/<status>([^<]+)</i);
|
|
83
72
|
|
|
84
73
|
if (idMatch && statusMatch) {
|
|
85
74
|
const ch = idMatch[1];
|
|
86
75
|
const statusStr = statusMatch[1].toLowerCase();
|
|
87
|
-
|
|
88
76
|
const isOnline = statusStr.includes("online") || statusStr.includes("recording");
|
|
89
77
|
|
|
90
78
|
if (isOnline && statoCanale[ch] === false) {
|
|
@@ -118,25 +106,32 @@ module.exports = function(RED) {
|
|
|
118
106
|
|
|
119
107
|
const heartbeatInterval = setInterval(checkCameras, 30000);
|
|
120
108
|
|
|
121
|
-
// --- DOWNLOAD
|
|
109
|
+
// --- DOWNLOAD CON DEBUG COPIOSO ---
|
|
122
110
|
async function downloadMedia(evento, channelID) {
|
|
111
|
+
node.warn(`[STEP 1] Inizio downloadMedia. Evento: ${evento}, Canale originario: ${channelID}`);
|
|
112
|
+
|
|
123
113
|
const nowTime = Date.now();
|
|
124
114
|
if (!lastTriggerTime[channelID]) lastTriggerTime[channelID] = 0;
|
|
125
|
-
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
|
+
}
|
|
126
119
|
lastTriggerTime[channelID] = nowTime;
|
|
127
120
|
|
|
128
121
|
const camAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|
|
129
|
-
|
|
130
122
|
const referenceTime = new Date();
|
|
131
123
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
132
124
|
|
|
133
125
|
const startTime = toHikDate(new Date(referenceTime.getTime() - (20 * 1000)));
|
|
134
126
|
const endTime = toHikDate(new Date(referenceTime.getTime() + (20 * 1000)));
|
|
127
|
+
node.warn(`[STEP 2] Finestra temporale impostata: ${startTime} -> ${endTime}`);
|
|
135
128
|
|
|
136
129
|
node.status({fill:"yellow", shape:"dot", text:`Download Cam ${channelID}...`});
|
|
137
130
|
const nomeCamera = await getCameraName(channelID);
|
|
138
131
|
|
|
132
|
+
node.warn(`[STEP 3] Attesa di 6 secondi per la scrittura su disco dell'NVR...`);
|
|
139
133
|
await new Promise(resolve => setTimeout(resolve, 6000));
|
|
134
|
+
|
|
140
135
|
const baseUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt`;
|
|
141
136
|
|
|
142
137
|
let eventoNVR = "FieldDetection";
|
|
@@ -161,72 +156,95 @@ module.exports = function(RED) {
|
|
|
161
156
|
};
|
|
162
157
|
|
|
163
158
|
const parsedChannel = parseInt(channelID, 10);
|
|
164
|
-
const trackVideo = (parsedChannel * 100) + 1;
|
|
165
|
-
const trackFoto = (parsedChannel * 100) + 3;
|
|
159
|
+
const trackVideo = (parsedChannel * 100) + 1;
|
|
160
|
+
const trackFoto = (parsedChannel * 100) + 3;
|
|
166
161
|
|
|
167
162
|
const tracks = [
|
|
168
|
-
{ id: trackVideo.toString(), isFoto: false },
|
|
169
|
-
{ id: trackFoto.toString(), isFoto: true }
|
|
163
|
+
{ id: trackVideo.toString(), isFoto: false, label: "VIDEO" },
|
|
164
|
+
{ id: trackFoto.toString(), isFoto: true, label: "FOTO" }
|
|
170
165
|
];
|
|
171
166
|
let fileDaCancellare = [];
|
|
172
167
|
|
|
173
168
|
try {
|
|
174
169
|
for (let t of tracks) {
|
|
175
|
-
|
|
170
|
+
node.warn(`[STEP 4] Elaborazione traccia ${t.label} (ID: ${t.id})`);
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
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>`;
|
|
180
|
-
} else {
|
|
181
|
-
// XML ADATTATO PER I VIDEO: Cambiato il contentType in video per non mandare l'NVR in errore 400
|
|
182
|
-
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>video</contentType></contentTypeList><maxResults>30</maxResults><metadataList><metadataDescriptor>recordType.meta.hikvision.com/${eventoNVR}</metadataDescriptor></metadataList></CMSearchDescription>`;
|
|
183
|
-
}
|
|
172
|
+
// Ripristiniamo l'XML puro clonato dal tuo ispeziona immagini per traccia 203
|
|
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>`;
|
|
184
174
|
|
|
185
|
-
|
|
186
|
-
method: 'POST', url: `${baseUrl}/search`, data: searchXml, headers: { "Content-Type": "application/xml" }
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
let xml = resSearch.data.replace(/<(\/?)\w+:/g, "<$1");
|
|
190
|
-
const uriMatch = xml.match(/<playbackURI>([^<]+)</);
|
|
175
|
+
node.warn(`[STEP 4.1] Invio POST /search per traccia ${t.id}. XML inviato:\n${searchXml}`);
|
|
191
176
|
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
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" }
|
|
199
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>([^<]+)</);
|
|
200
189
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
fs.writeFileSync(fullImgPath, buffer);
|
|
205
|
-
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
206
|
-
fileDaCancellare.push(fullImgPath);
|
|
207
|
-
} else {
|
|
208
|
-
if (buffer.slice(0, 4).toString() === 'IMKH') buffer = buffer.slice(40);
|
|
209
|
-
const rawPath = path.join(baseStorage, `raw_${timestamp}.mp4`);
|
|
210
|
-
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
211
|
-
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}`);
|
|
212
193
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
output.video_base64 = fs.readFileSync(rawPath, { encoding: 'base64' });
|
|
220
|
-
}
|
|
221
|
-
fileDaCancellare.push(rawPath);
|
|
222
|
-
resolve();
|
|
223
|
-
});
|
|
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'
|
|
224
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)}`;
|
|
225
241
|
}
|
|
242
|
+
node.error(`[CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
|
|
226
243
|
}
|
|
227
244
|
}
|
|
228
245
|
|
|
229
246
|
if (output.foto_base64 || output.video_base64) {
|
|
247
|
+
node.warn(`[STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
|
|
230
248
|
node.send({ payload: output });
|
|
231
249
|
|
|
232
250
|
setTimeout(() => {
|
|
@@ -234,15 +252,16 @@ module.exports = function(RED) {
|
|
|
234
252
|
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
235
253
|
}
|
|
236
254
|
}, 120000);
|
|
255
|
+
} else {
|
|
256
|
+
node.warn(`[STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
|
|
237
257
|
}
|
|
238
258
|
|
|
239
259
|
} catch (e) {
|
|
240
|
-
node.error(`
|
|
260
|
+
node.error(`[FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
|
|
241
261
|
}
|
|
242
262
|
updateNodeStatus();
|
|
243
263
|
}
|
|
244
264
|
|
|
245
|
-
// --- ALERT STREAM ---
|
|
246
265
|
function startAlertStream() {
|
|
247
266
|
if (isClosing) return;
|
|
248
267
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|