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 CHANGED
@@ -5,7 +5,7 @@
5
5
  defaults: {
6
6
  name: { value: "" },
7
7
  host: { value: "" },
8
- channels: { value: "1" },
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="text" id="node-input-startTime" placeholder="Formato: AAAA-MM-GG ORE:MIN:SEC (Es: 2026-07-14 10:00:00)">
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="text" id="node-input-endTime" placeholder="Formato: AAAA-MM-GG ORE:MIN:SEC (Es: 2026-07-14 10:05:00)">
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. LOGICA DI PULIZIA DELLE DATE PER HIKVISION (es. "2026-07-14 10:00:00" -> "20260714T100000")
67
- const pulisciDataNVR = (dataStr) => {
68
- return dataStr
69
- .replace(/[-:\s]/g, '') // Rimuove trattini, due punti e spazi
70
- .split('.')[0] // Rimuove eventuali millisecondi
71
- .replace('Z', ''); // Rimuove la Z se presente
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
- const startTimeNVR = pulisciDataNVR(startTimeRaw);
75
- const endTimeNVR = pulisciDataNVR(endTimeRaw);
97
+ // RTSP senza porta
98
+ playbackURIGrezzo = `rtsp://${host}/Streaming/tracks/TRACK_PLACEHOLDER/?starttime=${startTimeNVR}&endtime=${endTimeNVR}`;
99
+ }
76
100
 
77
- // 🌟 2. CREAZIONE CARTELLA DI DESTINAZIONE (C:\download\YYYY-MM-DD)
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
- // 🌟 3. CICLO DI DOWNLOAD E SALVATAGGIO SU DISCO
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
- const playbackURI = `rtsp://${host}/Streaming/tracks/${trackId}/?starttime=${startTimeNVR}&endtime=${endTimeNVR}`;
107
- const downloadUrl = `${protocol}://${host}:${port}/ISAPI/ContentMgmt/download?playbackURI=${encodeURIComponent(playbackURI)}`;
129
+ // Sostituiamo il placeholder con la traccia corrente del ciclo
130
+ const uriCorrente = playbackURIGrezzo.replace('TRACK_PLACEHOLDER', trackId);
131
+ const playbackURIXml = uriCorrente.replace(/&/g, '&amp;');
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: downloadUrl,
113
- responseType: 'arraybuffer',
114
- httpsAgent: protocol === 'https' ? httpsAgent : undefined,
115
- timeout: 120000 // 2 minuti di timeout per i video corposi
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
- // Scrittura fisica del file su disco C:\download\data\file.mp4
128
- fs.writeFileSync(finalFilePath, videoBuffer);
129
- node.log(`File salvato con successo in: ${finalFilePath}`);
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; // Spedisce il percorso del file salvato come testo
178
+ outMsg.payload = finalFilePath;
134
179
  outMsg.channel = ch;
135
180
  outMsg.filename = finalFileName;
136
181
  node.send(outMsg);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-hik-media-buffer",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "description": "Ottiene buffer video e immagine da camere Hikvision via ISAPI",
5
5
  "keywords": [
6
6
  "node-red",