node-red-contrib-hik-media-buffer 1.2.5 → 1.2.7
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-download.html +3 -3
- package/hik-download.js +73 -28
- package/package.json +1 -1
package/hik-download.html
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
defaults: {
|
|
6
6
|
name: { value: "" },
|
|
7
7
|
host: { value: "" },
|
|
8
|
-
channels: { value: "
|
|
8
|
+
channels: { value: "" },
|
|
9
9
|
protocol: { value: "http" },
|
|
10
10
|
port: { value: "80" },
|
|
11
11
|
user: { value: "admin" },
|
|
@@ -50,12 +50,12 @@
|
|
|
50
50
|
|
|
51
51
|
<div class="form-row">
|
|
52
52
|
<label for="node-input-startTime"><i class="fa fa-calendar"></i> Data Inizio</label>
|
|
53
|
-
<input type="
|
|
53
|
+
<input type="datetime-local" id="node-input-startTime" step="1" style="width: 70%">
|
|
54
54
|
</div>
|
|
55
55
|
|
|
56
56
|
<div class="form-row">
|
|
57
57
|
<label for="node-input-endTime"><i class="fa fa-calendar"></i> Data Fine</label>
|
|
58
|
-
<input type="
|
|
58
|
+
<input type="datetime-local" id="node-input-endTime" step="1" style="width: 70%">
|
|
59
59
|
</div>
|
|
60
60
|
|
|
61
61
|
<div class="form-row">
|
package/hik-download.js
CHANGED
|
@@ -21,7 +21,6 @@ module.exports = function(RED) {
|
|
|
21
21
|
|
|
22
22
|
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
23
23
|
|
|
24
|
-
// Helper per decifrare i canali (es: "1,3" o "1-3")
|
|
25
24
|
function parseChannels(channelsStr) {
|
|
26
25
|
let channels = [];
|
|
27
26
|
if (!channelsStr) return [1];
|
|
@@ -63,19 +62,43 @@ module.exports = function(RED) {
|
|
|
63
62
|
return;
|
|
64
63
|
}
|
|
65
64
|
|
|
66
|
-
// 🌟 1.
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
// 🌟 1. FORMATTAZIONE DELLE DATE IN BASE ALLA PORTA (Bivio Logico)
|
|
66
|
+
let startTimeNVR, endTimeNVR, playbackURIGrezzo, payloadDownload;
|
|
67
|
+
|
|
68
|
+
const isPort85 = (port.toString() === "85");
|
|
69
|
+
|
|
70
|
+
if (isPort85) {
|
|
71
|
+
// Formato specifico DVR Porta 85: AAAA-MM-GGTHH:MM:SS (Senza Z, con trattini e due punti)
|
|
72
|
+
const formattaDataDVR = (dataStr) => {
|
|
73
|
+
let d = dataStr.replace(' ', 'T').split('.')[0].replace('Z', '');
|
|
74
|
+
if (d.includes('T') && d.split('T')[1].length === 5) {
|
|
75
|
+
d += ':00';
|
|
76
|
+
}
|
|
77
|
+
return d;
|
|
78
|
+
};
|
|
79
|
+
startTimeNVR = formattaDataDVR(startTimeRaw);
|
|
80
|
+
endTimeNVR = formattaDataDVR(endTimeRaw);
|
|
81
|
+
|
|
82
|
+
// RTSP con porta e XML senza namespace
|
|
83
|
+
const trackId = targetChannels[0] + "01"; // Default temporaneo per comporre l'URI prima del ciclo
|
|
84
|
+
playbackURIGrezzo = `rtsp://${host}:${port}/Streaming/tracks/TRACK_PLACEHOLDER/?starttime=${startTimeNVR}&endtime=${endTimeNVR}`;
|
|
85
|
+
} else {
|
|
86
|
+
// Formato classico NVR (Senza trattini e senza due punti)
|
|
87
|
+
const pulisciDataNVR = (dataStr) => {
|
|
88
|
+
let d = dataStr.replace(' ', 'T').replace(/[-:]/g, '').split('.')[0].replace('Z', '');
|
|
89
|
+
if (d.includes('T') && d.split('T')[1].length === 4) {
|
|
90
|
+
d += '00';
|
|
91
|
+
}
|
|
92
|
+
return d;
|
|
93
|
+
};
|
|
94
|
+
startTimeNVR = pulisciDataNVR(startTimeRaw);
|
|
95
|
+
endTimeNVR = pulisciDataNVR(endTimeRaw);
|
|
73
96
|
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
// RTSP senza porta
|
|
98
|
+
playbackURIGrezzo = `rtsp://${host}/Streaming/tracks/TRACK_PLACEHOLDER/?starttime=${startTimeNVR}&endtime=${endTimeNVR}`;
|
|
99
|
+
}
|
|
76
100
|
|
|
77
|
-
//
|
|
78
|
-
// Estraiamo la data del giorno dallo startTime (i primi 10 caratteri, es: "2026-07-14")
|
|
101
|
+
// Creazione cartella di salvataggio C:\download\YYYY-MM-DD
|
|
79
102
|
const dataGiorno = startTimeRaw.substring(0, 10).replace(/\//g, '-');
|
|
80
103
|
const baseDir = "C:\\download";
|
|
81
104
|
const targetDir = path.join(baseDir, dataGiorno);
|
|
@@ -92,7 +115,7 @@ module.exports = function(RED) {
|
|
|
92
115
|
|
|
93
116
|
const digest = new DigestAuthClass({ username: user, password: pass });
|
|
94
117
|
|
|
95
|
-
// 🌟
|
|
118
|
+
// 🌟 2. CICLO DI DOWNLOAD
|
|
96
119
|
for (let index = 0; index < targetChannels.length; index++) {
|
|
97
120
|
const ch = targetChannels[index];
|
|
98
121
|
const trackId = ch + "01";
|
|
@@ -103,34 +126,56 @@ module.exports = function(RED) {
|
|
|
103
126
|
text: `Download Ch ${ch} (${index + 1}/${targetChannels.length})...`
|
|
104
127
|
});
|
|
105
128
|
|
|
106
|
-
|
|
107
|
-
const
|
|
129
|
+
// Sostituiamo il placeholder con la traccia corrente del ciclo
|
|
130
|
+
const uriCorrente = playbackURIGrezzo.replace('TRACK_PLACEHOLDER', trackId);
|
|
131
|
+
const playbackURIXml = uriCorrente.replace(/&/g, '&');
|
|
132
|
+
|
|
133
|
+
// Costruiamo l'XML a seconda del tipo di macchina
|
|
134
|
+
if (isPort85) {
|
|
135
|
+
payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
|
|
136
|
+
<downloadRequest>
|
|
137
|
+
<playbackURI>${playbackURIXml}</playbackURI>
|
|
138
|
+
</downloadRequest>`;
|
|
139
|
+
} else {
|
|
140
|
+
payloadDownload = `<?xml version="1.0" encoding="UTF-8"?>
|
|
141
|
+
<downloadRequest version="1.0" xmlns="http://www.isapi.org/ver20/XMLSchema">
|
|
142
|
+
<playbackURI>${playbackURIXml}</playbackURI>
|
|
143
|
+
</downloadRequest>`;
|
|
144
|
+
}
|
|
108
145
|
|
|
109
146
|
try {
|
|
110
147
|
const response = await digest.request({
|
|
111
148
|
method: 'GET',
|
|
112
|
-
url:
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
149
|
+
url: `${protocol}://${host}:${port}/ISAPI/ContentMgmt/download`,
|
|
150
|
+
data: payloadDownload,
|
|
151
|
+
headers: { "Content-Type": "application/xml" },
|
|
152
|
+
responseType: 'stream',
|
|
153
|
+
insecureHTTPParser: true,
|
|
154
|
+
httpsAgent: protocol === "https" ? httpsAgent : undefined,
|
|
155
|
+
timeout: 120000
|
|
116
156
|
});
|
|
117
157
|
|
|
118
158
|
if (response.status === 200 || response.status === 206) {
|
|
119
|
-
const videoBuffer = Buffer.from(response.data);
|
|
120
|
-
|
|
121
|
-
// Prepariamo il nome del file ripulito (es: NVR_Cam1_100000.mp4)
|
|
122
159
|
const nomeCondominio = (msg.nvr_name || node.name || 'NVR').replace(/[^a-zA-Z0-9]/g, '_');
|
|
123
160
|
const oraInizio = startTimeNVR.split('T')[1] || startTimeNVR;
|
|
124
|
-
const finalFileName = `${nomeCondominio}_Cam${ch}_${oraInizio}.mp4`;
|
|
161
|
+
const finalFileName = `${nomeCondominio}_Cam${ch}_${oraInizio.replace(/:/g, '')}.mp4`;
|
|
125
162
|
const finalFilePath = path.join(targetDir, finalFileName);
|
|
126
163
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
const writer = fs.createWriteStream(finalFilePath);
|
|
165
|
+
response.data.pipe(writer);
|
|
166
|
+
|
|
167
|
+
await new Promise((resolve, reject) => {
|
|
168
|
+
writer.on('finish', resolve);
|
|
169
|
+
writer.on('error', (err) => {
|
|
170
|
+
writer.close();
|
|
171
|
+
reject(err);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
node.log(`File salvato in: ${finalFilePath}`);
|
|
130
176
|
|
|
131
|
-
// Inviamo comunque il messaggio avanti nel flusso nel caso servisse ad altri nodi
|
|
132
177
|
let outMsg = RED.util.cloneMessage(msg);
|
|
133
|
-
outMsg.payload = finalFilePath;
|
|
178
|
+
outMsg.payload = finalFilePath;
|
|
134
179
|
outMsg.channel = ch;
|
|
135
180
|
outMsg.filename = finalFileName;
|
|
136
181
|
node.send(outMsg);
|