homebridge-ring-secure-video 1.0.0-beta.7 → 1.0.0
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 +57 -0
- package/README.md +20 -17
- package/config.schema.json +62 -29
- package/lib/camera-source.js +43 -6
- package/lib/camera.js +0 -4
- package/lib/ffmpeg-capabilities.js +83 -10
- package/lib/hksv-options.js +5 -0
- package/lib/hksv-work-queue.js +0 -12
- package/lib/intercom.js +58 -37
- package/lib/ring-media-ingress.js +3 -4
- package/lib/ring-platform.js +37 -12
- package/package.json +1 -2
- package/branding/Homebridge_x_Ring.svg +0 -1
- package/branding/icon.png +0 -0
- package/branding/lighting.png +0 -0
- package/branding/product.png +0 -0
- package/branding/security-system.png +0 -0
- package/lib/util.js +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,62 @@
|
|
|
1
1
|
# homebridge-ring-secure-video
|
|
2
2
|
|
|
3
|
+
## 1.0.0 (2026-11-14)
|
|
4
|
+
|
|
5
|
+
First stable release. Compared with homebridge-ring 14.3.0, it adds HomeKit Secure Video
|
|
6
|
+
recording with media profiles (`media`), a separate motion sensor per camera for instant
|
|
7
|
+
notifications, motion from Ring's event history when push notifications fail, automatic Ring
|
|
8
|
+
session refresh with an optional sensor, an optional recording on doorbell press, Ring Intercom
|
|
9
|
+
Video as a camera, and the fixes listed in the betas below. Every homebridge-ring setting is
|
|
10
|
+
available with the same name. Tested on a Raspberry Pi 4 (Node 22, Homebridge 2.5) with a Ring
|
|
11
|
+
Video Doorbell 3 Plus, Ring Alarm and 21 sensors.
|
|
12
|
+
|
|
13
|
+
Changes since 1.0.0-beta.7:
|
|
14
|
+
|
|
15
|
+
**Fixes:**
|
|
16
|
+
|
|
17
|
+
- A packet on a live view's audio port that isn't valid RTP no longer stops Homebridge (any
|
|
18
|
+
device on the network could send one). Any other error that would have reached rxjs's
|
|
19
|
+
unhandled-error handler (which stops Homebridge) is logged instead; if it ended a stream, such
|
|
20
|
+
as a live view's audio, only that stream stops.
|
|
21
|
+
- Ring Intercom Video: a pre-warmed live call that fails no longer stops Homebridge, and the
|
|
22
|
+
recording starts its own call; an unclaimed pre-warmed call is stopped after 20 s without
|
|
23
|
+
stopping a call a recording took meanwhile. Pre-warming happens only with HKSV recording on.
|
|
24
|
+
The check for an active ding runs only for Intercom Video (audio intercoms get dings by push),
|
|
25
|
+
every 5 s instead of 3 s, and a slow check no longer has its answer dropped when the next is
|
|
26
|
+
due.
|
|
27
|
+
- A renewed Ring token is saved only into this plugin's platform block. Before, if no block of
|
|
28
|
+
this plugin had the old token (it was changed in the settings meanwhile, or only another
|
|
29
|
+
plugin's block had it, such as homebridge-ring set up with the same token), it was written by
|
|
30
|
+
text replace, possibly into the other plugin's block; now config.json is left as it is, and
|
|
31
|
+
"Could not save a new Ring token" is logged once.
|
|
32
|
+
- When Homebridge shuts down, live views, talk-back, recordings and Ring calls are ended and the
|
|
33
|
+
plugin disconnects from Ring, instead of leaving them running until the process exits.
|
|
34
|
+
- With `codec` set to `auto` (the default outside `lowPower`), recordings on a PC, Docker, a NAS
|
|
35
|
+
or a Raspberry Pi 5 no longer fail by picking the Raspberry Pi 4 hardware encoder
|
|
36
|
+
(`h264_v4l2m2m`) just because FFmpeg lists it: it is picked only on Linux with a video encoder
|
|
37
|
+
device Homebridge can open, and `h264_videotoolbox` only on a Mac; otherwise `auto` picks
|
|
38
|
+
`libx264`. The check now asks the FFmpeg that records (the one bundled with the plugin, or the
|
|
39
|
+
one set with `ffmpegPath`) instead of a system `ffmpeg`, and gives up after 10 s instead of
|
|
40
|
+
holding recordings if FFmpeg hangs; a check that fails is logged and tried again 5 minutes
|
|
41
|
+
later. FFmpeg is only asked when `auto` has to choose (not with `lowPower` or a set codec).
|
|
42
|
+
|
|
43
|
+
**Settings form and docs:**
|
|
44
|
+
|
|
45
|
+
- "Custom FFmpeg Path" is in the settings form again (it was only in the JSON editor), and CI
|
|
46
|
+
now fails if a setting is left out of the form. Settings are grouped by topic, the hide-camera
|
|
47
|
+
settings have descriptions (hiding the camera motion sensor also stops HKSV recordings), and
|
|
48
|
+
the prebuffer setting no longer suggests it adds video from before the motion.
|
|
49
|
+
- The README shows the plugin's own icon instead of the "Homebridge x Ring" logo; Ring's logo
|
|
50
|
+
and unused upstream images are removed and no longer in the npm package. The README says which
|
|
51
|
+
setups are tested and which aren't.
|
|
52
|
+
|
|
53
|
+
**Other:** unused code removed; the encoder check runs FFmpeg once instead of twice.
|
|
54
|
+
|
|
55
|
+
**Known:** `npm audit --omit=dev` reports one high-severity advisory for `ip`
|
|
56
|
+
(GHSA-2p57-rm9w-gvfp), listed against `ip`, `werift` and ring-client-api 14.3.0. It concerns
|
|
57
|
+
`ip.isPublic`, which werift doesn't call, so the plugin isn't affected; it goes away with
|
|
58
|
+
ring-client-api 15 and the werift 0.26 it uses.
|
|
59
|
+
|
|
3
60
|
## 1.0.0-beta.7 (2026-11-14)
|
|
4
61
|
|
|
5
62
|
**Changes in behaviour to check after updating:**
|
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img alt="
|
|
2
|
+
<img alt="Ring Secure Video" src="https://raw.githubusercontent.com/zmanuel1234/homebridge-ring-secure-video/main/branding/homebridge-ring-secure-video.svg" width="150">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
# homebridge-ring-secure-video
|
|
@@ -13,8 +13,7 @@ It is meant to replace `homebridge-ring` on its own: one plugin, one Ring sign-i
|
|
|
13
13
|
|
|
14
14
|
## What it adds to homebridge-ring
|
|
15
15
|
|
|
16
|
-
Everything `homebridge-ring` exposes is here too
|
|
17
|
-
platform names differ, so its accessories are new to HomeKit and are set up again), plus:
|
|
16
|
+
Everything `homebridge-ring` exposes is here too, with the same settings, plus:
|
|
18
17
|
|
|
19
18
|
- **HomeKit Secure Video recording** for Ring cameras and doorbells, with media profiles for small
|
|
20
19
|
machines such as a Raspberry Pi (`media`).
|
|
@@ -105,8 +104,8 @@ Without `media.profile`, the profile is `adaptive`, so a config that only had
|
|
|
105
104
|
|
|
106
105
|
## Installation
|
|
107
106
|
|
|
108
|
-
Requires Node.js 22 or 24 and Homebridge 1.8 or later (2.
|
|
109
|
-
globally:
|
|
107
|
+
Requires Node.js 22 or 24 and Homebridge 1.8 or later (tested with 2.5; 1.8 is supported but not
|
|
108
|
+
tested). If Homebridge is installed globally:
|
|
110
109
|
|
|
111
110
|
```bash
|
|
112
111
|
npm i -g homebridge-ring-secure-video
|
|
@@ -161,11 +160,15 @@ Changing `homeKitAccessoryTag` updates both the exposed accessory name and the g
|
|
|
161
160
|
|
|
162
161
|
## HKSV Status
|
|
163
162
|
|
|
164
|
-
|
|
163
|
+
Behavior may vary by camera model, wired vs battery power, Ring API changes, and FFmpeg
|
|
164
|
+
environment.
|
|
165
165
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
166
|
+
Tested: complete HomeKit clips from a Ring Video Doorbell 3 Plus on a Raspberry Pi 4 (Node 22,
|
|
167
|
+
Homebridge 2.5) with each of `copy`, `h264_v4l2m2m` and `libx264` (see below). The HKSV fork this
|
|
168
|
+
continues reported 3 cameras on an M4 Mac mini. Not tested here: other Ring cameras, battery
|
|
169
|
+
cameras, Ring Intercom Video, `h264_videotoolbox` and Homebridge 1.8. Please
|
|
170
|
+
[report](https://github.com/zmanuel1234/homebridge-ring-secure-video/issues) how it works with
|
|
171
|
+
your setup.
|
|
169
172
|
|
|
170
173
|
Recordings follow the frame rate and audio format HomeKit asks for, and include audio only when
|
|
171
174
|
**Record Audio** is on in the camera's settings in the Home app.
|
|
@@ -218,12 +221,15 @@ ignores all of them. The Pi's hardware encoder uses `bitrateKbps` and `keyframeI
|
|
|
218
221
|
| Profile (`media.profile`) | `auto` picks |
|
|
219
222
|
| --- | --- |
|
|
220
223
|
| `lowPower` | `copy` |
|
|
221
|
-
| `adaptive` (default) or `quality` | `h264_v4l2m2m`
|
|
224
|
+
| `adaptive` (default) or `quality` | `h264_v4l2m2m` on Linux with a usable video encoder device (Raspberry Pi 4), else `h264_videotoolbox` on a Mac, else `libx264` |
|
|
222
225
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
`
|
|
226
|
-
|
|
226
|
+
A hardware encoder is picked only if the FFmpeg that records has it and the computer has the
|
|
227
|
+
hardware: `h264_v4l2m2m` needs a Linux video encoder device that Homebridge can open (the Pi 4's
|
|
228
|
+
`bcm2835-codec-encode`), so a Raspberry Pi 5, a PC or a NAS gets `libx264`, as does Docker unless
|
|
229
|
+
the device is passed through. The log shows the choice for each recording:
|
|
230
|
+
`Starting HKSV recording pipeline ... video=<codec>`. If FFmpeg doesn't answer when asked which
|
|
231
|
+
encoders it has, an error is logged (the first time) and `auto` records with `libx264` until FFmpeg
|
|
232
|
+
is asked again, at the first recording after 5 minutes.
|
|
227
233
|
|
|
228
234
|
#### Settings verified on a Raspberry Pi 4
|
|
229
235
|
|
|
@@ -248,9 +254,6 @@ For a Raspberry Pi, start with:
|
|
|
248
254
|
}
|
|
249
255
|
```
|
|
250
256
|
|
|
251
|
-
### Minimum specifications for HKSV:
|
|
252
|
-
[TBD - will be added as more users test and report their setups]
|
|
253
|
-
|
|
254
257
|
## Troubleshooting
|
|
255
258
|
|
|
256
259
|
For support and debugging:
|
package/config.schema.json
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"customUi": true,
|
|
6
6
|
"customUiPath": "./lib/homebridge-ui",
|
|
7
7
|
"customUiDevServer": "http://localhost:3000/api/plugins/settings-ui/homebridge-ring-secure-video",
|
|
8
|
-
"headerDisplay": "See
|
|
8
|
+
"headerDisplay": "Sign in to Ring here to get a refresh token (\"Generate New Refresh Token\" signs in again or changes accounts). See the [README](https://github.com/zmanuel1234/homebridge-ring-secure-video#readme) for each setting.",
|
|
9
9
|
"accountLinkingHeader": "Please supply your Ring credentials. These will be used to contact the Ring servers and generate a unique access token for your account. Your email and password will be sent directly to Ring and will not be stored anywhere.",
|
|
10
10
|
"schema": {
|
|
11
11
|
"type": "object",
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
"type": "integer",
|
|
108
108
|
"minimum": 4000,
|
|
109
109
|
"maximum": 16000,
|
|
110
|
-
"description": "
|
|
110
|
+
"description": "The pre-event length the plugin tells HomeKit it supports, 4000-16000 ms (default 4000). It doesn't add video from before the motion: the plugin keeps no video buffer and starts Ring's video when a recording begins, so leave this at the default."
|
|
111
111
|
},
|
|
112
112
|
"fragmentLengthMs": {
|
|
113
113
|
"title": "Fragment length (ms)",
|
|
@@ -193,11 +193,13 @@
|
|
|
193
193
|
},
|
|
194
194
|
"hideCameraLight": {
|
|
195
195
|
"title": "Hide Camera Light",
|
|
196
|
-
"type": "boolean"
|
|
196
|
+
"type": "boolean",
|
|
197
|
+
"description": "Hides the light switch of cameras with a light (Floodlight Cam, Spotlight Cam)."
|
|
197
198
|
},
|
|
198
199
|
"hideCameraMotionSensor": {
|
|
199
200
|
"title": "Hide Camera Motion Sensors",
|
|
200
|
-
"type": "boolean"
|
|
201
|
+
"type": "boolean",
|
|
202
|
+
"description": "Hides each camera's motion sensor. HomeKit Secure Video records when that sensor reports motion, so hiding it also stops recordings (and makes Doorbell Press Starts a Recording do nothing)."
|
|
201
203
|
},
|
|
202
204
|
"cameraMotionNotificationSensor": {
|
|
203
205
|
"title": "Separate Motion Sensor for Notifications",
|
|
@@ -212,16 +214,18 @@
|
|
|
212
214
|
},
|
|
213
215
|
"hideCameraSirenSwitch": {
|
|
214
216
|
"title": "Hide Camera Siren Switch",
|
|
215
|
-
"type": "boolean"
|
|
217
|
+
"type": "boolean",
|
|
218
|
+
"description": "Hides the siren switch of cameras with a siren."
|
|
216
219
|
},
|
|
217
220
|
"hideInHomeDoorbellSwitch": {
|
|
218
221
|
"title": "Hide In-Home Doorbell Switch",
|
|
219
|
-
"type": "boolean"
|
|
222
|
+
"type": "boolean",
|
|
223
|
+
"description": "Hides the switch that turns the in-home (mechanical or digital) chime of a wired Ring doorbell on or off."
|
|
220
224
|
},
|
|
221
225
|
"hideAlarmSirenSwitch": {
|
|
222
226
|
"title": "Hide Alarm Siren Switch",
|
|
223
227
|
"type": "boolean",
|
|
224
|
-
"description": "Hides switch that allows you to turn on the siren of the Ring Alarm. Enable this
|
|
228
|
+
"description": "Hides switch that allows you to turn on the siren of the Ring Alarm. Enable this if your Siri commands keep setting off the siren and you don't care about having the switch"
|
|
225
229
|
},
|
|
226
230
|
"externalCameraIdSalt": {
|
|
227
231
|
"title": "External Camera ID Salt",
|
|
@@ -231,7 +235,7 @@
|
|
|
231
235
|
"showPanicButtons": {
|
|
232
236
|
"title": "Show Panic Buttons",
|
|
233
237
|
"type": "boolean",
|
|
234
|
-
"description": "Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk
|
|
238
|
+
"description": "Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk: they are not guaranteed to work in an emergency, and no responsibility is taken for false alarms.** These function just like the SOS sliders in the Ring app."
|
|
235
239
|
},
|
|
236
240
|
"ffmpegPath": {
|
|
237
241
|
"title": "Custom FFmpeg Path",
|
|
@@ -256,7 +260,7 @@
|
|
|
256
260
|
"avoidSnapshotBatteryDrain": {
|
|
257
261
|
"title": "Avoid Snapshot Battery Drain",
|
|
258
262
|
"type": "boolean",
|
|
259
|
-
"description": "Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery. This is useful if have the Home app open constantly, causing frequent snapshot requests."
|
|
263
|
+
"description": "Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery. This is useful if you have the Home app open constantly, causing frequent snapshot requests."
|
|
260
264
|
},
|
|
261
265
|
"beamDurationSeconds": {
|
|
262
266
|
"title": "Ring Smart Lighting Timer",
|
|
@@ -273,7 +277,7 @@
|
|
|
273
277
|
"disableLogs": {
|
|
274
278
|
"title": "Disable All Logging",
|
|
275
279
|
"type": "boolean",
|
|
276
|
-
"description": "Disables all logs generated by
|
|
280
|
+
"description": "Disables all logs generated by this plugin"
|
|
277
281
|
},
|
|
278
282
|
"cameraStatusPollingSeconds": {
|
|
279
283
|
"title": "Camera/Chime Status Polling",
|
|
@@ -327,7 +331,7 @@
|
|
|
327
331
|
},
|
|
328
332
|
{
|
|
329
333
|
"type": "fieldset",
|
|
330
|
-
"title": "Optional
|
|
334
|
+
"title": "Optional Settings",
|
|
331
335
|
"expandable": false,
|
|
332
336
|
"items": [
|
|
333
337
|
{
|
|
@@ -335,15 +339,42 @@
|
|
|
335
339
|
"title": "HomeKit Secure Video",
|
|
336
340
|
"expandable": true,
|
|
337
341
|
"items": [
|
|
338
|
-
"alarmOnEntryDelay",
|
|
339
342
|
"enableHksv",
|
|
340
|
-
"
|
|
341
|
-
"
|
|
342
|
-
|
|
343
|
+
"disableHksvOnBattery",
|
|
344
|
+
"media"
|
|
345
|
+
]
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
"type": "fieldset",
|
|
349
|
+
"title": "Motion and Doorbell",
|
|
350
|
+
"expandable": true,
|
|
351
|
+
"items": [
|
|
352
|
+
"cameraMotionNotificationSensor",
|
|
353
|
+
"doorbellPressMotion",
|
|
354
|
+
"enableCameraMotionHistory"
|
|
355
|
+
]
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
"type": "fieldset",
|
|
359
|
+
"title": "Ring Session",
|
|
360
|
+
"expandable": true,
|
|
361
|
+
"items": [
|
|
343
362
|
"automaticSessionRefresh",
|
|
344
363
|
"sessionRefreshSensor",
|
|
345
|
-
"
|
|
346
|
-
"
|
|
364
|
+
"forceRefreshRingPushCredentials",
|
|
365
|
+
"forceRefreshRingApiSession"
|
|
366
|
+
]
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
"type": "fieldset",
|
|
370
|
+
"title": "Alarm and Location Mode",
|
|
371
|
+
"expandable": true,
|
|
372
|
+
"items": [
|
|
373
|
+
"alarmOnEntryDelay",
|
|
374
|
+
"nightModeBypassFor",
|
|
375
|
+
"hideAlarmSirenSwitch",
|
|
376
|
+
"showPanicButtons",
|
|
377
|
+
"locationModePollingSeconds"
|
|
347
378
|
]
|
|
348
379
|
},
|
|
349
380
|
{
|
|
@@ -351,17 +382,20 @@
|
|
|
351
382
|
"title": "Accessory Visibility",
|
|
352
383
|
"expandable": true,
|
|
353
384
|
"items": [
|
|
354
|
-
"homeKitAccessoryTag",
|
|
355
385
|
"hideLightGroups",
|
|
356
386
|
"hideDoorbellSwitch",
|
|
357
387
|
"hideCameraLight",
|
|
358
388
|
"hideCameraMotionSensor",
|
|
359
|
-
"cameraMotionNotificationSensor",
|
|
360
|
-
"doorbellPressMotion",
|
|
361
389
|
"hideCameraSirenSwitch",
|
|
362
|
-
"hideInHomeDoorbellSwitch"
|
|
363
|
-
|
|
364
|
-
|
|
390
|
+
"hideInHomeDoorbellSwitch"
|
|
391
|
+
]
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
"type": "fieldset",
|
|
395
|
+
"title": "HomeKit Identity (advanced)",
|
|
396
|
+
"expandable": true,
|
|
397
|
+
"items": [
|
|
398
|
+
"homeKitAccessoryTag",
|
|
365
399
|
"externalCameraIdSalt"
|
|
366
400
|
]
|
|
367
401
|
},
|
|
@@ -432,22 +466,21 @@
|
|
|
432
466
|
},
|
|
433
467
|
{
|
|
434
468
|
"type": "help",
|
|
435
|
-
"helpvalue": "<p>If you only want to display certain device types, you can specify
|
|
469
|
+
"helpvalue": "<p>If you only want to display certain device types, you can specify those device types here. All other device types will be removed from HomeKit. Example: security-panel to show your alarm, hp_cam_v1 to show floodlight cams.</p>"
|
|
436
470
|
}
|
|
437
471
|
]
|
|
438
472
|
},
|
|
439
473
|
{
|
|
440
474
|
"type": "fieldset",
|
|
441
|
-
"title": "
|
|
475
|
+
"title": "Cameras, Lights and Diagnostics",
|
|
442
476
|
"expandable": true,
|
|
443
477
|
"items": [
|
|
444
478
|
"avoidSnapshotBatteryDrain",
|
|
445
479
|
"beamDurationSeconds",
|
|
446
|
-
"debug",
|
|
447
|
-
"disableLogs",
|
|
448
480
|
"cameraStatusPollingSeconds",
|
|
449
|
-
"
|
|
450
|
-
"
|
|
481
|
+
"ffmpegPath",
|
|
482
|
+
"debug",
|
|
483
|
+
"disableLogs"
|
|
451
484
|
]
|
|
452
485
|
}
|
|
453
486
|
]
|
package/lib/camera-source.js
CHANGED
|
@@ -10,7 +10,7 @@ import { RtcpSenderInfo, RtcpSrPacket, RtpPacket, SrtpSession, SrtcpSession, } f
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { FragmentedMp4Parser } from "./fragmented-mp4-parser.js";
|
|
12
12
|
import { getFfmpegCapabilities, } from "./ffmpeg-capabilities.js";
|
|
13
|
-
import { getHksvRecordingResolutions, selectHksvVideoCodec } from "./hksv-options.js";
|
|
13
|
+
import { getHksvRecordingResolutions, hksvCodecNeedsCapabilities, selectHksvVideoCodec, } from "./hksv-options.js";
|
|
14
14
|
import { hksvRecordingQueue } from "./hksv-work-queue.js";
|
|
15
15
|
import { RingMediaIngress, } from "./ring-media-ingress.js";
|
|
16
16
|
import { normalizeMediaConfig } from "./media-config.js";
|
|
@@ -128,7 +128,15 @@ class StreamingSessionWrapper {
|
|
|
128
128
|
startStreamRequest.audio.packet_time, audioSrtpSession = new SrtpSession(getSessionConfig(this.audioSrtp));
|
|
129
129
|
let runningTimestamp;
|
|
130
130
|
this.transcodedAudioSplitter.addMessageHandler(({ message }) => {
|
|
131
|
-
|
|
131
|
+
let rtp;
|
|
132
|
+
try {
|
|
133
|
+
rtp = RtpPacket.deSerialize(message);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
// Not a valid RTP packet (too short, or a bad header length): dropped, so it doesn't reach
|
|
137
|
+
// the rxjs unhandled-error path (logged per packet, or fatal if no handler is set).
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
132
140
|
// For some reason HAP uses RFC 3550 timestamps instead of following RTP Paylod
|
|
133
141
|
// Format for Opus Speech and Audio Codec from RFC 7587 like everyone else.
|
|
134
142
|
// This calculates and replaces the timestamps before forwarding to Homekit.
|
|
@@ -374,6 +382,23 @@ export function isHksvEnabled(config, operatingOnBattery) {
|
|
|
374
382
|
return (config.enableHksv !== false &&
|
|
375
383
|
!(config.disableHksvOnBattery !== false && operatingOnBattery));
|
|
376
384
|
}
|
|
385
|
+
/** Every camera's source, so they can all be stopped when Homebridge shuts down. */
|
|
386
|
+
const cameraSources = new Set();
|
|
387
|
+
/**
|
|
388
|
+
* Homebridge is shutting down: end every live view, recording and Ring call, so no FFmpeg (live
|
|
389
|
+
* view, talk-back, recording) is left running after the bridge stops.
|
|
390
|
+
*/
|
|
391
|
+
export function shutDownCameraSources() {
|
|
392
|
+
for (const source of cameraSources) {
|
|
393
|
+
try {
|
|
394
|
+
source.shutdown();
|
|
395
|
+
}
|
|
396
|
+
catch (e) {
|
|
397
|
+
// One camera failing must not keep the others (or Ring's connection) open.
|
|
398
|
+
logError(e);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
377
402
|
export class CameraSource {
|
|
378
403
|
controller;
|
|
379
404
|
sessions = {};
|
|
@@ -391,6 +416,7 @@ export class CameraSource {
|
|
|
391
416
|
this.ringCamera = ringCamera;
|
|
392
417
|
this.config = config;
|
|
393
418
|
this.mediaIngress = new RingMediaIngress(ringCamera);
|
|
419
|
+
cameraSources.add(this);
|
|
394
420
|
const enableHksv = isHksvEnabled(config, ringCamera.operatingOnBattery);
|
|
395
421
|
const controllerOptions = {
|
|
396
422
|
cameraStreamCount: enableHksv ? 1 : 10,
|
|
@@ -551,12 +577,11 @@ export class CameraSource {
|
|
|
551
577
|
this.previousLoadSnapshotPromise = undefined;
|
|
552
578
|
}
|
|
553
579
|
}
|
|
554
|
-
fn = 1;
|
|
555
580
|
async loadAndCacheSnapshot(imageUuid) {
|
|
556
581
|
const start = Date.now();
|
|
557
582
|
logDebug(`Loading new snapshot into cache for ${this.ringCamera.name}${imageUuid ? ' by uuid' : ''}`);
|
|
558
583
|
try {
|
|
559
|
-
const
|
|
584
|
+
const newSnapshot = await this.ringCamera.getSnapshot({ uuid: imageUuid });
|
|
560
585
|
this.cachedSnapshot = newSnapshot;
|
|
561
586
|
this.cachedSnapshotAt = Date.now();
|
|
562
587
|
logDebug(`Snapshot cached for ${this.ringCamera.name}${imageUuid ? ' by uuid' : ''} (${getDurationSeconds(start)}s)`);
|
|
@@ -591,7 +616,7 @@ export class CameraSource {
|
|
|
591
616
|
}
|
|
592
617
|
return this.cachedSnapshot;
|
|
593
618
|
}
|
|
594
|
-
async handleSnapshotRequest(
|
|
619
|
+
async handleSnapshotRequest(_request, callback) {
|
|
595
620
|
try {
|
|
596
621
|
const snapshot = await this.getCurrentSnapshot();
|
|
597
622
|
if (!snapshot) {
|
|
@@ -771,7 +796,9 @@ export class CameraSource {
|
|
|
771
796
|
releaseQueueSlot = () => queueLease.release();
|
|
772
797
|
updateQueuedBytes = (bytes) => queueLease.setQueuedBytes(bytes);
|
|
773
798
|
throwIfClosed();
|
|
774
|
-
const capabilities =
|
|
799
|
+
const capabilities = hksvCodecNeedsCapabilities(this.config)
|
|
800
|
+
? await getFfmpegCapabilities()
|
|
801
|
+
: undefined, videoArguments = this.getHksvVideoArguments(capabilities), videoCodecIndex = videoArguments.indexOf('-vcodec'), selectedVideoCodec = videoCodecIndex >= 0
|
|
775
802
|
? String(videoArguments[videoCodecIndex + 1])
|
|
776
803
|
: 'unknown';
|
|
777
804
|
throwIfClosed();
|
|
@@ -907,6 +934,16 @@ export class CameraSource {
|
|
|
907
934
|
releaseQueueSlot?.();
|
|
908
935
|
}
|
|
909
936
|
}
|
|
937
|
+
shutdown() {
|
|
938
|
+
for (const [sessionId, session] of Object.entries(this.sessions)) {
|
|
939
|
+
delete this.sessions[sessionId];
|
|
940
|
+
session.stop();
|
|
941
|
+
}
|
|
942
|
+
for (const streamId of [...this.activeRecordingSessions.keys()]) {
|
|
943
|
+
this.closeRecordingStream(streamId, 'Homebridge shutting down');
|
|
944
|
+
}
|
|
945
|
+
this.mediaIngress.shutdown();
|
|
946
|
+
}
|
|
910
947
|
closeRecordingStream(streamId, reason) {
|
|
911
948
|
logInfo(`HKSV recording stream closed for ${this.ringCamera.name} (streamId=${streamId}, reason=${String(reason)})`);
|
|
912
949
|
this.closedRecordingStreams.add(streamId);
|
package/lib/camera.js
CHANGED
|
@@ -227,7 +227,6 @@ export function addDoorbellPressMotion(motion, presses, onPressMotion, watchdogM
|
|
|
227
227
|
}));
|
|
228
228
|
}
|
|
229
229
|
export class Camera extends BaseDataAccessory {
|
|
230
|
-
inHomeDoorbellStatus;
|
|
231
230
|
cameraSource;
|
|
232
231
|
device;
|
|
233
232
|
accessory;
|
|
@@ -346,9 +345,6 @@ export class Camera extends BaseDataAccessory {
|
|
|
346
345
|
});
|
|
347
346
|
}
|
|
348
347
|
if (device.hasInHomeDoorbell && !config.hideInHomeDoorbellSwitch) {
|
|
349
|
-
this.device.onInHomeDoorbellStatus.subscribe((data) => {
|
|
350
|
-
this.inHomeDoorbellStatus = data;
|
|
351
|
-
});
|
|
352
348
|
this.registerObservableCharacteristic({
|
|
353
349
|
characteristicType: Characteristic.On,
|
|
354
350
|
serviceType: Service.Switch,
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
import { execFile } from 'node:child_process';
|
|
2
|
+
import { accessSync, constants, readdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
2
4
|
import { promisify } from 'node:util';
|
|
5
|
+
import { defaultFfmpegPath } from '@homebridge/camera-utils';
|
|
3
6
|
import { getFfmpegPath } from 'ring-client-api/ffmpeg';
|
|
4
|
-
import { logDebug } from 'ring-client-api/util';
|
|
7
|
+
import { logDebug, logError } from 'ring-client-api/util';
|
|
5
8
|
const execFileAsync = promisify(execFile);
|
|
6
|
-
|
|
9
|
+
/** A probe that hangs must not hold every recording (they wait for it) forever. */
|
|
10
|
+
export const FFMPEG_PROBE_TIMEOUT_MS = 10_000;
|
|
11
|
+
let cachedCapabilities,
|
|
12
|
+
/** Set when the last check failed: when to ask FFmpeg again. */
|
|
13
|
+
retryAt, reportedFailure = false;
|
|
14
|
+
/** How long a failed check is kept, so a hanging FFmpeg doesn't hold every recording. */
|
|
15
|
+
export const FFMPEG_PROBE_RETRY_MS = 5 * 60_000;
|
|
16
|
+
/** The FFmpeg HKSV recordings run with: the plugin's ffmpegPath, or the bundled one. */
|
|
17
|
+
export function getRecordingFfmpegPath() {
|
|
18
|
+
return getFfmpegPath() || defaultFfmpegPath;
|
|
19
|
+
}
|
|
7
20
|
function parseNames(output) {
|
|
8
21
|
return new Set(output
|
|
9
22
|
.split(/\s+/)
|
|
10
23
|
.map((entry) => entry.trim())
|
|
11
24
|
.filter(Boolean));
|
|
12
25
|
}
|
|
13
|
-
async function getFfmpegOutput(args) {
|
|
14
|
-
const ffmpegPath = getFfmpegPath() || 'ffmpeg';
|
|
26
|
+
export async function getFfmpegOutput(args, ffmpegPath = getRecordingFfmpegPath(), timeout = FFMPEG_PROBE_TIMEOUT_MS) {
|
|
15
27
|
try {
|
|
16
28
|
const { stdout, stderr } = await execFileAsync(ffmpegPath, args, {
|
|
17
29
|
maxBuffer: 1024 * 1024,
|
|
30
|
+
timeout,
|
|
18
31
|
});
|
|
19
32
|
return `${stdout}\n${stderr}`;
|
|
20
33
|
}
|
|
@@ -22,14 +35,70 @@ async function getFfmpegOutput(args) {
|
|
|
22
35
|
return `${e.stdout ?? ''}\n${e.stderr ?? ''}`;
|
|
23
36
|
}
|
|
24
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* The names of the Linux video devices Homebridge can open (a Raspberry Pi 4's encoder is
|
|
40
|
+
* "bcm2835-codec-encode"). A device listed in sysfs but not usable, as in Docker without the
|
|
41
|
+
* device passed through or without access to the video group, is left out.
|
|
42
|
+
*/
|
|
43
|
+
export function getV4l2DeviceNames(sysfs = '/sys/class/video4linux', dev = '/dev') {
|
|
44
|
+
try {
|
|
45
|
+
return readdirSync(sysfs).flatMap((device) => {
|
|
46
|
+
try {
|
|
47
|
+
accessSync(join(dev, device), constants.R_OK | constants.W_OK);
|
|
48
|
+
return [readFileSync(join(sysfs, device, 'name'), 'utf8').trim()];
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A video encoder device: not a JPEG/image encoder (the Pi 4 has both) or an MPEG capture card's
|
|
61
|
+
* ("ivtv0 encoder MPG").
|
|
62
|
+
*/
|
|
63
|
+
export function isV4l2VideoEncoder(name) {
|
|
64
|
+
return /encode|-enc$/i.test(name) && !/image|jpeg|mpe?g/i.test(name);
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* FFmpeg builds list hardware encoders whether or not the hardware is there: the bundled FFmpeg
|
|
68
|
+
* and Ubuntu's list h264_v4l2m2m on any x86 machine, where encoding with it fails. So
|
|
69
|
+
* h264_v4l2m2m only counts on Linux with a usable V4L2 video encoder device (a Raspberry Pi 4 has
|
|
70
|
+
* one, a Pi 5 none), and h264_videotoolbox only on macOS.
|
|
71
|
+
*/
|
|
72
|
+
export function getUsableEncoders(listed, platform = process.platform, v4l2DeviceNames = platform === 'linux' ? getV4l2DeviceNames() : []) {
|
|
73
|
+
const usable = new Set(listed);
|
|
74
|
+
if (platform !== 'linux' || !v4l2DeviceNames.some(isV4l2VideoEncoder)) {
|
|
75
|
+
usable.delete('h264_v4l2m2m');
|
|
76
|
+
}
|
|
77
|
+
if (platform !== 'darwin') {
|
|
78
|
+
usable.delete('h264_videotoolbox');
|
|
79
|
+
}
|
|
80
|
+
return usable;
|
|
81
|
+
}
|
|
25
82
|
async function loadCapabilities() {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
83
|
+
const encodersOutput = await getFfmpegOutput(['-hide_banner', '-encoders']);
|
|
84
|
+
if (/Encoders:/.test(encodersOutput)) {
|
|
85
|
+
retryAt = undefined;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// FFmpeg didn't start or answer in time (a slow first start on a busy Pi): ask again a little
|
|
89
|
+
// later rather than record in software until a restart.
|
|
90
|
+
retryAt = Date.now() + FFMPEG_PROBE_RETRY_MS;
|
|
91
|
+
const message = `Could not list the encoders of FFmpeg (${getRecordingFfmpegPath()}); codec auto records with libx264 until it answers (asked again after ${FFMPEG_PROBE_RETRY_MS / 60_000} minutes)`;
|
|
92
|
+
if (reportedFailure) {
|
|
93
|
+
logDebug(message);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
reportedFailure = true;
|
|
97
|
+
logError(message);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
30
100
|
const capabilities = {
|
|
31
|
-
encoders: parseNames(encodersOutput),
|
|
32
|
-
hwaccels: parseNames(hwaccelsOutput),
|
|
101
|
+
encoders: getUsableEncoders(parseNames(encodersOutput)),
|
|
33
102
|
};
|
|
34
103
|
logDebug(`FFmpeg HKSV capabilities: encoders=${[
|
|
35
104
|
'h264_v4l2m2m',
|
|
@@ -41,6 +110,10 @@ async function loadCapabilities() {
|
|
|
41
110
|
return capabilities;
|
|
42
111
|
}
|
|
43
112
|
export function getFfmpegCapabilities() {
|
|
113
|
+
if (retryAt !== undefined && Date.now() >= retryAt) {
|
|
114
|
+
retryAt = undefined;
|
|
115
|
+
cachedCapabilities = undefined;
|
|
116
|
+
}
|
|
44
117
|
cachedCapabilities ??= loadCapabilities();
|
|
45
118
|
return cachedCapabilities;
|
|
46
119
|
}
|
package/lib/hksv-options.js
CHANGED
|
@@ -15,6 +15,11 @@ export function getHksvRecordingResolutions(config) {
|
|
|
15
15
|
[320, 240, 15],
|
|
16
16
|
];
|
|
17
17
|
}
|
|
18
|
+
/** Only auto outside lowPower asks FFmpeg which encoders it has. */
|
|
19
|
+
export function hksvCodecNeedsCapabilities(config) {
|
|
20
|
+
const media = normalizeMediaConfig(config);
|
|
21
|
+
return media.recording.codec === 'auto' && media.profile !== 'lowPower';
|
|
22
|
+
}
|
|
18
23
|
export function selectHksvVideoCodec(configuredCodec, capabilities, config) {
|
|
19
24
|
const media = normalizeMediaConfig(config);
|
|
20
25
|
const codec = configuredCodec ?? media.recording.codec;
|
package/lib/hksv-work-queue.js
CHANGED
|
@@ -20,18 +20,6 @@ export class ResourceGovernor {
|
|
|
20
20
|
this.queueTimeoutMs = normalizeTimeout(options.queueTimeoutMs);
|
|
21
21
|
this.drain();
|
|
22
22
|
}
|
|
23
|
-
/**
|
|
24
|
-
* Compatibility entry point for v14 callers. New platform code should call
|
|
25
|
-
* configure() once at startup instead.
|
|
26
|
-
*/
|
|
27
|
-
setConcurrency(concurrency) {
|
|
28
|
-
this.configure({ recordingConcurrency: concurrency });
|
|
29
|
-
}
|
|
30
|
-
/** Compatibility API returning only an idempotent release function. */
|
|
31
|
-
async acquire(signal) {
|
|
32
|
-
const lease = await this.acquireLease({ signal });
|
|
33
|
-
return () => lease.release();
|
|
34
|
-
}
|
|
35
23
|
acquireLease(request = {}) {
|
|
36
24
|
const signal = request.signal;
|
|
37
25
|
if (signal?.aborted) {
|
package/lib/intercom.js
CHANGED
|
@@ -2,9 +2,9 @@ import { RingCamera } from 'ring-client-api';
|
|
|
2
2
|
import { hap } from "./hap.js";
|
|
3
3
|
import { BaseDataAccessory } from "./base-data-accessory.js";
|
|
4
4
|
import { logError, logInfo } from 'ring-client-api/util';
|
|
5
|
-
import { filter, map, share,
|
|
6
|
-
import { interval, merge, Subject } from 'rxjs';
|
|
7
|
-
import { CameraSource } from "./camera-source.js";
|
|
5
|
+
import { exhaustMap, filter, map, share, throttleTime } from 'rxjs/operators';
|
|
6
|
+
import { EMPTY, interval, merge, Subject } from 'rxjs';
|
|
7
|
+
import { CameraSource, isHksvEnabled } from "./camera-source.js";
|
|
8
8
|
/**
|
|
9
9
|
* Creates a RingCamera-compatible proxy for a Ring Intercom Video device.
|
|
10
10
|
* Ring Intercom Video is classified as RingIntercom by ring-client-api but has a
|
|
@@ -86,20 +86,32 @@ function createIntercomVideoProxy(device) {
|
|
|
86
86
|
if (preWarmedCall)
|
|
87
87
|
return;
|
|
88
88
|
logInfo(`${device.name}: pre-warming camera connection`);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
89
|
+
const call = realStartLiveCall();
|
|
90
|
+
preWarmedCall = call;
|
|
91
|
+
// Handled here, so a call that fails before anything awaits it isn't an unhandled rejection
|
|
92
|
+
// (which stops Homebridge); a recording then starts its own call instead of this failed one.
|
|
93
|
+
call.catch((e) => {
|
|
94
|
+
if (preWarmedCall !== call)
|
|
95
|
+
return;
|
|
96
|
+
logError(`${device.name}: pre-warmed camera connection failed`);
|
|
97
|
+
logError(e);
|
|
98
|
+
preWarmedCall = null;
|
|
99
|
+
if (preWarmCleanup) {
|
|
100
|
+
clearTimeout(preWarmCleanup);
|
|
101
101
|
preWarmCleanup = null;
|
|
102
102
|
}
|
|
103
|
+
});
|
|
104
|
+
// Clean up if HKSV doesn't claim it within 20 seconds. Taken out of the slot first, so a
|
|
105
|
+
// recording arriving while it still connects starts its own call rather than get this one
|
|
106
|
+
// stopped under it, and a newer pre-warm isn't cleared by this one's timer.
|
|
107
|
+
preWarmCleanup = setTimeout(() => {
|
|
108
|
+
if (preWarmedCall !== call)
|
|
109
|
+
return;
|
|
110
|
+
preWarmedCall = null;
|
|
111
|
+
preWarmCleanup = null;
|
|
112
|
+
call
|
|
113
|
+
.then((session) => session.stop())
|
|
114
|
+
.catch(() => undefined); // a failed call has nothing to stop
|
|
103
115
|
}, 20000);
|
|
104
116
|
});
|
|
105
117
|
// Override getSnapshot: intercom uses the same doorbots snapshot endpoint as cameras.
|
|
@@ -124,6 +136,8 @@ function createIntercomVideoProxy(device) {
|
|
|
124
136
|
});
|
|
125
137
|
return proxy;
|
|
126
138
|
}
|
|
139
|
+
/** How often Intercom Video asks Ring for an active ding (its ding pushes don't arrive). */
|
|
140
|
+
const DING_POLL_INTERVAL_MS = 5000;
|
|
127
141
|
export class Intercom extends BaseDataAccessory {
|
|
128
142
|
unlocking = false;
|
|
129
143
|
unlockTimeout;
|
|
@@ -161,29 +175,32 @@ export class Intercom extends BaseDataAccessory {
|
|
|
161
175
|
// notifications (different category than RingIntercom.onDing expects),
|
|
162
176
|
// so onDing never fires. Poll Ring's active-dings endpoint as a reliable
|
|
163
177
|
// fallback — it reflects the current ring in real time (unlike the history
|
|
164
|
-
// endpoint which can lag by ~60 seconds).
|
|
178
|
+
// endpoint which can lag by ~60 seconds). Only Intercom Video needs it; a request
|
|
179
|
+
// still running when the next one is due skips that one rather than piling up.
|
|
165
180
|
onDingDetected = (() => {
|
|
166
181
|
let lastDingId;
|
|
167
|
-
const polled =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
182
|
+
const polled = !isIntercomVideo
|
|
183
|
+
? EMPTY
|
|
184
|
+
: interval(DING_POLL_INTERVAL_MS).pipe(exhaustMap(async () => {
|
|
185
|
+
try {
|
|
186
|
+
const active = await device.restClient.request({
|
|
187
|
+
url: 'https://api.ring.com/clients_api/dings/active',
|
|
188
|
+
});
|
|
189
|
+
return Array.isArray(active) ? active : [];
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return [];
|
|
193
|
+
}
|
|
194
|
+
}), filter((active) => {
|
|
195
|
+
const ding = active.find((d) => String(d.doorbot_id) === String(device.id) &&
|
|
196
|
+
d.kind === 'ding');
|
|
197
|
+
if (!ding)
|
|
198
|
+
return false;
|
|
199
|
+
if (String(ding.id) === lastDingId)
|
|
200
|
+
return false;
|
|
201
|
+
lastDingId = String(ding.id);
|
|
202
|
+
return true;
|
|
203
|
+
}), map(() => undefined));
|
|
187
204
|
// share() makes this hot: one polling interval, one lastDingId,
|
|
188
205
|
// emission fans out to all subscribers (doorbell service + auto-open)
|
|
189
206
|
return merge(device.onDing, polled).pipe(share());
|
|
@@ -279,7 +296,11 @@ export class Intercom extends BaseDataAccessory {
|
|
|
279
296
|
});
|
|
280
297
|
// Pre-warm the camera WebRTC connection on every ding so HKSV recording
|
|
281
298
|
// gets live video instead of black frames.
|
|
282
|
-
|
|
299
|
+
// Only with HKSV: recordings are what it is for (a live view opened within 20 s would also
|
|
300
|
+
// use it), and without HKSV it would open a Ring live call on every ding that usually nothing
|
|
301
|
+
// claims.
|
|
302
|
+
if (intercomVideoProxy &&
|
|
303
|
+
isHksvEnabled(config, intercomVideoProxy.operatingOnBattery)) {
|
|
283
304
|
const proxy = intercomVideoProxy;
|
|
284
305
|
onDingDetected.subscribe(() => {
|
|
285
306
|
;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { reservePorts, RtpSplitter } from '@homebridge/camera-utils';
|
|
2
|
+
import { getRecordingFfmpegPath } from "./ffmpeg-capabilities.js";
|
|
3
3
|
import { logDebug, logError, logInfo } from 'ring-client-api/util';
|
|
4
4
|
import { Subscription } from 'rxjs';
|
|
5
5
|
import { take } from 'rxjs/operators';
|
|
@@ -125,7 +125,6 @@ export class RingMediaIngress {
|
|
|
125
125
|
return {
|
|
126
126
|
session,
|
|
127
127
|
subscribeVideo: (handler) => session.onVideoRtp.subscribe((packet) => handler(clonePacket(packet))),
|
|
128
|
-
subscribeAudio: (handler) => session.onAudioRtp.subscribe((packet) => handler(clonePacket(packet))),
|
|
129
128
|
createTranscoder: (options) => this.createTranscoder(session, options),
|
|
130
129
|
release: () => {
|
|
131
130
|
if (released)
|
|
@@ -274,7 +273,7 @@ export class RingMediaIngress {
|
|
|
274
273
|
}
|
|
275
274
|
releaseFfmpeg = hksvRecordingQueue.trackFfmpegProcess();
|
|
276
275
|
ffmpeg = new ManagedFfmpegProcess({
|
|
277
|
-
ffmpegPath:
|
|
276
|
+
ffmpegPath: getRecordingFfmpegPath(),
|
|
278
277
|
ffmpegArgs: [
|
|
279
278
|
'-hide_banner', '-protocol_whitelist', 'pipe,udp,rtp,file,crypto',
|
|
280
279
|
...(usingOpus ? ['-acodec', 'libopus'] : []),
|
package/lib/ring-platform.js
CHANGED
|
@@ -19,6 +19,7 @@ import { Fan } from "./fan.js";
|
|
|
19
19
|
import { Outlet } from "./outlet.js";
|
|
20
20
|
import { Switch } from "./switch.js";
|
|
21
21
|
import { Camera } from "./camera.js";
|
|
22
|
+
import { shutDownCameraSources } from "./camera-source.js";
|
|
22
23
|
import { PushHealthMonitor, readPushRecoveryState, writePushRecoveryState, } from "./push-health.js";
|
|
23
24
|
import { readFileSync } from 'fs';
|
|
24
25
|
import { getSessionRefreshSensorUuid, SessionRefreshSensor, } from "./session-refresh-sensor.js";
|
|
@@ -36,6 +37,16 @@ import { Valve } from "./valve.js";
|
|
|
36
37
|
import { getRemovedSettings, sanitizeMediaConfig } from "./media-config.js";
|
|
37
38
|
import { configureHksvResourceGovernor } from "./hksv-work-queue.js";
|
|
38
39
|
import { getMotionNotificationSensorUuid, setUpMotionNotificationSensor, } from "./motion-notification-sensor.js";
|
|
40
|
+
import { config as rxjsConfig } from 'rxjs';
|
|
41
|
+
// rxjs rethrows an error from a subscriber that has no error handler on a timer, where nothing
|
|
42
|
+
// can catch it, and that stops Homebridge and every accessory on it. Log it instead. If the error
|
|
43
|
+
// ended a stream (an operator such as map threw), that stream stays ended, e.g. a live view's
|
|
44
|
+
// audio; a subscriber that throws keeps receiving values.
|
|
45
|
+
// On the main bridge plugins can share one rxjs, so a handler another plugin set is kept.
|
|
46
|
+
rxjsConfig.onUnhandledError ??= (e) => {
|
|
47
|
+
logError('Unhandled error (logged instead of stopping Homebridge):');
|
|
48
|
+
logError(e);
|
|
49
|
+
};
|
|
39
50
|
const ignoreHiddenDeviceTypes = [
|
|
40
51
|
RingDeviceType.RingNetAdapter,
|
|
41
52
|
RingDeviceType.ZigbeeAdapter,
|
|
@@ -137,23 +148,28 @@ function removePushCredentialsFromRefreshToken(refreshToken, removeHardwareId =
|
|
|
137
148
|
}
|
|
138
149
|
}
|
|
139
150
|
function updateRefreshTokenInConfig(configContents, oldRefreshToken, newRefreshToken, clearRefreshFlags) {
|
|
151
|
+
let homebridgeConfig;
|
|
140
152
|
try {
|
|
141
|
-
|
|
142
|
-
platform.platform === `${pluginName}.${platformName}`) &&
|
|
143
|
-
platform.refreshToken === oldRefreshToken);
|
|
144
|
-
if (!platformConfig) {
|
|
145
|
-
throw new Error('Ring platform config not found');
|
|
146
|
-
}
|
|
147
|
-
platformConfig.refreshToken = newRefreshToken;
|
|
148
|
-
if (clearRefreshFlags) {
|
|
149
|
-
platformConfig.forceRefreshRingPushCredentials = false;
|
|
150
|
-
platformConfig.forceRefreshRingApiSession = false;
|
|
151
|
-
}
|
|
152
|
-
return `${JSON.stringify(homebridgeConfig, null, 4)}\n`;
|
|
153
|
+
homebridgeConfig = JSON.parse(configContents);
|
|
153
154
|
}
|
|
154
155
|
catch {
|
|
156
|
+
// Not JSON Homebridge could read either; replace the token where it is.
|
|
155
157
|
return configContents.replace(oldRefreshToken, newRefreshToken);
|
|
156
158
|
}
|
|
159
|
+
const platformConfig = homebridgeConfig.platforms?.find((platform) => (platform.platform === platformName ||
|
|
160
|
+
platform.platform === `${pluginName}.${platformName}`) &&
|
|
161
|
+
platform.refreshToken === oldRefreshToken);
|
|
162
|
+
if (!platformConfig) {
|
|
163
|
+
// The token was changed in the config meanwhile, or is only in another plugin's block (a
|
|
164
|
+
// homebridge-ring set up with the same token): leave the file as it is.
|
|
165
|
+
return configContents;
|
|
166
|
+
}
|
|
167
|
+
platformConfig.refreshToken = newRefreshToken;
|
|
168
|
+
if (clearRefreshFlags) {
|
|
169
|
+
platformConfig.forceRefreshRingPushCredentials = false;
|
|
170
|
+
platformConfig.forceRefreshRingApiSession = false;
|
|
171
|
+
}
|
|
172
|
+
return `${JSON.stringify(homebridgeConfig, null, 4)}\n`;
|
|
157
173
|
}
|
|
158
174
|
/**
|
|
159
175
|
* This platform's block as saved in config.json now, if exactly one block matches and it has a
|
|
@@ -338,6 +354,13 @@ export class RingPlatform {
|
|
|
338
354
|
this.shuttingDown = true;
|
|
339
355
|
clearTimeout(this.retryTimer);
|
|
340
356
|
this.sessionRefreshSensor?.stop();
|
|
357
|
+
shutDownCameraSources();
|
|
358
|
+
try {
|
|
359
|
+
this.ringApi?.disconnect();
|
|
360
|
+
}
|
|
361
|
+
catch (e) {
|
|
362
|
+
logError(e);
|
|
363
|
+
}
|
|
341
364
|
});
|
|
342
365
|
if (config.automaticSessionRefresh !== false) {
|
|
343
366
|
this.pushHealth = new PushHealthMonitor(api.user.storagePath(), () => this.refreshSessionAutomatically());
|
|
@@ -364,6 +387,7 @@ export class RingPlatform {
|
|
|
364
387
|
retryTimer;
|
|
365
388
|
shuttingDown = false;
|
|
366
389
|
devicesSetUp = false;
|
|
390
|
+
ringApi;
|
|
367
391
|
pushHealth;
|
|
368
392
|
gotNewToken = false;
|
|
369
393
|
sessionRefreshSensor;
|
|
@@ -511,6 +535,7 @@ export class RingPlatform {
|
|
|
511
535
|
return;
|
|
512
536
|
}
|
|
513
537
|
this.devicesSetUp = true;
|
|
538
|
+
this.ringApi = ringApi;
|
|
514
539
|
const cachedAccessoryIds = Object.keys(this.homebridgeAccessories), platformAccessories = [], externalAccessories = [], activeAccessoryIds = [];
|
|
515
540
|
logInfo('Found the following locations:');
|
|
516
541
|
locations.forEach((location) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-ring-secure-video",
|
|
3
3
|
"displayName": "Ring Secure Video",
|
|
4
|
-
"version": "1.0.0
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"description": "Homebridge plugin for Ring devices with HomeKit Secure Video recording, based on homebridge-ring and homebridge-ring-hksv",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -58,7 +58,6 @@
|
|
|
58
58
|
},
|
|
59
59
|
"homepage": "https://github.com/zmanuel1234/homebridge-ring-secure-video#readme",
|
|
60
60
|
"files": [
|
|
61
|
-
"branding",
|
|
62
61
|
"CHANGELOG.md",
|
|
63
62
|
"config.schema.json",
|
|
64
63
|
"lib",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1800" height="630" viewBox="0 0 1800 630"><defs><style>.cls-1{isolation:isolate;}.cls-2{fill:#535556;}.cls-3{fill:#1c9ad6;}.cls-4{fill:#9596ae;}.cls-5{fill:url(#linear-gradient);}.cls-6{fill:url(#radial-gradient);}.cls-7{opacity:0.25;mix-blend-mode:multiply;}.cls-8{fill:#fff;}.cls-9{fill:none;stroke:#9596ae;stroke-linecap:round;stroke-miterlimit:10;stroke-width:5px;}</style><linearGradient id="linear-gradient" x1="450" y1="86.48" x2="450" y2="386.48" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#57277c"/><stop offset="1" stop-color="#491f59"/></linearGradient><radialGradient id="radial-gradient" cx="450" cy="236.48" r="150" gradientUnits="userSpaceOnUse"><stop offset="0" stop-opacity="0"/><stop offset="1" stop-opacity="0.1"/></radialGradient></defs><g class="cls-1"><g id="Plugin_Logo" data-name="Plugin Logo"><path class="cls-2" d="M1210.33,243.93a9.27,9.27,0,0,1-2.9,6.77,9.61,9.61,0,0,1-7.08,2.82,10,10,0,0,1-6.77-2.9c-7.1-7.1-18.76-9.94-26.38-9.91q-17.39,0-27.2,11.9t-9.83,33.79V391.09a8.7,8.7,0,0,1-3.05,7.25,10.06,10.06,0,0,1-13.18,0,8.62,8.62,0,0,1-3-7.25V286.4a86.7,86.7,0,0,1,3.54-25.25,60.49,60.49,0,0,1,10.62-20.58,50.73,50.73,0,0,1,17.53-14.09,55.09,55.09,0,0,1,24.59-5.14,61.34,61.34,0,0,1,21.25,4.18,51.37,51.37,0,0,1,18.66,11.58,9.58,9.58,0,0,1,3.25,6.84Z"/><path class="cls-2" d="M1258.57,391.08a8.68,8.68,0,0,1-3,7.26,10.22,10.22,0,0,1-13.21,0,8.68,8.68,0,0,1-3-7.26V234.27a8.67,8.67,0,0,1,3-7.25,10.24,10.24,0,0,1,13.21,0,8.69,8.69,0,0,1,3,7.25Z"/><path class="cls-2" d="M1426,391.08a8.72,8.72,0,0,1-3.07,7.26,10.24,10.24,0,0,1-13.19,0,8.64,8.64,0,0,1-3-7.26V284.16a42.28,42.28,0,0,0-3.4-16.89,43.47,43.47,0,0,0-80.11,0,42.25,42.25,0,0,0-3.37,16.89V391.08a8.68,8.68,0,0,1-3,7.26,10.22,10.22,0,0,1-13.21,0,8.68,8.68,0,0,1-3-7.26V284.16a61.22,61.22,0,0,1,4.92-24.47,63.29,63.29,0,0,1,13.35-20,64,64,0,0,1,19.68-13.35,62.85,62.85,0,0,1,49.18,0,61.7,61.7,0,0,1,33.29,33.29,61,61,0,0,1,4.92,24.47Z"/><path class="cls-2" d="M1590,402.34a60.9,60.9,0,0,1-4.91,24.59,62.18,62.18,0,0,1-33.37,33.15,61.22,61.22,0,0,1-24.47,4.92,73.94,73.94,0,0,1-14.16-1.45,81.45,81.45,0,0,1-14.76-4.35,64.35,64.35,0,0,1-13.5-7.38,40.48,40.48,0,0,1-10.13-10.3,9.26,9.26,0,0,1-1.6-5.49,8.83,8.83,0,0,1,3-6.91,10,10,0,0,1,6.91-2.75,11,11,0,0,1,4,.81,7.79,7.79,0,0,1,3.71,3.37c3.83,5.76,12.08,10.32,17.54,12.07a61,61,0,0,0,18.81,3.08,42.45,42.45,0,0,0,16.89-3.4,43.67,43.67,0,0,0,23.16-23.16,42.24,42.24,0,0,0,3.52-16.8V380.21a62.5,62.5,0,0,1-19.67,12.71,61.72,61.72,0,0,1-23.8,4.68,61,61,0,0,1-24.6-4.92,62.63,62.63,0,0,1-33.14-33.15,60.57,60.57,0,0,1-4.92-24.59V284.16a61,61,0,0,1,4.92-24.47,62.9,62.9,0,0,1,13.35-20,63.91,63.91,0,0,1,19.67-13.35,62.85,62.85,0,0,1,49.18,0,62.51,62.51,0,0,1,38.22,57.76v118.2Zm-19.3-118.18a42.27,42.27,0,0,0-3.39-16.89,43.58,43.58,0,0,0-80.22,0,42.68,42.68,0,0,0-3.37,16.9v50.53a42.7,42.7,0,0,0,3.37,17,43.72,43.72,0,0,0,9.35,13.84,43.19,43.19,0,0,0,13.84,9.32,43.47,43.47,0,0,0,57-23.16,42.38,42.38,0,0,0,3.47-17Z"/><circle class="cls-3" cx="1248.94" cy="181.5" r="16.5"/></g><g id="Homebridge"><g id="Text"><path class="cls-4" d="M136.23,471.8h29V440.67H178v75.9H165.27V482.8h-29v33.77H123.47v-75.9h12.76Z"/><path class="cls-4" d="M222.8,517.56c-16.94,0-28.38-10.23-28.38-34.1v-9.68c0-23.87,11.44-34.1,28.38-34.1s28.49,10.23,28.49,34.1v9.68C251.29,507.33,239.85,517.56,222.8,517.56Zm-15.62-34.1c0,16.39,5.5,23,15.62,23s15.73-6.6,15.73-23v-9.68c0-16.39-5.61-23-15.73-23s-15.62,6.6-15.62,23Z"/><path class="cls-4" d="M301.67,484.67l21.89-44h12.21v75.9H323.45V465.75l-17.38,36.19h-9L280,466.08v50.49H267.68v-75.9h12.1Z"/><path class="cls-4" d="M401.77,440.67v11.11H368v20h30.47v11H368v22.77h33.77v11H355.24v-75.9Z"/><path class="cls-4" d="M441,440.67c17.82,0,26.95,6.6,26.95,19.69a17.12,17.12,0,0,1-11.11,16.5c8.58,2.42,13.53,8.36,13.53,17.93,0,11.44-7.26,21.78-27,21.78H416.62v-75.9Zm-11.66,31h12.43c9.46,0,13.42-4.29,13.42-10.56,0-5.94-3-9.46-15.07-9.46H429.38Zm0,33.88h13.31c10.23,0,15-4.18,15-11.44,0-8-5.28-11.44-16.5-11.44H429.38Z"/><path class="cls-4" d="M527.72,516.57l-14.63-30.8H498.35v30.8H485.59v-75.9h25.85c17.49,0,27.94,7.59,27.94,22.66,0,10.45-5,17.16-13.75,20.35l16.17,32.89Zm-17.16-41.8c10.78,0,16.06-2.86,16.06-11.44s-5.28-11.55-16.06-11.55H498.35v23Z"/><path class="cls-4" d="M569.4,516.57H556.64v-75.9H569.4Z"/><path class="cls-4" d="M589.86,516.57v-75.9h25.63c17,0,28.49,9.13,28.49,33.44v9c0,24.31-11.44,33.44-28.49,33.44Zm41.36-33.44v-9c0-15.62-4.84-22.33-15.73-22.33H602.62v53.79h12.87C626.38,505.57,631.22,498.75,631.22,483.13Z"/><path class="cls-4" d="M713.61,477.63v5.83c0,23.87-10.89,34.1-27.94,34.1s-28.38-10.23-28.38-34.1v-9.13c0-24.42,11.44-34.65,28.38-34.65,15.4,0,25.19,8.58,27.61,24.53H701c-1.87-8.8-6.71-13.42-15.29-13.42-10.12,0-15.62,6.6-15.62,23v9.68c0,16.39,5.5,23,15.62,23,9.68,0,14.52-5.72,15.4-17.71H686V477.63Z"/><path class="cls-4" d="M776.53,440.67v11.11H742.76v20h30.47v11H742.76v22.77h33.77v11H730v-75.9Z"/></g><g id="Logo"><circle class="cls-5" cx="450" cy="236.48" r="150"/><circle class="cls-6" cx="450" cy="236.48" r="150"/><image class="cls-7" width="255" height="258" transform="translate(324 110.23)" xlink:href="data:image/png;base64,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"/><path class="cls-8" d="M551.72,221.24h-.16l-18.34-18.33a3.58,3.58,0,0,1-1-2.5v-42.3A16.24,16.24,0,0,0,516,141.89H504.51a16.25,16.25,0,0,0-16.24,16.22v.24l-27.06-26.21a16.32,16.32,0,0,0-22.78.19L346.76,224a16.21,16.21,0,0,0,0,22.93l0,0,8.73,8.68a16.25,16.25,0,0,0,22.86.09l70.46-69.42a3.54,3.54,0,0,1,5,0l69,69a3.56,3.56,0,0,1,0,5l-8.48,8.48a3.56,3.56,0,0,1-5,0l-48.47-47a16.28,16.28,0,0,0-22.82.2l-46.51,46.75a16.26,16.26,0,0,0,0,22.92l8.61,8.61a16.27,16.27,0,0,0,22.89.06l25.07-24.78a3.57,3.57,0,0,1,5,0l24.94,24.51a3.51,3.51,0,0,1,0,5L453.38,329.9c-.11,0-.2-.07-.32-.07a9.26,9.26,0,1,0,9.26,9.26v-.16l24.81-24.8a16.24,16.24,0,0,0,0-23l-.1-.09-24.93-24.5a16.29,16.29,0,0,0-22.79,0l-25.15,24.81a3.54,3.54,0,0,1-5,0l-8.6-8.62a3.56,3.56,0,0,1,0-5L447.11,231a3.55,3.55,0,0,1,5-.05l48.46,47.09a16.3,16.3,0,0,0,22.78-.17l8.49-8.49a16.24,16.24,0,0,0,0-22.95l-69.06-69.06a16.26,16.26,0,0,0-22.87-.1l-70.46,69.4a3.57,3.57,0,0,1-5,0L355.76,238a3.54,3.54,0,0,1,0-5l91.67-91.67a3.56,3.56,0,0,1,5,0l48.59,47V158.11a3.55,3.55,0,0,1,3.54-3.54H516a3.57,3.57,0,0,1,3.55,3.54v42.36A16.37,16.37,0,0,0,524.31,212l18.24,18.24c0,.12-.07.21-.07.32a9.26,9.26,0,1,0,9.25-9.27Z"/></g></g><g id="Divider"><line class="cls-9" x1="998" y1="14.23" x2="902" y2="614.23"/></g></g></svg>
|
package/branding/icon.png
DELETED
|
Binary file
|
package/branding/lighting.png
DELETED
|
Binary file
|
package/branding/product.png
DELETED
|
Binary file
|
|
Binary file
|
package/lib/util.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { createHash } from 'crypto';
|
|
2
|
-
export function generateMacAddress(uuid) {
|
|
3
|
-
const sha1sum = createHash('sha1');
|
|
4
|
-
sha1sum.update(uuid);
|
|
5
|
-
const s = sha1sum.digest('hex');
|
|
6
|
-
let i = 0;
|
|
7
|
-
return 'xx:xx:xx:xx:xx:xx'.replace(/[x]/g, () => s[i++]).toUpperCase();
|
|
8
|
-
}
|