homebridge-hikvision-dvr 1.2.1 → 1.2.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.2
4
+
5
+ Fixes video freezing and high load on a Raspberry Pi when several cameras are opened at once (for example the 4-camera grid on an Apple TV).
6
+
7
+ - With *Video processing* on Automatic, the plugin now chooses how to serve each stream by how many are being viewed at the same time. One viewer gets the main stream at up to 1280x720; two get the main stream at up to 960x540; three or more get the DVR's sub stream scaled to 640x360 at 800 kbps (the Pi hardware encoder runs four of these at 25 fps; a fifth is passed through unchanged). The DVR can only deliver about 5.5 Mbit/s in total, so several main streams at once gave about 13 fps anyway.
8
+ - The choice is made for each stream when it starts and the log says which path was used and why. When the grid is closed, the next single view is back on the best path.
9
+ - A failing hardware encode no longer switches the whole plugin to libx264 until restart. Only that stream falls back (to libx264 at up to 640 px wide, or pass-through if two are already running). Only if nothing else is encoding in hardware is the encoder treated as wedged, and then it is avoided for 10 minutes and tried again, not for good.
10
+ - Waits up to 8 s (was 6 s) for the hardware encoder's first output before falling back.
11
+ - Live view starts much faster on a Pi: the first picture now arrives in about 0.5 s for a single view and 0.7-0.9 s per tile in the grid (was 2.5 s and 2.6-5 s). The hardware path was throwing away its first keyframe (about 2 s) and probing its input for longer than needed. When the DVR joins a sub stream mid-picture-group (which used to delay a tile by 2-3 s), the plugin now asks it for a keyframe.
12
+ - Hardware *decoding* on the Pi was tested and is not used: it started slower, used more CPU and dropped frames.
13
+
3
14
  ## 1.2.1
4
15
 
5
16
  - The plugin's settings page in the Homebridge UI shows an **Open web console** button and the console's address, instead of trying to embed the console (the DVR's pages don't work well inside the Homebridge UI, which also blocks their dialogs).
package/README.md CHANGED
@@ -31,7 +31,7 @@ Only the IP address, username and password are needed. Everything else is option
31
31
  | Motion sensors | on | A motion sensor per camera, from the DVR's own motion detection (see below) |
32
32
  | Motion reset | 30 s | How long motion stays on after the DVR's last motion event |
33
33
  | Verification code | read from DVR | Only needed if the plugin can't read it. Use the 6-letter code on the DVR's sticker, or the one set in Hik-Connect / HiLook |
34
- | Video processing | Automatic | Uses the Raspberry Pi hardware encoder when available (for views wider than 640 px; smaller views use libx264, which starts faster), otherwise libx264. *No transcoding* passes the DVR's stream through untouched (lowest CPU, experimental) |
34
+ | Video processing | Automatic | Uses the Raspberry Pi hardware encoder when available (for views wider than 640 px; smaller views use libx264, which starts faster), otherwise libx264. With several viewers at once (e.g. an Apple TV camera grid) it automatically uses smaller output and the DVR's sub stream so a Pi keeps up (see below). *No transcoding* passes the DVR's stream through untouched (lowest CPU, experimental) |
35
35
  | Use sub stream up to width | 0 (never) | e.g. `320` serves Apple Watch views from the low-res sub stream |
36
36
  | Minimum bitrate | automatic | iOS often asks for only ~300 kbps; the plugin uses at least 1200 kbps for 720p+. `0` = exactly what iOS asks |
37
37
  | Max viewers per camera | 2 | |
@@ -79,6 +79,21 @@ which keeps the load on the DVR and the Homebridge machine down. Limitations:
79
79
  - It is plain HTTP, like the DVR itself, so **don't port-forward it** or expose it to the internet.
80
80
  - Tested only on a HiLook DVR-104G-F1.
81
81
 
