node-red-contrib-hik-media-buffer 1.1.106 → 1.1.107
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 +58 -48
- package/package.json +1 -1
package/hik-media-buffer.js
CHANGED
|
@@ -97,25 +97,17 @@ module.exports = function(RED) {
|
|
|
97
97
|
const referenceTime = new Date();
|
|
98
98
|
const timestamp = Math.floor(referenceTime.getTime() / 1000);
|
|
99
99
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const month = pad(timeGoBack.getUTCMonth() + 1);
|
|
109
|
-
const day = pad(timeGoBack.getUTCDate());
|
|
110
|
-
const hours = pad(timeGoBack.getUTCHours());
|
|
111
|
-
const minutes = pad(timeGoBack.getUTCMinutes());
|
|
112
|
-
const seconds = pad(timeGoBack.getUTCSeconds());
|
|
113
|
-
|
|
114
|
-
// Output pulito: 20260624T085731Z
|
|
115
|
-
const rtspTimestamp = `${year}${month}${day}T${hours}${minutes}${seconds}Z`;
|
|
100
|
+
function toXmlDate(d) {
|
|
101
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
102
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}Z`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Cerchiamo il record video da 2 minuti prima a 1 minuto dopo l'evento
|
|
106
|
+
const startTime = toXmlDate(new Date(referenceTime.getTime() - (2 * 60 * 1000)));
|
|
107
|
+
const endTime = toXmlDate(new Date(referenceTime.getTime() + (1 * 60 * 1000)));
|
|
116
108
|
|
|
117
|
-
node.status({fill:"yellow", shape:"dot", text:`
|
|
118
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
109
|
+
node.status({fill:"yellow", shape:"dot", text:`Ricerca file NVR...`});
|
|
110
|
+
await new Promise(resolve => setTimeout(resolve, 5000)); // Aspetta che l'NVR crei il file
|
|
119
111
|
|
|
120
112
|
let output = {
|
|
121
113
|
tipo_messaggio: "evento", nome_cliente: node.name, nome_telecamera: nomeCamera,
|
|
@@ -127,46 +119,64 @@ module.exports = function(RED) {
|
|
|
127
119
|
|
|
128
120
|
try {
|
|
129
121
|
const trackId = parseInt(channelID) * 100 + 1;
|
|
122
|
+
const searchXml = `<?xml version="1.0" encoding="utf-8"?><CMSearchDescription><searchID>00000000-0000-0000-0000-000000000000</searchID><trackList><trackID>${trackId}</trackID></trackList><timeSpanList><timeSpan><startTime>${startTime}</startTime><endTime>${endTime}</endTime></timeSpan></timeSpanList><maxResults>1</maxResults></CMSearchDescription>`.trim();
|
|
123
|
+
|
|
124
|
+
const tempXmlPath = path.join(baseStorage, `search_xml_${channelID}_${timestamp}.xml`);
|
|
125
|
+
fs.writeFileSync(tempXmlPath, searchXml, 'utf8');
|
|
126
|
+
|
|
127
|
+
const targetUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/search`;
|
|
128
|
+
const curlSearch = `curl -s -X POST --digest -u "${node.user}:${node.pass}" -H "Content-Type: application/xml" -d "@${tempXmlPath}" "${targetUrl}"`;
|
|
129
|
+
|
|
130
|
+
let xmlResponseRaw = await new Promise((resolve) => { exec(curlSearch, (err, stdout) => resolve(err ? null : stdout)); });
|
|
131
|
+
try { if (fs.existsSync(tempXmlPath)) fs.unlinkSync(tempXmlPath); } catch(e){}
|
|
132
|
+
|
|
133
|
+
const playbackUriMatch = xmlResponseRaw ? xmlResponseRaw.match(/<playbackURI>([^<]+)<\/playbackURI>/i) : null;
|
|
134
|
+
|
|
135
|
+
if (!playbackUriMatch) {
|
|
136
|
+
node.warn(`[DEBUG HTTP] Nessun file trovato nell'archivio per la cam ${channelID}.`);
|
|
137
|
+
updateNodeStatus();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const playbackUri = playbackUriMatch[1].trim();
|
|
142
|
+
node.warn(`[DEBUG HTTP] Trovato URI d'archivio: ${playbackUri}`);
|
|
143
|
+
|
|
144
|
+
const rawVideoPath = path.join(baseStorage, `raw_${channelID}_${timestamp}.mp4`);
|
|
130
145
|
const fixedPath = path.join(baseStorage, `hik_v_${channelID}_${timestamp}.mp4`);
|
|
131
146
|
const fullImgPath = path.join(baseStorage, `img_${timestamp}.jpg`);
|
|
132
147
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
node.warn(`[DEBUG
|
|
148
|
+
// --- 1. DOWNLOAD DIRETTO VIA HTTP (ISAPI DOWNLOAD) ---
|
|
149
|
+
// Scarichiamo il file video originale memorizzato sull'NVR usando l'API di download nativa
|
|
150
|
+
node.warn(`[DEBUG HTTP] Avvio download del file originale dall'hard disk...`);
|
|
151
|
+
const downloadUrl = `${node.protocol}://${node.host}:${node.port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(playbackUri)}`;
|
|
152
|
+
const curlDownloadCmd = `curl -s -X GET --digest -u "${node.user}:${node.pass}" "${downloadUrl}" -o "${rawVideoPath}"`;
|
|
136
153
|
|
|
137
|
-
|
|
138
|
-
// -stimeout imposta il timeout interno della connessione a 20 secondi (in microsecondi)
|
|
139
|
-
// -fflags +genpts forza FFmpeg a ricostruire i timestamp mancanti ignorando i drop iniziali dell'NVR
|
|
140
|
-
const ffmpegRtspCmd = `ffmpeg -y -rtsp_transport tcp -stimeout 20000000 -i "${archiveRtspUrl}" -t 15 -fflags +genpts -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
|
|
154
|
+
await new Promise((resolve) => { exec(curlDownloadCmd, () => resolve()); });
|
|
141
155
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if (err) node.error(`[DEBUG FFMPEG VIDEO ERRORE]: ${err.message}`);
|
|
145
|
-
resolve();
|
|
146
|
-
});
|
|
147
|
-
});
|
|
156
|
+
if (fs.existsSync(rawVideoPath) && fs.statSync(rawVideoPath).size > 5000) {
|
|
157
|
+
node.warn(`[DEBUG HTTP] File scaricato (${fs.statSync(rawVideoPath).size} byte). Lo ritaglio in locale...`);
|
|
148
158
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
159
|
+
// --- 2. TAGLIO LOCALE E CONVERSIONE AUDIO CON FFMPEG ---
|
|
160
|
+
// Visto che il file è sul tuo PC, FFmpeg lavora in locale (velocità istantanea, zero timeout di rete!)
|
|
161
|
+
const ffmpegCutCmd = `ffmpeg -y -i "${rawVideoPath}" -t 15 -c:v copy -c:a aac -movflags +faststart "${fixedPath}"`;
|
|
162
|
+
await new Promise((resolve) => { exec(ffmpegCutCmd, () => resolve()); });
|
|
153
163
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const ffmpegImgCmd = `ffmpeg -y -i "${fixedPath}" -vframes 1 -q:v 2 "${fullImgPath}"`;
|
|
164
|
+
if (fs.existsSync(fixedPath)) {
|
|
165
|
+
output.video_base64 = fs.readFileSync(fixedPath, { encoding: 'base64' });
|
|
166
|
+
fileDaCancellare.push(fixedPath);
|
|
158
167
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
168
|
+
// --- 3. ESTRAZIONE FOTO DAL VIDEO REGISTRATO ---
|
|
169
|
+
const ffmpegImgCmd = `ffmpeg -y -i "${fixedPath}" -vframes 1 -q:v 2 "${fullImgPath}"`;
|
|
170
|
+
await new Promise((resolve) => { exec(ffmpegImgCmd, () => resolve()); });
|
|
162
171
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
if (fs.existsSync(fullImgPath)) {
|
|
173
|
+
output.foto_base64 = fs.readFileSync(fullImgPath, { encoding: 'base64' });
|
|
174
|
+
fileDaCancellare.push(fullImgPath);
|
|
175
|
+
}
|
|
167
176
|
}
|
|
177
|
+
fileDaCancellare.push(rawVideoPath);
|
|
168
178
|
} else {
|
|
169
|
-
node.warn(`[DEBUG
|
|
179
|
+
node.warn(`[DEBUG HTTP] Il download del file da NVR è fallito o il file è vuoto.`);
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
// --- SPEDIZIONE PAYLOAD ---
|
|
@@ -180,7 +190,7 @@ module.exports = function(RED) {
|
|
|
180
190
|
}
|
|
181
191
|
|
|
182
192
|
} catch (e) {
|
|
183
|
-
node.error(`Errore
|
|
193
|
+
node.error(`Errore nel download HTTP d'archivio: ${e.message}`);
|
|
184
194
|
}
|
|
185
195
|
updateNodeStatus();
|
|
186
196
|
}
|