homebridge-ring 11.0.6 → 11.1.0-beta.1

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
@@ -2,6 +2,34 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
+ ## [11.1.0-beta.1](https://github.com/dgreif/ring/compare/v11.1.0-beta.0...v11.1.0-beta.1) (2022-08-01)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **homebridge:** add unbridgeCameras option to ui schema ([638a0b3](https://github.com/dgreif/ring/commit/638a0b373fac72b0058851836ff6d2bdd237e47c))
11
+
12
+ ## [11.1.0-beta.0](https://github.com/dgreif/ring/compare/v11.0.7...v11.1.0-beta.0) (2022-08-01)
13
+
14
+
15
+ ### Features
16
+
17
+ * **homebridge:** allow cameras to be unbridged ([7302298](https://github.com/dgreif/ring/commit/73022983eed59e6af4bfdf0d8f1ee9d8ee8f6953))
18
+ * **homebridge:** use opus transcoding for unbridge cameras ([e9eb7a4](https://github.com/dgreif/ring/commit/e9eb7a4940ce7d01dee920593af3bdd12e25fa1a))
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * update dependencies ([6f98cdf](https://github.com/dgreif/ring/commit/6f98cdf48895daf3217890b6631a6dbb2ccd340f))
24
+
25
+ ### [11.0.7](https://github.com/dgreif/ring/compare/v11.0.6...v11.0.7) (2022-07-10)
26
+
27
+
28
+ ### Bug Fixes
29
+
30
+ * hide errors from snapshot request timeouts ([f997e34](https://github.com/dgreif/ring/commit/f997e340ad3c579d059ae07c11ad82ddc5ceb181)), closes [#932](https://github.com/dgreif/ring/issues/932)
31
+ * update dependencies ([ce0bf2a](https://github.com/dgreif/ring/commit/ce0bf2aa81d55e629931f38ba2c6ca16a79fa0d2))
32
+
5
33
  ### [11.0.6](https://github.com/dgreif/ring/compare/v11.0.5...v11.0.6) (2022-06-29)
6
34
 
7
35
 
package/README.md CHANGED
@@ -72,6 +72,7 @@ Only include an optional parameter if you actually need it. Default behavior wi
72
72
 
73
73
  | Option | Default | Explanation |
74
74
  | ---------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
75
+ | `unbridgeCameras` | `false` | if `true`, all Ring Cameras we be treated as external accessories, which generally leads to better performance. This means they each need to be individually added to HomeKit. WARNING: If your cameras are already bridged, they will be deleted from HomeKit when you enable this option, and you will need to reconfigure any associated automations or HomeKit settings |
75
76
  | `alarmOnEntryDelay` | `false` | if `true`, HomeKit will register a delayed entry event as a triggered alarm. There are some households where this is a nice feature as a heads up if you have other people who enter your house and you want an alert so that you can disable the alarm for them before it actually goes off. This works well if you automatically arm/disarm on leave/arrive (see setup instructions below) |
76
77
  | `beamDurationSeconds` | `60` for light groups, previous from Ring app for individual lights | Ring smart lighting has a default behavior of only staying on for 60 seconds when you turn on a light via the Ring app. To force a duration when the light is turned on from HomeKit, set this option to a specific number of seconds. If this option is not set, the lights will use the duration from the previous time the light was turned on in the Ring app. For light groups, this will default to 60 seconds. The maximum value is `32767`, which is ~9.1 hours. |
77
78
  | `hideDeviceIds` | `[]` | Allows you to hide specific devices by an array of ids. The id for each device is logged when homebridge starts. |
@@ -16,6 +16,11 @@
16
16
  "description": "Click \"Generate New Refresh Token\" above to change accounts or if your refresh token has expired",
17
17
  "placeholder": "Refresh Token"
18
18
  },
19
+ "unbridgeCameras": {
20
+ "title": "Unbridge Cameras",
21
+ "type": "boolean",
22
+ "description": "If enabled, all Ring Cameras we be treated as external accessories, which generally leads to better performance. This means they each need to be individually added to HomeKit. WARNING: If your cameras are already bridged, they will be deleted from HomeKit when you enable this option, and you will need to reconfigure any associated automations or HomeKit settings"
23
+ },
19
24
  "alarmOnEntryDelay": {
20
25
  "title": "Alarm on Entry Delay",
21
26
  "type": "boolean",
@@ -74,15 +79,11 @@
74
79
  "oneOf": [
75
80
  {
76
81
  "title": "Home",
77
- "enum": [
78
- "some"
79
- ]
82
+ "enum": ["some"]
80
83
  },
81
84
  {
82
85
  "title": "Away",
83
- "enum": [
84
- "all"
85
- ]
86
+ "enum": ["all"]
86
87
  }
87
88
  ]
88
89
  },
@@ -156,6 +157,7 @@
156
157
  "title": "Optional Configuration",
157
158
  "expandable": true,
158
159
  "items": [
160
+ "unbridgeCameras",
159
161
  "alarmOnEntryDelay",
160
162
  "hideLightGroups",
161
163
  "hideDoorbellSwitch",
@@ -282,7 +282,7 @@ class RingRestClient {
282
282
  if (response.statusCode) {
283
283
  (0, util_1.logError)(`Request to ${url} failed with status ${response.statusCode}. Response body: ${(0, util_1.stringify)(response.body)}`);
284
284
  }
285
- else {
285
+ else if (!options.allowNoResponse) {
286
286
  (0, util_1.logError)(`Request to ${url} failed:`);
287
287
  (0, util_1.logError)(e);
288
288
  }
@@ -356,6 +356,7 @@ class RingCamera extends subscribed_1.Subscribed {
356
356
  headers: {
357
357
  accept: 'image/jpeg',
358
358
  },
359
+ allowNoResponse: true,
359
360
  }), { responseTimestamp, timeMillis } = response, timestampAge = Math.abs(responseTimestamp - timeMillis);
360
361
  this.lastSnapshotTimestamp = timeMillis;
361
362
  this.lastSnapshotTimestampLocal = Date.now() - timestampAge;
@@ -2,10 +2,11 @@ import { RingCamera } from '../api';
2
2
  import { CameraStreamingDelegate, PrepareStreamCallback, PrepareStreamRequest, SnapshotRequest, SnapshotRequestCallback, StreamingRequest, StreamRequestCallback } from 'homebridge';
3
3
  export declare class CameraSource implements CameraStreamingDelegate {
4
4
  private ringCamera;
5
+ private useOpus;
5
6
  controller: import("homebridge").CameraController;
6
7
  private sessions;
7
8
  private cachedSnapshot?;
8
- constructor(ringCamera: RingCamera);
9
+ constructor(ringCamera: RingCamera, useOpus?: boolean);
9
10
  private previousLoadSnapshotPromise?;
10
11
  loadSnapshot(imageUuid?: string): Promise<any>;
11
12
  fn: number;
@@ -10,6 +10,7 @@ const fs_1 = require("fs");
10
10
  const util_2 = require("util");
11
11
  const ffmpeg_1 = require("../api/ffmpeg");
12
12
  const werift_1 = require("werift");
13
+ const opus_repacketizer_1 = require("./opus-repacketizer");
13
14
  const readFileAsync = (0, util_2.promisify)(fs_1.readFile), cameraOfflinePath = require.resolve('../../media/camera-offline.jpg'), snapshotsBlockedPath = require.resolve('../../media/snapshots-blocked.jpg');
14
15
  function getDurationSeconds(start) {
15
16
  return (Date.now() - start) / 1000;
@@ -37,6 +38,7 @@ class StreamingSessionWrapper {
37
38
  this.videoSrtp = (0, camera_utils_1.generateSrtpOptions)();
38
39
  this.audioSplitter = new camera_utils_1.RtpSplitter();
39
40
  this.videoSplitter = new camera_utils_1.RtpSplitter();
41
+ this.repacketizeAudioSplitter = new camera_utils_1.RtpSplitter();
40
42
  this.libfdkAacInstalledPromise = (0, camera_utils_1.doesFfmpegSupportCodec)('libfdk_aac', (0, ffmpeg_1.getFfmpegPath)())
41
43
  .then((supported) => {
42
44
  if (!supported) {
@@ -89,9 +91,57 @@ class StreamingSessionWrapper {
89
91
  .catch(util_1.logError);
90
92
  }));
91
93
  }
94
+ listenForAudioPackets(startStreamRequest) {
95
+ const { targetAddress, audio: { port: audioPort }, } = this.prepareStreamRequest, { audio: { codec: audioCodec, sample_rate: audioSampleRate }, } = startStreamRequest,
96
+ // Repacketize the audio stream after it's been transcoded
97
+ opusRepacketizer = new opus_repacketizer_1.OpusRepacketizer(1), audioIntervalScale = audioSampleRate / 8, audioSrtpSession = new werift_1.SrtpSession(getSessionConfig(this.audioSrtp));
98
+ let firstTimestamp, audioPacketCount = 0;
99
+ this.repacketizeAudioSplitter.addMessageHandler(({ message }) => {
100
+ let rtp = werift_1.RtpPacket.deSerialize(message);
101
+ if (audioCodec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
102
+ // borrowed from scrypted
103
+ // Original source: https://github.com/koush/scrypted/blob/c13ba09889c3e0d9d3724cb7d49253c9d787fb97/plugins/homekit/src/types/camera/camera-streaming-srtp-sender.ts#L124-L143
104
+ rtp = opusRepacketizer.repacketize(werift_1.RtpPacket.deSerialize(message));
105
+ if (!rtp) {
106
+ return null;
107
+ }
108
+ if (!firstTimestamp) {
109
+ firstTimestamp = rtp.header.timestamp;
110
+ }
111
+ // from HAP spec:
112
+ // RTP Payload Format for Opus Speech and Audio Codec RFC 7587 with an exception
113
+ // that Opus audio RTP Timestamp shall be based on RFC 3550.
114
+ // RFC 3550 indicates that PCM audio based with a sample rate of 8k and a packet
115
+ // time of 20ms would have a monotonic interval of 8k / (1000 / 20) = 160.
116
+ // So 24k audio would have a monotonic interval of (24k / 8k) * 160 = 480.
117
+ // HAP spec also states that it may request packet times of 20, 30, 40, or 60.
118
+ // In practice, HAP has been seen to request 20 on LAN and 60 over LTE.
119
+ // So the RTP timestamp must scale accordingly.
120
+ // Further investigation indicates that HAP doesn't care about the actual sample rate at all,
121
+ // that's merely a suggestion. When encoding Opus, it can seemingly be an arbitrary sample rate,
122
+ // audio will work so long as the rtp timestamps are created properly: which is a construct of the sample rate
123
+ // HAP requests, and the packet time is respected,
124
+ // opus 48khz will work just fine.
125
+ rtp.header.timestamp =
126
+ (firstTimestamp + audioPacketCount * 180 * audioIntervalScale) %
127
+ 0xffffffff;
128
+ audioPacketCount++;
129
+ }
130
+ // encrypt the packet
131
+ const encryptedPacket = audioSrtpSession.encrypt(rtp.payload, rtp.header);
132
+ // send the encrypted packet to HomeKit
133
+ this.audioSplitter
134
+ .send(encryptedPacket, {
135
+ port: audioPort,
136
+ address: targetAddress,
137
+ })
138
+ .catch(util_1.logError);
139
+ return null;
140
+ });
141
+ }
92
142
  async activate(request) {
93
143
  let sentVideo = false;
94
- const { targetAddress, audio: { port: audioPort, srtp_key: remoteAudioSrtpKey, srtp_salt: remoteAudioSrtpSalt, }, video: { port: videoPort }, } = this.prepareStreamRequest,
144
+ const { targetAddress, audio: { srtp_key: remoteAudioSrtpKey, srtp_salt: remoteAudioSrtpSalt }, video: { port: videoPort }, } = this.prepareStreamRequest,
95
145
  // use to encrypt Ring video to HomeKit
96
146
  videoSrtpSession = new werift_1.SrtpSession(getSessionConfig(this.videoSrtp));
97
147
  // Set up packet forwarding for video stream
@@ -119,20 +169,23 @@ class StreamingSessionWrapper {
119
169
  audio: [
120
170
  '-map',
121
171
  '0:a',
122
- // OPUS specific - it works, but audio is very choppy
123
- // '-acodec',
124
- // 'libopus',
125
- // '-vbr',
126
- // 'on',
127
- // '-frame_duration',
128
- // 20,
129
- // '-application',
130
- // 'lowdelay',
131
- // AAC-eld specific
132
- '-acodec',
133
- 'libfdk_aac',
134
- '-profile:a',
135
- 'aac_eld',
172
+ ...(request.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */
173
+ ? [
174
+ // OPUS specific - it works, but audio is very choppy
175
+ '-acodec',
176
+ 'libopus',
177
+ '-frame_duration',
178
+ request.audio.packet_time,
179
+ '-application',
180
+ 'lowdelay',
181
+ ]
182
+ : [
183
+ // AAC-eld specific
184
+ '-acodec',
185
+ 'libfdk_aac',
186
+ '-profile:a',
187
+ 'aac_eld',
188
+ ]),
136
189
  // Shared options
137
190
  '-flags',
138
191
  '+global_header',
@@ -150,11 +203,8 @@ class StreamingSessionWrapper {
150
203
  this.audioSsrc,
151
204
  '-f',
152
205
  'rtp',
153
- '-srtp_out_suite',
154
- 'AES_CM_128_HMAC_SHA1_80',
155
- '-srtp_out_params',
156
- (0, camera_utils_1.encodeSrtpOptions)(this.audioSrtp),
157
- `srtp://${targetAddress}:${audioPort}?pkt_size=188`,
206
+ `rtp://127.0.0.1:${await this.repacketizeAudioSplitter
207
+ .portPromise}?pkt_size=188`,
158
208
  ],
159
209
  video: false,
160
210
  output: [],
@@ -180,15 +230,16 @@ class StreamingSessionWrapper {
180
230
  // deSerialize will sometimes fail, but the errors can be ignored
181
231
  }
182
232
  return null;
183
- }), usingOpus = await this.streamingSession.isUsingOpus, returnAudioTranscoder = new camera_utils_1.ReturnAudioTranscoder({
233
+ }), isRingUsingOpus = await this.streamingSession.isUsingOpus, returnAudioTranscoder = new camera_utils_1.ReturnAudioTranscoder({
184
234
  prepareStreamRequest: this.prepareStreamRequest,
235
+ startStreamRequest: request,
185
236
  incomingAudioOptions: {
186
237
  ssrc: this.audioSsrc,
187
238
  rtcpPort: 0, // we don't care about rtcp for incoming audio
188
239
  },
189
240
  outputArgs: [
190
241
  '-acodec',
191
- ...(usingOpus
242
+ ...(isRingUsingOpus
192
243
  ? ['libopus', '-ac', 2, '-ar', '48k']
193
244
  : ['pcm_mulaw', '-ac', 1, '-ar', '8k']),
194
245
  '-flags',
@@ -209,18 +260,21 @@ class StreamingSessionWrapper {
209
260
  returnAudioTranscoder.stop();
210
261
  returnAudioTranscodedSplitter.close();
211
262
  });
263
+ this.listenForAudioPackets(request);
212
264
  await returnAudioTranscoder.start();
213
265
  await transcodingPromise;
214
266
  }
215
267
  stop() {
216
268
  this.audioSplitter.close();
269
+ this.repacketizeAudioSplitter.close();
217
270
  this.videoSplitter.close();
218
271
  this.streamingSession.stop();
219
272
  }
220
273
  }
221
274
  class CameraSource {
222
- constructor(ringCamera) {
275
+ constructor(ringCamera, useOpus = false) {
223
276
  this.ringCamera = ringCamera;
277
+ this.useOpus = useOpus;
224
278
  this.controller = new hap_1.hap.CameraController({
225
279
  cameraStreamCount: 10,
226
280
  delegate: this,
@@ -244,12 +298,28 @@ class CameraSource {
244
298
  },
245
299
  },
246
300
  audio: {
247
- codecs: [
248
- {
249
- type: "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */,
250
- samplerate: 16 /* AudioStreamingSamplerate.KHZ_16 */,
251
- },
252
- ],
301
+ codecs: this.useOpus
302
+ ? [
303
+ {
304
+ type: "OPUS" /* AudioStreamingCodecType.OPUS */,
305
+ // required by watch
306
+ samplerate: 8 /* AudioStreamingSamplerate.KHZ_8 */,
307
+ },
308
+ {
309
+ type: "OPUS" /* AudioStreamingCodecType.OPUS */,
310
+ samplerate: 16 /* AudioStreamingSamplerate.KHZ_16 */,
311
+ },
312
+ {
313
+ type: "OPUS" /* AudioStreamingCodecType.OPUS */,
314
+ samplerate: 24 /* AudioStreamingSamplerate.KHZ_24 */,
315
+ },
316
+ ]
317
+ : [
318
+ {
319
+ type: "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */,
320
+ samplerate: 16 /* AudioStreamingSamplerate.KHZ_16 */,
321
+ },
322
+ ],
253
323
  },
254
324
  },
255
325
  });
@@ -294,6 +364,10 @@ class CameraSource {
294
364
  catch (e) {
295
365
  this.cachedSnapshot = undefined;
296
366
  (0, util_1.logDebug)(`Failed to cache snapshot for ${this.ringCamera.name} (${getDurationSeconds(start)}s), The camera currently reports that it is ${this.ringCamera.isOffline ? 'offline' : 'online'}`);
367
+ // log additioanl snapshot error message if one is present
368
+ if (e.message.includes('Snapshot')) {
369
+ (0, util_1.logDebug)(e.message);
370
+ }
297
371
  }
298
372
  }
299
373
  getCurrentSnapshot() {
@@ -13,7 +13,7 @@ class Camera extends base_data_accessory_1.BaseDataAccessory {
13
13
  this.accessory = accessory;
14
14
  this.logger = logger;
15
15
  this.config = config;
16
- this.cameraSource = new camera_source_1.CameraSource(this.device);
16
+ this.cameraSource = new camera_source_1.CameraSource(this.device, this.config.unbridgeCameras);
17
17
  if (!hap_1.hap.CameraController) {
18
18
  const error = 'HAP CameraController not found. Please make sure you are on homebridge version 1.0.0 or newer';
19
19
  logger.error(error);
@@ -3,6 +3,7 @@ import { API } from 'homebridge';
3
3
  export interface RingPlatformConfig extends RingApiOptions {
4
4
  alarmOnEntryDelay?: boolean;
5
5
  beamDurationSeconds?: number;
6
+ ffmpegPath?: string;
6
7
  hideLightGroups?: boolean;
7
8
  hideDoorbellSwitch?: boolean;
8
9
  hideCameraLight?: boolean;
@@ -12,10 +13,10 @@ export interface RingPlatformConfig extends RingApiOptions {
12
13
  hideAlarmSirenSwitch?: boolean;
13
14
  hideUnsupportedServices?: boolean;
14
15
  hideDeviceIds?: string[];
16
+ nightModeBypassFor: AlarmMode;
15
17
  onlyDeviceTypes?: string[];
16
18
  showPanicButtons?: boolean;
17
- nightModeBypassFor: AlarmMode;
18
- ffmpegPath?: string;
19
+ unbridgeCameras?: boolean;
19
20
  }
20
21
  export declare function updateHomebridgeConfig(homebridge: API, update: (config: string) => string): boolean;
21
22
  export declare function getSystemId(homebridge: API): string;
@@ -0,0 +1,7 @@
1
+ import type { RtpPacket } from 'werift';
2
+ export declare class OpusRepacketizer {
3
+ framesPerPacket: number;
4
+ packets: RtpPacket[];
5
+ constructor(framesPerPacket: number);
6
+ repacketize(packet: RtpPacket): RtpPacket | undefined;
7
+ }
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ // OpusRepacketizer is borrowed from scrypted
3
+ // Original source: https://github.com/koush/scrypted/blob/c13ba09889c3e0d9d3724cb7d49253c9d787fb97/plugins/homekit/src/types/camera/opus-repacketizer.ts
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.OpusRepacketizer = void 0;
6
+ // https://datatracker.ietf.org/doc/html/rfc6716
7
+ // INPUT
8
+ // 0 1 2 3
9
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
10
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
11
+ // | config |s|0|0| |
12
+ // +-+-+-+-+-+-+-+-+ |
13
+ // | Compressed frame 1 (N-1 bytes)... :
14
+ // : |
15
+ // | |
16
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
17
+ // OUTPUT
18
+ // 0 1 2 3
19
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
20
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
21
+ // | config |s|1|1|1|p| M | Padding length (Optional) :
22
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23
+ // : N1 (1-2 bytes): N2 (1-2 bytes): ... : N[M-1] |
24
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25
+ // | |
26
+ // : Compressed frame 1 (N1 bytes)... :
27
+ // | |
28
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
29
+ // | |
30
+ // : Compressed frame 2 (N2 bytes)... :
31
+ // | |
32
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
33
+ // | |
34
+ // : ... :
35
+ // | |
36
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
37
+ // | |
38
+ // : Compressed frame M... :
39
+ // | |
40
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
41
+ // : Opus Padding (Optional)... |
42
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
43
+ // Figure 6: A CBR Code 3 Packet
44
+ // In the VBR case, the (optional) padding length is followed by M-1
45
+ // frame lengths (indicated by "N1" to "N[M-1]" in Figure 7), each
46
+ // encoded in a one- or two-byte sequence as described above. The
47
+ // packet MUST contain enough data for the M-1 lengths after removing
48
+ // the (optional) padding, and the sum of these lengths MUST be no
49
+ // larger than the number of bytes remaining in the packet after
50
+ // decoding them [R7]. The compressed data for all M frames follows,
51
+ // each frame consisting of the indicated number of bytes, with the
52
+ // final frame consuming any remaining bytes before the final padding,
53
+ // as illustrated in Figure 6. The number of header bytes (TOC byte,
54
+ // frame count byte, padding length bytes, and frame length bytes), plus
55
+ // the signaled length of the first M-1 frames themselves, plus the
56
+ // signaled length of the padding MUST be no larger than N, the total
57
+ // size of the packet.
58
+ class OpusRepacketizer {
59
+ constructor(framesPerPacket) {
60
+ this.framesPerPacket = framesPerPacket;
61
+ this.packets = [];
62
+ }
63
+ // repacketize a packet with a single frame into a packet with multiple frames.
64
+ repacketize(packet) {
65
+ if (this.framesPerPacket === 1)
66
+ return packet;
67
+ if (packet.payload[0] & 0b00000011) {
68
+ throw new Error('expected opus packet with a single frame.');
69
+ }
70
+ this.packets.push(packet);
71
+ if (this.packets.length !== this.framesPerPacket) {
72
+ return;
73
+ }
74
+ const first = this.packets[0], depacketized = this.packets.map((p) => p.payload.subarray(1));
75
+ this.packets = [];
76
+ let config = first.payload[0];
77
+ config |= 0b00000011;
78
+ const frameCount = 0b10000000 | this.framesPerPacket, newHeader = [config, frameCount];
79
+ // depacketize by stripping off the config byte
80
+ // M-1 length bytes
81
+ newHeader.push(...depacketized.slice(0, -1).map((data) => data.length));
82
+ const headerBuffer = Buffer.from(newHeader), payload = Buffer.concat([headerBuffer, ...depacketized]);
83
+ packet.payload = payload;
84
+ return packet;
85
+ }
86
+ }
87
+ exports.OpusRepacketizer = OpusRepacketizer;
@@ -155,7 +155,7 @@ class RingPlatform {
155
155
  controlCenterDisplayName: 'homebridge-ring',
156
156
  ...config,
157
157
  systemId,
158
- }), locations = await ringApi.getLocations(), cachedAccessoryIds = Object.keys(this.homebridgeAccessories), platformAccessories = [], activeAccessoryIds = [];
158
+ }), locations = await ringApi.getLocations(), cachedAccessoryIds = Object.keys(this.homebridgeAccessories), platformAccessories = [], externalAccessories = [], activeAccessoryIds = [];
159
159
  this.log.info('Found the following locations:');
160
160
  locations.forEach((location) => {
161
161
  this.log.info(` locationId: ${location.id} - ${location.name}`);
@@ -202,7 +202,7 @@ class RingPlatform {
202
202
  }
203
203
  this.log.info(`Configuring ${cameras.length} cameras and ${hapDevices.length} devices for location "${location.name}" - locationId: ${location.id}`);
204
204
  hapDevices.forEach(({ deviceType, device, isCamera, id, name, AccessoryClass }) => {
205
- const uuid = hap_1.hap.uuid.generate(debugPrefix + id), displayName = debugPrefix + name;
205
+ const uuid = hap_1.hap.uuid.generate(debugPrefix + id), displayName = debugPrefix + name, isExternalCamera = isCamera && this.config.unbridgeCameras;
206
206
  if (!AccessoryClass ||
207
207
  (config.hideLightGroups &&
208
208
  deviceType === api_1.RingDeviceType.BeamsLightGroupSwitch) ||
@@ -215,15 +215,30 @@ class RingPlatform {
215
215
  }
216
216
  return;
217
217
  }
218
+ if (isExternalCamera && this.homebridgeAccessories[uuid]) {
219
+ // Camera was previously bridged. Remove it from the platform so that it can be added as an external accessory
220
+ this.log.warn(`Camera ${displayName} was previously bridged. You will need to manually pair it as a new accessory.`);
221
+ this.api.unregisterPlatformAccessories(exports.pluginName, exports.platformName, [
222
+ this.homebridgeAccessories[uuid],
223
+ ]);
224
+ delete this.homebridgeAccessories[uuid];
225
+ }
218
226
  const createHomebridgeAccessory = () => {
219
227
  const accessory = new api.platformAccessory(displayName, uuid, isCamera
220
228
  ? 17 /* hap.Categories.CAMERA */
221
229
  : 11 /* hap.Categories.SECURITY_SYSTEM */);
222
- this.log.info(`Adding new accessory ${uuid} ${deviceType} ${displayName}`);
223
- platformAccessories.push(accessory);
230
+ if (isExternalCamera) {
231
+ this.log.info(`Configured camera ${uuid} ${deviceType} ${displayName}`);
232
+ externalAccessories.push(accessory);
233
+ }
234
+ else {
235
+ this.log.info(`Adding new accessory ${uuid} ${deviceType} ${displayName}`);
236
+ platformAccessories.push(accessory);
237
+ }
224
238
  if (isCamera &&
239
+ !isExternalCamera &&
225
240
  typeof hap_1.hap.Accessory.cleanupAccessoryData === 'function') {
226
- // This is a one-time cleanup that will remove persist files for old external accessories from before camera bridging in version 8
241
+ // This is a one-time cleanup that will remove persist files for old external accessories from unbridged cameras
227
242
  hap_1.hap.Accessory.cleanupAccessoryData((0, util_2.generateMacAddress)(accessory.UUID));
228
243
  }
229
244
  return accessory;
@@ -236,6 +251,9 @@ class RingPlatform {
236
251
  if (platformAccessories.length) {
237
252
  api.registerPlatformAccessories(exports.pluginName, exports.platformName, platformAccessories);
238
253
  }
254
+ if (externalAccessories.length) {
255
+ api.publishExternalAccessories(exports.pluginName, externalAccessories);
256
+ }
239
257
  const staleAccessories = cachedAccessoryIds
240
258
  .filter((cachedId) => !activeAccessoryIds.includes(cachedId))
241
259
  .map((id) => this.homebridgeAccessories[id]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-ring",
3
- "version": "11.0.6",
3
+ "version": "11.1.0-beta.1",
4
4
  "description": "Homebridge plugin for Ring doorbells, cameras, security alarm system and smart lighting",
5
5
  "main": "lib/homebridge/index.js",
6
6
  "bin": {
@@ -40,44 +40,44 @@
40
40
  "license": "MIT",
41
41
  "dependencies": {
42
42
  "@eneris/push-receiver": "^3.0.2",
43
- "@homebridge/camera-utils": "^2.1.2",
43
+ "@homebridge/camera-utils": "^2.2.0",
44
44
  "@homebridge/plugin-ui-utils": "^0.0.19",
45
- "werift": "0.15.8",
45
+ "werift": "0.15.10",
46
46
  "@types/socket.io-client": "1.4.36",
47
47
  "colors": "^1.4.0",
48
48
  "debug": "^4.3.4",
49
49
  "got": "^11.8.5",
50
- "rxjs": "^7.5.5",
50
+ "rxjs": "^7.5.6",
51
51
  "socket.io-client": "^2.4.0",
52
- "systeminformation": "^5.11.22",
52
+ "systeminformation": "^5.12.1",
53
53
  "uuid": "^8.3.2",
54
- "ws": "^8.8.0"
54
+ "ws": "^8.8.1"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@swc-node/register": "^1.5.1",
58
58
  "@types/debug": "4.1.7",
59
- "@types/jest": "28.1.3",
60
- "@types/node": "18.0.0",
59
+ "@types/jest": "28.1.6",
60
+ "@types/node": "18.6.3",
61
61
  "@types/uuid": "8.3.4",
62
62
  "@types/ws": "^8.5.3",
63
- "@typescript-eslint/eslint-plugin": "5.30.0",
64
- "@typescript-eslint/parser": "5.30.0",
65
- "concurrently": "^7.2.2",
63
+ "@typescript-eslint/eslint-plugin": "5.31.0",
64
+ "@typescript-eslint/parser": "5.31.0",
65
+ "concurrently": "^7.3.0",
66
66
  "conventional-github-releaser": "3.1.5",
67
67
  "dotenv": "16.0.1",
68
- "esbuild": "^0.14.47",
69
- "eslint": "8.18.0",
68
+ "esbuild": "^0.14.51",
69
+ "eslint": "8.20.0",
70
70
  "eslint-config-prettier": "8.5.0",
71
- "eslint-plugin-jest": "26.5.3",
72
- "eslint-plugin-prettier": "4.1.0",
71
+ "eslint-plugin-jest": "26.7.0",
72
+ "eslint-plugin-prettier": "4.2.1",
73
73
  "express": "4.18.1",
74
74
  "homebridge": "1.5.0",
75
- "jest": "28.1.2",
75
+ "jest": "28.1.3",
76
76
  "prettier": "2.7.1",
77
77
  "reflect-metadata": "0.1.13",
78
78
  "rimraf": "3.0.2",
79
79
  "standard-version": "9.5.0",
80
- "ts-jest": "28.0.5",
80
+ "ts-jest": "28.0.7",
81
81
  "typescript": "4.7.4"
82
82
  },
83
83
  "engines": {