82
+ ## Several cameras at once
83
+
84
+ An Apple TV camera grid starts four streams at the same moment. On a Raspberry Pi (Automatic video processing) the plugin
85
+ picks a path for every stream as it starts, depending on how many are being viewed, and logs the choice:
86
+
87
+ | Viewers | Source | Output |
88
+ |---|---|---|
89
+ | 1 | main stream | up to 1280x720 (hardware encoder; libx264 if the view is 640 px or less) |
90
+ | 2 | main stream | up to 960x540, hardware encoder |
91
+ | 3 or more | sub stream | 640x360, 800 kbps, hardware encoder; a fifth and later stream is passed through unchanged |
92
+
93
+ If the hardware encoder fails for one stream, only that stream falls back to libx264. If it is wedged (nothing is encoding in hardware),
94
+ it is avoided for 10 minutes and then tried again. The DVR itself delivers only a few Mbit/s in total, so several main streams at once
95
+ would be slow even with a faster computer. Hardware video *decoding* on the Pi was tried and made things slower.
96
+
82
97
  ## How it works
83
98
 
84
99
  - Discovers cameras via ISAPI: analog DVR inputs, NVR IP channels, or a single IP camera.
package/lib/camera.js CHANGED
@@ -11,7 +11,26 @@ const START_CODE = Buffer.from([0, 0, 0, 1]);
11
11
  const PROFILES = ['baseline', 'main', 'high'];
12
12
  const LEVELS = ['3.1', '3.2', '4.0'];
13
13
  const SOFTWARE_MAX_WIDTH = 640; // auto mode: views up to this width use libx264 even on a Pi
14
- const HW_TIMEOUT = 6000; // ms to wait for the hardware encoder's first output
14
+ const HW_TIMEOUT = 8000; // ms to wait for the hardware encoder's first output
15
+ const HW_RETRY = 10 * 60 * 1000; // after a real hardware-encoder failure, use software only this long
16
+ const PREPARE_TTL = 10000; // a prepared-but-not-started session counts towards "how many viewers are coming" this long
17
+ // Concurrency tiers for auto mode on a Pi (measured on a Pi 3 A+, one stream = ffmpeg decode of the DVR's 960x1080 main stream,
18
+ // scale, hardware encode: 1280x720 ~100% of a core, 960x540 ~87%; sub stream to 640x360 ~25%; the DVR itself delivers
19
+ // only ~5.5 Mbit/s in total, so 3+ main streams drop to ~13 fps; the Pi codec fails to open an encoder for the 4th 720p session).
20
+ const MAIN_MAX_VIEWERS = 2; // up to this many concurrent viewers get the main stream
21
+ const SMALL = { width: 640, height: 360, bitrate: 800 }; // light path: sub stream scaled up, hardware encoded
22
+ const MAX_HW_SESSIONS = 4; // hardware encodes at once; further viewers get the sub stream passed through
23
+ const MAX_SW_SESSIONS = 2; // libx264 encodes at once while the hardware encoder is marked broken or one hardware encode failed
24
+ // Hardware DECODE (-c:v h264_v4l2m2m before -i) was measured on the Pi 3 A+ and is deliberately not used: with the DVR's FLV pipe
25
+ // input it was slower to start (2-7 s vs 1.5 s), used 2.5-3.3 cores instead of ~1 (ffmpeg copies every frame out of the codec's
26
+ // buffers) and delivered only a fraction of the frames when combined with the hardware encoder, while software decoding of the
27
+ // 960x1080 main stream only costs about half a core.
28
+
29
+ /** Per-platform streaming state shared by all cameras (kept on the platform object). */
30
+ function streamState(platform) {
31
+ if (!platform.streamState) platform.streamState = { active: new Set(), prepared: new Map(), hwBrokenUntil: 0 };
32
+ return platform.streamState;
33
+ }
15
34
 
