homebridge-ring 9.23.0 → 10.0.0-beta.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 CHANGED
@@ -2,6 +2,61 @@
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
+ ## [10.0.0-beta.0](https://github.com/dgreif/ring/compare/v9.25.0-beta.1...v10.0.0-beta.0) (2022-02-18)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * `ring-client-api` only (homebridge not impacted) - All streaming will now be done using the new Ring api via WebRTC + WebSockets. For those using `camera.streamVideo`, you should not notice any difference. Anyone directly interacting with sip sessions, you will need to migrate to the new `LiveCall` api. It's fairly similar, and should be more reliable long term. A number of other sip-specific methods have been removed from the `RingCamera` api as well.
11
+ * Node 10 is no longer officially supported. It may still work, but could break without warning in the future.
12
+ * **homebridge:** The minimum supported homebridge version for this plugin is now 1.4.0
13
+ * **homebridge:** This change will break all existing automations related to cameras, and will likely move them back to the default room in HomeKit. Once updated, you will have to set up your automations again manually. This is necessary to get camera audio working again after the switch to Opus
14
+ * **homebridge:** Switching to Opus requires the camera to be removed and re-added to HomeKit. Until this is done, you will not receive audio from your cameras.
15
+
16
+ ### Features
17
+
18
+ * add floodlight pro ([f749d38](https://github.com/dgreif/ring/commit/f749d38e0528e631864bf20819692a466744e1f2)), closes [#889](https://github.com/dgreif/ring/issues/889)
19
+ * **homebridge:** stream opus audio without ffmpeg for transcoding ([ee142bc](https://github.com/dgreif/ring/commit/ee142bc0eee9dcfda41d162b2105535b8fdcbf9a))
20
+
21
+
22
+ ### Bug Fixes
23
+
24
+ * drop support for node 10 ([509d556](https://github.com/dgreif/ring/commit/509d55670b3dd4efee1be968618f6bc6b142ae2b))
25
+ * **homebridge:** force cameras to be recreated in HomeKit ([94a1520](https://github.com/dgreif/ring/commit/94a15204f4a9ccbe94968be8db05add6baa5fcfa))
26
+ * **homebridge:** increase minimum homebridge version to 1.4.0 ([d5be74a](https://github.com/dgreif/ring/commit/d5be74aae45a3e353bcaba4fc42fa25c6b2fa37e))
27
+ * remove sip calling in favor of live calls ([7aeae37](https://github.com/dgreif/ring/commit/7aeae37e6e93b68747e14983cea9c0a9229faec6))
28
+ * use Buffer.from ([2b28d90](https://github.com/dgreif/ring/commit/2b28d9028fc7f2c2e11b11cf3b27cc03f097eab2))
29
+
30
+ ## [9.25.0-beta.1](https://github.com/dgreif/ring/compare/v9.25.0-beta.0...v9.25.0-beta.1) (2022-02-14)
31
+
32
+
33
+ ### Features
34
+
35
+ * use new WebRTC endpoint for live calls ([6e7779f](https://github.com/dgreif/ring/commit/6e7779feef9789583921fc44bab32c099f3026d2))
36
+
37
+ ## [9.25.0-beta.0](https://github.com/dgreif/ring/compare/v9.24.0...v9.25.0-beta.0) (2022-01-29)
38
+
39
+
40
+ ### Features
41
+
42
+ * **api:** decouple ffmpeg prep from execution ([#859](https://github.com/dgreif/ring/issues/859)) ([e3cc9d1](https://github.com/dgreif/ring/commit/e3cc9d15584377bf2be94a9d457efc3dfcb4bd8b))
43
+ * handle new camera types with no battery ([11ae286](https://github.com/dgreif/ring/commit/11ae28697d8cb0e2da71e880c82e1883f04047b9))
44
+ * slightly faster snapshots via new endpoint ([b840471](https://github.com/dgreif/ring/commit/b840471bc6b7b66e5873370251bd57c1a35b267a))
45
+
46
+ ## [9.24.0](https://github.com/dgreif/ring/compare/v9.23.0...v9.24.0) (2022-01-22)
47
+
48
+
49
+ ### Features
50
+
51
+ * support glassbreak sensor ([98271f9](https://github.com/dgreif/ring/commit/98271f91f13aaae051fccaab7165cd17f2a2377c))
52
+
53
+
54
+ ### Bug Fixes
55
+
56
+ * allow process to exit when RingApi.disconnect() is called ([#851](https://github.com/dgreif/ring/issues/851)) ([8ef25ac](https://github.com/dgreif/ring/commit/8ef25acca66c533873b7fd43c7c899e34b4a7617))
57
+ * avoid error when ring terminates video stream ([7717d5e](https://github.com/dgreif/ring/commit/7717d5eaa40afed3d34f0e8e6eea1402e21afef7)), closes [#853](https://github.com/dgreif/ring/issues/853)
58
+ * update dependencies ([c80d052](https://github.com/dgreif/ring/commit/c80d052f9eaf4c0a50a495166177daa40299ffa7))
59
+
5
60
  ## [9.23.0](https://github.com/dgreif/ring/compare/v9.22.1...v9.23.0) (2021-10-17)
6
61
 
7
62
 
package/README.md CHANGED
@@ -165,6 +165,10 @@ If you are having issues with your cameras in the Home app, please see the [Came
165
165
  * Detect water leak
166
166
  * Tamper status
167
167
  * Battery status
168
+ * Glassbreak Sensor (**untested**)
169
+ * Detect glass breaking
170
+ * Tamper status
171
+ * Battery status
168
172
  * Ring Smart Lights (Motion Detector, Flood/Path/Step/Spot Lights, Bulbs, Transformer, Outdoor Smart Plug, Mailbox Sensor)
169
173
  * On/Off
170
174
  * Brightness Level
package/lib/api/api.js CHANGED
@@ -24,7 +24,8 @@ class RingApi extends subscribed_1.Subscribed {
24
24
  super();
25
25
  this.options = options;
26
26
  this.restClient = new rest_client_1.RingRestClient(this.options);
27
- this.onRefreshTokenUpdated = this.restClient.onRefreshTokenUpdated.asObservable();
27
+ this.onRefreshTokenUpdated =
28
+ this.restClient.onRefreshTokenUpdated.asObservable();
28
29
  if (options.debug) {
29
30
  (0, util_1.enableDebug)();
30
31
  }
@@ -169,6 +170,7 @@ class RingApi extends subscribed_1.Subscribed {
169
170
  .catch((e) => {
170
171
  (0, util_1.logError)(e);
171
172
  });
173
+ this.restClient.clearTimeouts();
172
174
  }
173
175
  }
174
176
  exports.RingApi = RingApi;
@@ -4,4 +4,3 @@ export * from './ring-camera';
4
4
  export * from './ring-chime';
5
5
  export * from './ring-device';
6
6
  export * from './ring-types';
7
- export * from './sip-session';
package/lib/api/index.js CHANGED
@@ -16,4 +16,3 @@ __exportStar(require("./ring-camera"), exports);
16
16
  __exportStar(require("./ring-chime"), exports);
17
17
  __exportStar(require("./ring-device"), exports);
18
18
  __exportStar(require("./ring-types"), exports);
19
- __exportStar(require("./sip-session"), exports);
@@ -0,0 +1,37 @@
1
+ import { ReplaySubject } from 'rxjs';
2
+ import { RingCamera } from './ring-camera';
3
+ import { Subscribed } from './subscribed';
4
+ import { RtpPacket } from '@koush/werift';
5
+ declare type SpawnInput = string | number;
6
+ export interface FfmpegOptions {
7
+ input?: SpawnInput[];
8
+ video?: SpawnInput[] | false;
9
+ audio?: SpawnInput[];
10
+ output: SpawnInput[];
11
+ }
12
+ export declare class LiveCall extends Subscribed {
13
+ private sessionId;
14
+ private camera;
15
+ private readonly ws;
16
+ private readonly onWsOpen;
17
+ private readonly onMessage;
18
+ private readonly pc;
19
+ readonly onCallAnswered: ReplaySubject<string>;
20
+ readonly onCallEnded: ReplaySubject<void>;
21
+ private readonly audioSplitter;
22
+ private readonly videoSplitter;
23
+ readonly onVideoRtp: import("rxjs").Subject<RtpPacket>;
24
+ readonly onAudioRtp: import("rxjs").Subject<RtpPacket>;
25
+ constructor(sessionId: string, camera: RingCamera);
26
+ private handleMessage;
27
+ reservePort(bufferPorts?: number): Promise<number>;
28
+ startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
29
+ activated: boolean;
30
+ activate(): Promise<void>;
31
+ activateCameraSpeaker(): Promise<void>;
32
+ private sendMessage;
33
+ private callEnded;
34
+ stop(): void;
35
+ sendAudioPacket(rtp: RtpPacket): void;
36
+ }
37
+ export {};
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.LiveCall = void 0;
13
+ const rtp_utils_1 = require("./rtp-utils");
14
+ const ws_1 = require("ws");
15
+ const rxjs_1 = require("rxjs");
16
+ const peer_connection_1 = require("./peer-connection");
17
+ const util_1 = require("./util");
18
+ const operators_1 = require("rxjs/operators");
19
+ const subscribed_1 = require("./subscribed");
20
+ const camera_utils_1 = require("@homebridge/camera-utils");
21
+ const ffmpeg_1 = require("./ffmpeg");
22
+ class LiveCall extends subscribed_1.Subscribed {
23
+ constructor(sessionId, camera) {
24
+ super();
25
+ this.sessionId = sessionId;
26
+ this.camera = camera;
27
+ this.onCallAnswered = new rxjs_1.ReplaySubject(1);
28
+ this.onCallEnded = new rxjs_1.ReplaySubject(1);
29
+ this.audioSplitter = new camera_utils_1.RtpSplitter();
30
+ this.videoSplitter = new camera_utils_1.RtpSplitter();
31
+ this.activated = false;
32
+ const liveCallSession = (0, rtp_utils_1.parseLiveCallSession)(sessionId);
33
+ this.pc = new peer_connection_1.PeerConnection();
34
+ this.ws = new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
35
+ headers: {
36
+ API_VERSION: '3.1',
37
+ API_TOKEN: sessionId,
38
+ CLIENT_INFO: 'Ring/3.48.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
39
+ },
40
+ });
41
+ this.onAudioRtp = this.pc.onAudioRtp;
42
+ this.onVideoRtp = this.pc.onVideoRtp;
43
+ this.onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message');
44
+ this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
45
+ const onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
46
+ this.addSubscriptions(this.onMessage
47
+ .pipe((0, operators_1.concatMap)((message) => {
48
+ return this.handleMessage(JSON.parse(message.data));
49
+ }))
50
+ .subscribe(), this.onWsOpen.subscribe(() => {
51
+ (0, util_1.logDebug)(`WebSocket connected for ${this.camera.name}`);
52
+ }), onError.subscribe((e) => {
53
+ (0, util_1.logError)(e);
54
+ this.callEnded();
55
+ }), onClose.subscribe(() => {
56
+ this.callEnded();
57
+ }));
58
+ }
59
+ handleMessage(message) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ switch (message.method) {
62
+ case 'sdp':
63
+ const answer = yield this.pc.createAnswer(message);
64
+ this.sendMessage(Object.assign({ method: 'sdp' }, answer));
65
+ this.onCallAnswered.next(message.sdp);
66
+ return;
67
+ case 'ice':
68
+ yield this.pc.addIceCandidate({
69
+ candidate: message.ice,
70
+ sdpMLineIndex: message.mlineindex,
71
+ });
72
+ return;
73
+ }
74
+ });
75
+ }
76
+ reservePort(bufferPorts = 0) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ const ports = yield (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
79
+ return ports[0];
80
+ });
81
+ }
82
+ startTranscoding(ffmpegOptions) {
83
+ return __awaiter(this, void 0, void 0, function* () {
84
+ const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, ffmpegInputArguments = [
85
+ '-hide_banner',
86
+ '-protocol_whitelist',
87
+ 'pipe,udp,rtp,file,crypto',
88
+ '-acodec',
89
+ 'libopus',
90
+ '-f',
91
+ 'sdp',
92
+ ...(ffmpegOptions.input || []),
93
+ '-i',
94
+ 'pipe:',
95
+ ], inputSdp = (yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered))
96
+ .replace(/m=audio \d+/, `m=audio ${audioPort}`)
97
+ .replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
98
+ ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
99
+ ? ffmpegOptions.video || ['-vcodec', 'copy']
100
+ : []), ...(ffmpegOptions.output || [])),
101
+ ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
102
+ exitCallback: () => this.callEnded(),
103
+ logLabel: `From Ring (${this.camera.name})`,
104
+ logger: {
105
+ error: util_1.logError,
106
+ info: util_1.logDebug,
107
+ },
108
+ });
109
+ this.addSubscriptions(this.onAudioRtp
110
+ .pipe((0, operators_1.concatMap)((rtp) => {
111
+ return this.audioSplitter.send(rtp.serialize(), {
112
+ port: audioPort,
113
+ });
114
+ }))
115
+ .subscribe());
116
+ if (transcodeVideoStream) {
117
+ this.addSubscriptions(this.onVideoRtp
118
+ .pipe((0, operators_1.concatMap)((rtp) => {
119
+ return this.videoSplitter.send(rtp.serialize(), {
120
+ port: videoPort,
121
+ });
122
+ }))
123
+ .subscribe());
124
+ }
125
+ this.onCallEnded.subscribe(() => ff.stop());
126
+ ff.writeStdin(inputSdp);
127
+ // Activate the stream now that ffmpeg is ready to receive
128
+ yield this.activate();
129
+ });
130
+ }
131
+ activate() {
132
+ return __awaiter(this, void 0, void 0, function* () {
133
+ if (this.activated) {
134
+ return;
135
+ }
136
+ this.activated = true;
137
+ yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
138
+ this.sendMessage({ method: 'activate_session' });
139
+ this.sendMessage({
140
+ video_enabled: true,
141
+ audio_enabled: true,
142
+ method: 'stream_options',
143
+ });
144
+ });
145
+ }
146
+ activateCameraSpeaker() {
147
+ return __awaiter(this, void 0, void 0, function* () {
148
+ yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
149
+ this.sendMessage({
150
+ stealth_mode: false,
151
+ method: 'camera_options',
152
+ });
153
+ });
154
+ }
155
+ sendMessage(message) {
156
+ this.ws.send(JSON.stringify(message));
157
+ }
158
+ callEnded() {
159
+ try {
160
+ this.sendMessage({
161
+ reason: { code: 0, text: '' },
162
+ method: 'close',
163
+ });
164
+ this.ws.close();
165
+ }
166
+ catch (_) {
167
+ // ignore any errors since we are stopping the call
168
+ }
169
+ this.unsubscribe();
170
+ this.onCallEnded.next();
171
+ this.pc.close();
172
+ this.audioSplitter.close();
173
+ this.videoSplitter.close();
174
+ }
175
+ stop() {
176
+ this.callEnded();
177
+ }
178
+ sendAudioPacket(rtp) {
179
+ this.pc.returnAudioTrack.writeRtp(rtp);
180
+ }
181
+ }
182
+ exports.LiveCall = LiveCall;
@@ -0,0 +1,17 @@
1
+ import { MediaStreamTrack, RTCIceCandidate, RTCPeerConnection, RtcpPacket, RtpPacket } from '@koush/werift';
2
+ import { Subject } from 'rxjs';
3
+ export declare class PeerConnection {
4
+ pc: RTCPeerConnection;
5
+ onAudioRtp: Subject<RtpPacket>;
6
+ onAudioRtcp: Subject<RtcpPacket>;
7
+ onVideoRtp: Subject<RtpPacket>;
8
+ onVideoRtcp: Subject<RtcpPacket>;
9
+ returnAudioTrack: MediaStreamTrack;
10
+ constructor();
11
+ createAnswer(offer: {
12
+ type: 'offer';
13
+ sdp: string;
14
+ }): Promise<import("@koush/werift").RTCSessionDescription>;
15
+ addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
16
+ close(): void;
17
+ }
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.PeerConnection = void 0;
13
+ const werift_1 = require("@koush/werift");
14
+ const rxjs_1 = require("rxjs");
15
+ const util_1 = require("./util");
16
+ const debug = false;
17
+ class PeerConnection {
18
+ constructor() {
19
+ this.onAudioRtp = new rxjs_1.Subject();
20
+ this.onAudioRtcp = new rxjs_1.Subject();
21
+ this.onVideoRtp = new rxjs_1.Subject();
22
+ this.onVideoRtcp = new rxjs_1.Subject();
23
+ this.returnAudioTrack = new werift_1.MediaStreamTrack({ kind: 'audio' });
24
+ const pc = (this.pc = new werift_1.RTCPeerConnection({
25
+ codecs: {
26
+ audio: [
27
+ new werift_1.RTCRtpCodecParameters({
28
+ mimeType: 'audio/opus',
29
+ clockRate: 48000,
30
+ channels: 2,
31
+ }),
32
+ ],
33
+ video: [
34
+ new werift_1.RTCRtpCodecParameters({
35
+ mimeType: 'video/H264',
36
+ clockRate: 90000,
37
+ rtcpFeedback: [
38
+ { type: 'transport-cc' },
39
+ { type: 'ccm', parameter: 'fir' },
40
+ { type: 'nack' },
41
+ { type: 'nack', parameter: 'pli' },
42
+ { type: 'goog-remb' },
43
+ ],
44
+ parameters: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
45
+ }),
46
+ ],
47
+ },
48
+ })), audioTransceiver = pc.addTransceiver(this.returnAudioTrack, {
49
+ direction: 'sendrecv',
50
+ }), videoTransceiver = pc.addTransceiver('video', {
51
+ direction: 'recvonly',
52
+ });
53
+ audioTransceiver.onTrack.subscribe((track) => {
54
+ track.onReceiveRtp.subscribe((rtp) => {
55
+ this.onAudioRtp.next(rtp);
56
+ });
57
+ track.onReceiveRtcp.subscribe((rtcp) => {
58
+ this.onAudioRtcp.next(rtcp);
59
+ });
60
+ if (debug) {
61
+ track.onReceiveRtp.once(() => {
62
+ (0, util_1.logInfo)('received first audio packet');
63
+ });
64
+ }
65
+ });
66
+ videoTransceiver.onTrack.subscribe((track) => {
67
+ track.onReceiveRtp.subscribe((rtp) => {
68
+ this.onVideoRtp.next(rtp);
69
+ });
70
+ track.onReceiveRtcp.subscribe((rtcp) => {
71
+ this.onVideoRtcp.next(rtcp);
72
+ });
73
+ track.onReceiveRtp.once(() => {
74
+ if (debug) {
75
+ (0, util_1.logInfo)('received first video packet');
76
+ }
77
+ setInterval(() => videoTransceiver.receiver.sendRtcpPLI(track.ssrc), 2000);
78
+ });
79
+ });
80
+ }
81
+ createAnswer(offer) {
82
+ return __awaiter(this, void 0, void 0, function* () {
83
+ yield this.pc.setRemoteDescription(offer);
84
+ const answer = yield this.pc.createAnswer();
85
+ yield this.pc.setLocalDescription(answer);
86
+ return answer;
87
+ });
88
+ }
89
+ addIceCandidate(candidate) {
90
+ return this.pc.addIceCandidate(candidate);
91
+ }
92
+ close() {
93
+ this.pc.close().catch(util_1.logError);
94
+ }
95
+ }
96
+ exports.PeerConnection = PeerConnection;
@@ -6,6 +6,7 @@ export declare function deviceApi(path: string): string;
6
6
  export declare function appApi(path: string): string;
7
7
  export interface ExtendedResponse {
8
8
  responseTimestamp: number;
9
+ timeMillis: number;
9
10
  }
10
11
  export interface EmailAuth {
11
12
  email: string;
@@ -24,6 +25,7 @@ export declare class RingRestClient {
24
25
  refreshToken: string | undefined;
25
26
  private hardwareIdPromise;
26
27
  private _authPromise;
28
+ private timeouts;
27
29
  private clearPreviousAuth;
28
30
  private get authPromise();
29
31
  private sessionPromise?;
@@ -44,4 +46,5 @@ export declare class RingRestClient {
44
46
  url: string;
45
47
  }): Promise<T & ExtendedResponse>;
46
48
  getCurrentAuth(): Promise<AuthTokenResponse>;
49
+ clearTimeouts(): void;
47
50
  }
@@ -44,8 +44,13 @@ function requestWithRetry(requestOptions) {
44
44
  return __awaiter(this, void 0, void 0, function* () {
45
45
  try {
46
46
  const options = Object.assign(Object.assign({}, defaultRequestOptions), requestOptions), { headers, body } = (yield (0, got_1.default)(options)), data = body;
47
- if (data !== null && typeof data === 'object' && headers.date) {
48
- data.responseTimestamp = new Date(headers.date).getTime();
47
+ if (data !== null && typeof data === 'object') {
48
+ if (headers.date) {
49
+ data.responseTimestamp = new Date(headers.date).getTime();
50
+ }
51
+ if (headers['x-time-millis']) {
52
+ data.timeMillis = Number(headers['x-time-millis']);
53
+ }
49
54
  }
50
55
  return data;
51
56
  }
@@ -66,12 +71,15 @@ function requestWithRetry(requestOptions) {
66
71
  class RingRestClient {
67
72
  constructor(authOptions) {
68
73
  this.authOptions = authOptions;
69
- // prettier-ignore
70
- this.refreshToken = ('refreshToken' in this.authOptions ? this.authOptions.refreshToken : undefined);
71
- this.hardwareIdPromise = (0, util_1.getHardwareId)(this.authOptions.systemId);
74
+ this.timeouts = [];
72
75
  this.sessionPromise = undefined;
73
76
  this.using2fa = false;
74
77
  this.onRefreshTokenUpdated = new rxjs_1.ReplaySubject(1);
78
+ this.refreshToken =
79
+ 'refreshToken' in this.authOptions
80
+ ? this.authOptions.refreshToken
81
+ : undefined;
82
+ this.hardwareIdPromise = (0, util_1.getHardwareId)(this.authOptions.systemId);
75
83
  }
76
84
  clearPreviousAuth() {
77
85
  this._authPromise = undefined;
@@ -83,11 +91,12 @@ class RingRestClient {
83
91
  authPromise
84
92
  .then(({ expires_in }) => {
85
93
  // clear the existing auth promise 1 minute before it expires
86
- setTimeout(() => {
94
+ const timeout = setTimeout(() => {
87
95
  if (this._authPromise === authPromise) {
88
96
  this.clearPreviousAuth();
89
97
  }
90
98
  }, ((expires_in || 3600) - 60) * 1000);
99
+ this.timeouts.push(timeout);
91
100
  })
92
101
  .catch(() => {
93
102
  // ignore these errors here, they should be handled by the function making a rest request
@@ -288,5 +297,8 @@ class RingRestClient {
288
297
  getCurrentAuth() {
289
298
  return this.authPromise;
290
299
  }
300
+ clearTimeouts() {
301
+ this.timeouts.forEach(clearTimeout);
302
+ }
291
303
  }
292
304
  exports.RingRestClient = RingRestClient;
@@ -2,11 +2,9 @@
2
2
  import { ActiveDing, CameraData, CameraDeviceSettingsData, CameraEventOptions, CameraEventResponse, CameraHealth, HistoryOptions, PeriodicFootageResponse, VideoSearchResponse } from './ring-types';
3
3
  import { RingRestClient } from './rest-client';
4
4
  import { BehaviorSubject, Subject } from 'rxjs';
5
- import { SrtpOptions } from '@homebridge/camera-utils';
6
5
  import { DeepPartial } from './util';
7
- import { FfmpegOptions, SipSession } from './sip-session';
8
- import { SipOptions } from './sip-call';
9
6
  import { Subscribed } from './subscribed';
7
+ import { FfmpegOptions, LiveCall } from './live-call';
10
8
  export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'>): number | null;
11
9
  export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
12
10
  accountId: string;
@@ -23,7 +21,6 @@ export declare class RingCamera extends Subscribed {
23
21
  onData: BehaviorSubject<CameraData>;
24
22
  hasLight: boolean;
25
23
  hasSiren: boolean;
26
- hasBattery: boolean;
27
24
  onRequestUpdate: Subject<unknown>;
28
25
  onRequestActiveDings: Subject<unknown>;
29
26
  onNewDing: Subject<ActiveDing>;
@@ -40,6 +37,7 @@ export declare class RingCamera extends Subscribed {
40
37
  get name(): string;
41
38
  get activeDings(): ActiveDing[];
42
39
  get batteryLevel(): number | null;
40
+ get hasBattery(): boolean;
43
41
  get hasLowBattery(): boolean;
44
42
  get isCharging(): boolean;
45
43
  get operatingOnBattery(): boolean;
@@ -54,10 +52,7 @@ export declare class RingCamera extends Subscribed {
54
52
  getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
55
53
  setInHomeDoorbell(enable: boolean): Promise<boolean>;
56
54
  getHealth(): Promise<CameraHealth>;
57
- startVideoOnDemand(): Promise<("" | ActiveDing) & import("./rest-client").ExtendedResponse>;
58
- private pollForActiveDing;
59
- private expiredDingIds;
60
- getSipConnectionDetails(): Promise<ActiveDing>;
55
+ startLiveCall(): Promise<LiveCall>;
61
56
  private removeDingById;
62
57
  processActiveDing(ding: ActiveDing): void;
63
58
  getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
@@ -75,27 +70,23 @@ export declare class RingCamera extends Subscribed {
75
70
  }): Promise<string>;
76
71
  private isTimestampInLifeTime;
77
72
  get snapshotsAreBlocked(): boolean;
78
- private getSnapshotTimestamp;
79
- private refreshSnapshotInProgress?;
80
73
  get snapshotLifeTime(): number;
74
+ private lastSnapshotTimestamp;
81
75
  private lastSnapshotTimestampLocal;
82
76
  private lastSnapshotPromise?;
83
77
  get currentTimestampAge(): number;
84
- get currentTimestampExpiresIn(): number;
85
78
  get hasSnapshotWithinLifetime(): boolean;
86
79
  private checkIfSnapshotsAreBlocked;
87
- private requestSnapshotUpdate;
88
- private refreshSnapshot;
89
- getSnapshot(): Promise<Buffer | undefined>;
90
- getSipOptions(): Promise<SipOptions>;
91
- getUpdatedSipOptions(expiredDingId: string): Promise<SipOptions>;
92
- createSipSession(options?: {
93
- audio?: SrtpOptions;
94
- video?: SrtpOptions;
95
- skipFfmpegCheck?: boolean;
96
- }): Promise<SipSession>;
80
+ private shouldUseExistingSnapshotPromise;
81
+ private fetchingSnapshot;
82
+ getSnapshot(): Promise<Buffer>;
83
+ getNextSnapshot({ afterMs, maxWaitMs, force, }: {
84
+ afterMs?: number;
85
+ maxWaitMs?: number;
86
+ force?: boolean;
87
+ }): Promise<Buffer & import("./rest-client").ExtendedResponse>;
97
88
  recordToFile(outputPath: string, duration?: number): Promise<void>;
98
- streamVideo(ffmpegOptions: FfmpegOptions): Promise<SipSession>;
89
+ streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall>;
99
90
  subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
100
91
  unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
101
92
  subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;