node-red-contrib-hik-media-buffer 1.1.50 → 1.1.52
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 -66
- 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,94 @@ 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", type: "video" },
|
|
164
|
+
{ id: trackFoto.toString(), isFoto: true, label: "FOTO", type: "metadata" }
|
|
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
|
+
// COPIATO DALL'ISPEZIONA REALE AL 100%: Mantenuto l'ordine esatto dei tag per evitare l'errore "two root tags"
|
|
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>${t.type}</contentType></contentTypeList><maxResults>30</maxResults><searchResultPostion>0</searchResultPostion><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}: 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}`);
|
|
225
235
|
}
|
|
236
|
+
} catch (searchError) {
|
|
237
|
+
let errorDetails = searchError.message;
|
|
238
|
+
if (searchError.response) {
|
|
239
|
+
errorDetails += ` | Status: ${searchError.response.status} | Dati Risposta: ${JSON.stringify(searchError.response.data)}`;
|
|
240
|
+
}
|
|
241
|
+
node.error(`[CATCH TRACCIA ${t.id}] Errore durante la POST di ricerca: ${errorDetails}`);
|
|
226
242
|
}
|
|
227
243
|
}
|
|
228
244
|
|
|
229
245
|
if (output.foto_base64 || output.video_base64) {
|
|
246
|
+
node.warn(`[STEP 6] Spedizione payload verso Node-RED effettuata con successo!`);
|
|
230
247
|
node.send({ payload: output });
|
|
231
248
|
|
|
232
249
|
setTimeout(() => {
|
|
@@ -234,15 +251,16 @@ module.exports = function(RED) {
|
|
|
234
251
|
try { if (fs.existsSync(file)) fs.unlinkSync(file); } catch (err) {}
|
|
235
252
|
}
|
|
236
253
|
}, 120000);
|
|
254
|
+
} else {
|
|
255
|
+
node.warn(`[STEP 6] Nessun media Base64 popolato, nessun pacchetto inviato a Node-RED.`);
|
|
237
256
|
}
|
|
238
257
|
|
|
239
258
|
} catch (e) {
|
|
240
|
-
node.error(`
|
|
259
|
+
node.error(`[FATAL ERROR] Blocco principale downloadMedia fallito: ${e.message}`);
|
|
241
260
|
}
|
|
242
|
-
|
|
261
|
+
node.status({fill:"green", shape:"ring", text:"In ascolto"});
|
|
243
262
|
}
|
|
244
263
|
|
|
245
|
-
// --- ALERT STREAM ---
|
|
246
264
|
function startAlertStream() {
|
|
247
265
|
if (isClosing) return;
|
|
248
266
|
const nvrAuth = new AxiosDigestAuth({ username: node.user, password: node.pass });
|