16
35
  /**
17
36
  * HomeKit camera streaming delegate for one DVR channel.
@@ -123,6 +142,7 @@ class HikCamera {
123
142
  });
124
143
  socket.on('error', (e) => this.log.debug(`[${this.name}] RTCP socket error: ${e.message}`));
125
144
  const ssrc = crypto.randomBytes(4).readUInt32BE(0) & 0x7fffffff || 1;
145
+ streamState(this.platform).prepared.set(request.sessionID, Date.now());
126
146
  this.pending.set(request.sessionID, {
127
147
  address: request.targetAddress,
128
148
  port: request.video.port,
@@ -160,35 +180,98 @@ class HikCamera {
160
180
  }
161
181
  }
162
182
 
183
+ /**
184
+ * Decides how to serve one viewer: which DVR stream, which encoder and what output size.
185
+ * On a Pi (auto mode) this depends on how many viewers there are in total, since 4 camera tiles on an Apple TV
186
+ * all start at once and the Pi cannot transcode four main streams.
187
+ */
188
+ plan(sessionID, video) {
189
+ const p = this.platform;
190
+ const st = streamState(p);
191
+ st.prepared.delete(sessionID);
192
+ const now = Date.now();
193
+ for (const [id, t] of st.prepared) if (now - t > PREPARE_TTL) st.prepared.delete(id);
194
+ const viewers = st.active.size + st.prepared.size + 1;
195
+ const userSub = p.subStreamMaxWidth > 0 && video.width <= p.subStreamMaxWidth;
196
+ let sub = userSub;
197
+ let mode = p.encoder;
198
+ let cap = null; // [maxWidth, maxHeight, maxBitrate] of the output
199
+ let why = '';
200
+ const count = (m) => [...st.active].filter((x) => x.mode === m).length;
201
+ if (mode === 'h264_v4l2m2m' && p.encoderAuto) {
202
+ const hwOk = now >= st.hwBrokenUntil;
203
+ if (!hwOk) {
204
+ sub = true;
205
+ mode = count('libx264') < MAX_SW_SESSIONS ? 'libx264' : 'copy';
206
+ cap = [SMALL.width, SMALL.height, SMALL.bitrate];
207
+ why = 'hardware encoder marked broken, sub stream';
208
+ } else if (viewers <= 1) {
209
+ if (video.width <= SOFTWARE_MAX_WIDTH) mode = 'libx264';
210
+ cap = [1280, 720, Infinity];
211
+ why = 'single viewer, main stream';
212
+ } else if (viewers <= MAIN_MAX_VIEWERS) {
213
+ cap = [960, 540, Infinity];
214
+ why = `${viewers} viewers, main stream at reduced size`;
215
+ } else {
216
+ sub = true;
217
+ cap = [SMALL.width, SMALL.height, SMALL.bitrate];
218
+ why = `${viewers} viewers, light sub-stream path`;
219
+ if (count('h264_v4l2m2m') >= MAX_HW_SESSIONS) {
220
+ mode = 'copy';
221
+ why = `${viewers} viewers, sub stream passed through`;
222
+ }
223
+ }
224
+ } else if (mode === 'libx264' && p.encoderAuto && viewers > MAIN_MAX_VIEWERS) {
225
+ // No hardware encoder at all (e.g. a Mac or a PC): keep CPU use bounded the same way.
226
+ sub = true;
227
+ cap = [SMALL.width, SMALL.height, SMALL.bitrate];
228
+ why = `${viewers} viewers, light sub-stream path`;
229
+ }
230
+ if (!why) why = 'configured';
231
+ let width = video.width;
232
+ let height = video.height;
233
+ if (cap && width > cap[0]) {
234
+ width = cap[0];
235
+ height = Math.round((video.height * width) / video.width / 2) * 2;
236
+ }
237
+ return { sub, mode, width, height, maxBitrate: cap ? cap[2] : Infinity, why, viewers };
238
+ }
239
+
163
240
  startStream(sessionID, info, video, callback) {
164
241
  const p = this.platform;
165
- const useSub = p.subStreamMaxWidth > 0 && video.width <= p.subStreamMaxWidth;
242
+ const st = streamState(p);
243
+ const plan = this.plan(sessionID, video);
244
+ const useSub = plan.sub;
166
245
  const stream = useSub ? 2 : 1;
167
- // 'h264_v4l2m2m' | 'libx264' | 'copy'. Small views are cheap in software and start faster, and the Pi's hardware
168
- // encoder can wedge (no output, ever) after a kernel driver error, so only auto-pick it for larger views.
169
- let mode = p.encoder;
170
- if (mode === 'h264_v4l2m2m' && (p.hwBroken || (p.encoderAuto && video.width <= SOFTWARE_MAX_WIDTH))) mode = 'libx264';
246
+ // 'h264_v4l2m2m' | 'libx264' | 'copy'. A single small view is cheap in software and starts faster, and the Pi's
247
+ // hardware encoder can wedge (no output, ever) after a kernel driver error, so it is only auto-picked for larger views.
248
+ const mode = plan.mode;
249
+ let outW = plan.width;
250
+ let outH = plan.height;
171
251
  const fps = Math.min(video.fps, 25);
172
252
  // iOS often asks for ~300 kbps even on Wi-Fi, which looks poor; apply a resolution-based floor.
173
- const floor = p.minBitrate !== undefined ? p.minBitrate : video.width >= 1280 ? 1200 : video.width >= 640 ? 600 : 300;
174
- const bitrate = Math.max(video.max_bit_rate, floor, 100);
253
+ const floor = p.minBitrate !== undefined ? p.minBitrate : outW >= 1280 ? 1200 : outW >= 640 ? 600 : 300;
254
+ const bitrate = Math.min(Math.max(video.max_bit_rate, floor, 100), Math.max(plan.maxBitrate, floor));
175
255
  const log = this.log;
176
256
  const tag = `[${this.name}]`;
177
257
 
178
- log.info(`${tag} Starting ${video.width}x${video.height}@${fps} ${bitrate}kbps from ${useSub ? 'sub' : 'main'} stream (${mode})`);
258
+ log.info(`${tag} Starting ${outW}x${outH}@${fps} ${bitrate}kbps from ${useSub ? 'sub' : 'main'} stream (${mode}): ${plan.why}${outW !== video.width ? `; viewer asked for ${video.width}x${video.height}` : ''}`);
179
259
 
180
- const session = { procs: [], rtsp: null, socket: info.socket, timer: null, stopped: false, hwTimer: null, hwFallback: null, hwFirst: null, armHw: null };
260
+ const session = { mode, procs: [], rtsp: null, socket: info.socket, timer: null, stopped: false, hwTimer: null, hwFallback: null, hwFirst: null, armHw: null };
181
261
  this.sessions.set(sessionID, session);
262
+ st.active.add(session);
182
263
 
183
264
  const srtpOut = ['-payload_type', String(video.pt), '-ssrc', String(info.ssrc), '-f', 'rtp',
184
265
  '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80', '-srtp_out_params', info.srtp,
185
266
  `srtp://${info.address}:${info.port}?rtcpport=${info.port}&pkt_size=${video.mtu}`];
186
267
  const logLevel = ['-hide_banner', '-loglevel', p.debug ? 'warning' : 'error'];
187
268
  // Decrypted camera video arrives as FLV carrying the DVR's own timestamps.
269
+ // probesize: the FLV already carries SPS/PPS, so only the first frame is needed; a larger value makes ffmpeg wait for that many
270
+ // bytes of (low bitrate) sub stream to arrive in real time, ~0.4 s.
188
271
  // showall: the DVR's keyframes carry no recovery point, so without it the decoder drops ~1 s of frames before the first output.
189
272
  const input = [...logLevel, '-fflags', '+nobuffer+discardcorrupt', '-flags', 'low_delay', '-flags2', '+showall',
190
- '-probesize', '32768', '-analyzeduration', '0', '-f', 'flv', '-i', 'pipe:0', '-an', '-sn', '-dn'];
191
- const scale = `scale=${video.width}:${video.height}:flags=fast_bilinear,format=yuv420p`;
273
+ '-probesize', '4096', '-analyzeduration', '0', '-f', 'flv', '-i', 'pipe:0', '-an', '-sn', '-dn'];
274
+ let scale = `scale=${outW}:${outH}:flags=fast_bilinear,format=yuv420p`;
192
275
 
