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 +11 -0
- package/README.md +16 -1
- package/lib/camera.js +143 -18
- package/lib/isapi.js +6 -0
- package/package.json +1 -1
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 =
|
|
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
|
|
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'.
|
|
168
|
-
// encoder can wedge (no output, ever) after a kernel driver error, so only auto-
|
|
169
|
-
|
|
170
|
-
|
|
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 :
|
|
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 ${
|
|
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', '
|
|
191
|
-
|
|
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
|
-
|
|
210
|
-
|
|
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
|
|
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
|
-
|
|
238
|
-
|
|
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)
|
|
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.
|
|
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",
|