193
276
  let first; // the ffmpeg process fed by the RTSP stream
194
277
  let flv;
@@ -206,8 +289,12 @@ class HikCamera {
206
289
  // Pi hardware encoder doesn't repeat SPS/PPS before keyframes, which HomeKit needs.
207
290
  // Encode to a pipe, re-insert SPS/PPS in Node, and let a second (copy-only) ffmpeg do SRTP.
208
291
  // The encoder output is constant frame rate, so the sender can number frames itself.
209
- const sender = this.spawn(session, [...logLevel, '-fflags', '+nobuffer', '-probesize', '32768', '-analyzeduration', '0',
210
- '-f', 'h264', '-framerate', String(fps), '-i', 'pipe:0', '-c:v', 'copy', ...srtpOut]);
292
+ // -copyinkf: ffmpeg's raw h264 demuxer does not flag the encoder's first IDR as a keyframe, so a plain stream copy discards
293
+ // everything up to the second IDR (~2 s); the stream always starts with an IDR here, so keep the first packet.
294
+ // Probing must stay minimal (one packet, 32 bytes): with larger values ffmpeg decodes more of the input first, and under
295
+ // startup load (several tiles at once) that sometimes took 0.4-1 s. SPS/PPS are in the stream and rtp output needs nothing else.
296
+ const sender = this.spawn(session, [...logLevel, '-fflags', '+nobuffer', '-probesize', '32', '-analyzeduration', '0', '-max_probe_packets', '1',
297
+ '-f', 'h264', '-framerate', String(fps), '-i', 'pipe:0', '-c:v', 'copy', '-copyinkf', ...srtpOut]);
211
298
  first = this.spawn(session, [...input, '-vf', scale, '-r', String(fps), '-c:v', 'h264_v4l2m2m', '-g', String(fps * 2),
212
299
  '-b:v', `${bitrate}k`, '-f', 'h264', 'pipe:1']);
213
300
  let sps = null;
@@ -227,21 +314,42 @@ class HikCamera {
227
314
  splitter.push(chunk);
228
315
  clearTimeout(session.hwTimer);
229
316
  session.hwFallback = null;
317
+ session.hwOutput = true;
230
318
  });
231
- // No encoded output at all means the hardware encoder is wedged (or failed to open): switch this session to libx264 (and later ones).
319
+ // No encoded output at all: the hardware encoder failed to open, is overloaded or (rarely) wedged. Switch this session to
320
+ // libx264 at a small size. Only when no other hardware encode is working is the driver considered broken for everyone,
321
+ // and then only for HW_RETRY.
232
322
  session.hwFirst = first;
233
323
  session.hwFallback = (why) => {
234
324
  session.hwFallback = null;
235
325
  clearTimeout(session.hwTimer);
236
326
  if (session.stopped) return;
237
- p.hwBroken = true;
238
- log.warn(`${tag} Hardware encoder ${why} (the Pi's video driver may need a reboot), using libx264`);
327
+ const othersWorking = [...st.active].some((x) => x !== session && x.mode === 'h264_v4l2m2m' && x.hwOutput);
328
+ // Only a wedge (nothing else is encoding in hardware) disables the hardware encoder for everyone, and only for HW_RETRY.
329
+ // An ffmpeg that merely exited may have failed for other reasons (DVR stream gone), so it needs a V4L2 error to count.
330
+ const wedge = !othersWorking && (why.startsWith('produced') || /v4l2|video\d+|encoder|codec/i.test(session.lastErr || ''));
331
+ if (wedge) st.hwBrokenUntil = Date.now() + HW_RETRY;
332
+ const light = [...st.active].filter((x) => x !== session && x.mode === 'libx264').length < MAX_SW_SESSIONS;
333
+ log.warn(`${tag} Hardware encoder ${why}; this stream switches to ${light ? 'libx264' : 'pass-through'}${wedge ? ` and the hardware encoder is not used for ${HW_RETRY / 60000} min (the Pi's video driver may need a reboot)` : ' (other streams keep using the hardware encoder)'}`);
239
334
  for (const proc of session.procs.splice(0)) {
240
335
  proc.replaced = true;
241
336
  proc.kill('SIGTERM');
242
337
  setTimeout(() => proc.exitCode === null && proc.kill('SIGKILL'), 5000).unref();
243
338
  }
244
339
  started = false;
340
+ keyRequested = false;
341
+ requestKeyFrame();
342
+ if (!light) {
343
+ session.mode = 'copy';
344
+ launch('copy');
345
+ return;
346
+ }
347
+ session.mode = 'libx264';
348
+ if (outW > SOFTWARE_MAX_WIDTH) { // libx264 at 720p takes 2+ cores of a Pi
349
+ outH = Math.round((outH * SOFTWARE_MAX_WIDTH) / outW / 2) * 2;
350
+ outW = SOFTWARE_MAX_WIDTH;
351
+ scale = `scale=${outW}:${outH}:flags=fast_bilinear,format=yuv420p`;
352
+ }
245
353
  launch('libx264');
246
354
  };
247
355
  // The clock starts when the first keyframe is fed to the encoder, not while RTSP is still connecting.
@@ -261,8 +369,17 @@ class HikCamera {
261
369
  };
262
370
 
263
371
  let started = false;
372
+ let keyRequested = false;
264
373
  launch(mode);
265
374
 
375
+ // The DVR usually sends a keyframe right after PLAY, but sometimes (mostly sub streams, soon after another session on the
376
+ // same stream) it joins mid-GOP and the next keyframe is 2+ s away. Ask for one (ISAPI, harmless if unsupported), once.
377
+ const requestKeyFrame = () => {
378
+ if (keyRequested || !p.isapi || !p.isapi.requestKeyFrame) return;
379
+ keyRequested = true;
380
+ p.isapi.requestKeyFrame(this.channel, stream).then(() => log.debug(`${tag} Requested a keyframe`), (e) => log.debug(`${tag} Keyframe request failed: ${e.message}`));
381
+ };
382
+
266
383
  // RTSP -> decrypt -> ffmpeg
267
384
  const decryptor = new HikDecryptor(p.verificationCode);
268
385
  const rtsp = new RtspClient(this.rtspUrl(stream), { username: p.username, password: p.password });
@@ -280,7 +397,10 @@ class HikCamera {
280
397
  const plain = decryptor.process(nal, flagged);
281
398
  const type = plain[0] & 0x1f;
282
399
  if (!started) {
283
- if (type !== 7 && type !== 8 && type !== 5) return; // start on a keyframe
400
+ if (type !== 7 && type !== 8 && type !== 5) { // start on a keyframe
401
+ requestKeyFrame();
402
+ return;
403
+ }
284
404
  started = true;
285
405
  }
286
406
  flv.push(plain, ts, marker);
@@ -293,6 +413,7 @@ class HikCamera {
293
413
 
294
414
  rtsp.start().then(() => {
295
415
  log.debug(`${tag} RTSP playing ${this.rtspUrl(stream)}`);
416
+ session.kfTimer = setTimeout(() => { if (!started) requestKeyFrame(); }, 400);
296
417
  callback();
297
418
  }).catch((e) => {
298
419
  log.error(`${tag} Could not open DVR stream: ${e.message}`);
@@ -320,6 +441,7 @@ class HikCamera {
320
441
  proc.stdin.on('error', () => { /* EPIPE when ffmpeg exits */ });
321
442
  proc.stderr.on('data', (d) => {
322
443
  const text = d.toString().trim();
444
+ if (text) session.lastErr = text.slice(-500);
323
445
  if (text) this.log[this.platform.debug ? 'info' : 'debug'](`${tag} ffmpeg: ${text}`);
324
446
  });
325
447
  proc.on('error', (e) => this.log.error(`${tag} Failed to start ffmpeg (${this.platform.ffmpeg}): ${e.message}`));
@@ -346,6 +468,7 @@ class HikCamera {
346
468
  }
347
469
 
348
470
  stopStream(sessionID) {
471
+ streamState(this.platform).prepared.delete(sessionID);
349
472
  const pending = this.pending.get(sessionID);
350
473
  if (pending) {
351
474
  this.pending.delete(sessionID);
@@ -354,8 +477,10 @@ class HikCamera {
354
477
  const session = this.sessions.get(sessionID);
355
478
  if (!session) return;
356
479
  this.sessions.delete(sessionID);
480
+ streamState(this.platform).active.delete(session);
357
481
  session.stopped = true;
358
482
  clearTimeout(session.timer);
483
+ clearTimeout(session.kfTimer);
359
484
  if (session.rtsp) session.rtsp.stop();
360
485
  for (const proc of session.procs) {
361
486
  try { proc.stdin.end(); } catch (e) { /* ignore */ }
package/lib/isapi.js CHANGED
@@ -158,6 +158,12 @@ class Isapi {
158
158
  }
159
159
  }
160
160
 
161
+ /** Asks the DVR to send an IDR frame on a stream (Hikvision ISAPI; harmless on devices that lack it). */
162
+ async requestKeyFrame(channel, stream = 1) {
163
+ const res = await this.request('PUT', `/ISAPI/Streaming/channels/${channel * 100 + stream}/requestKeyFrame`);
164
+ if (res.status !== 200) throw new Error(`HTTP ${res.status}`);
165
+ }
166
+
161
167
  async snapshot(channel, stream = 1) {
162
168
  return this.get(`/ISAPI/Streaming/channels/${channel * 100 + stream}/picture`);
163
169
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-hikvision-dvr",
3
3
  "displayName": "Hikvision DVR / NVR Cameras",
4
- "version": "1.2.1",
4
+ "version": "1.2.2",
5
5
  "description": "Hikvision, HiLook and other Hik-based DVR/NVR cameras in HomeKit, with automatic camera discovery and support for encrypted (Hik-Connect / HiLook) streams.",
6
6
  "main": "index.js",
7
7
  "license": "MIT",