ring-client-api 10.0.0-beta.2 → 10.0.0-beta.5

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.
@@ -18,17 +18,6 @@ const operators_1 = require("rxjs/operators");
18
18
  const subscribed_1 = require("./subscribed");
19
19
  const camera_utils_1 = require("@homebridge/camera-utils");
20
20
  const ffmpeg_1 = require("./ffmpeg");
21
- // eslint-disable-next-line no-shadow
22
- var CloseReasonCode;
23
- (function (CloseReasonCode) {
24
- CloseReasonCode[CloseReasonCode["NormalClose"] = 0] = "NormalClose";
25
- // reason: { code: 5, text: '[rsl-apps/webrtc-liveview-server/Session.cpp:429] [Auth] [0xd540]: [rsl-apps/session-manager/Manager.cpp:227] [AppAuth] Unauthorized: invalid or expired token' }
26
- // reason: { code: 5, text: 'Authentication failed: -1' }
27
- // reason: { code: 5, text: 'Sessions with the provided ID not found' }
28
- CloseReasonCode[CloseReasonCode["AuthenticationFailed"] = 5] = "AuthenticationFailed";
29
- // reason: { code: 6, text: 'Timeout waiting for ping' }
30
- CloseReasonCode[CloseReasonCode["Timeout"] = 6] = "Timeout";
31
- })(CloseReasonCode || (CloseReasonCode = {}));
32
21
  function getCleanSdp(sdp, includeVideo) {
33
22
  return sdp
34
23
  .split('\nm=')
@@ -37,26 +26,27 @@ function getCleanSdp(sdp, includeVideo) {
37
26
  .filter((section) => includeVideo || !section.startsWith('m=video'))
38
27
  .join('\n');
39
28
  }
29
+ function parseLiveCallSession(sessionId) {
30
+ const encodedSession = sessionId.split('.')[1], buff = Buffer.from(encodedSession, 'base64'), text = buff.toString('ascii');
31
+ return JSON.parse(text);
32
+ }
40
33
  class LiveCall extends subscribed_1.Subscribed {
41
- constructor(authToken, camera) {
34
+ constructor(sessionId, camera) {
42
35
  super();
36
+ this.sessionId = sessionId;
43
37
  this.camera = camera;
44
- this.onSessionId = new rxjs_1.ReplaySubject(1);
45
38
  this.onCallAnswered = new rxjs_1.ReplaySubject(1);
46
39
  this.onCallEnded = new rxjs_1.ReplaySubject(1);
47
40
  this.audioSplitter = new camera_utils_1.RtpSplitter();
48
41
  this.videoSplitter = new camera_utils_1.RtpSplitter();
49
- this.onOfferSent = new rxjs_1.ReplaySubject(1);
50
- this.sessionId = null;
51
42
  this.activated = false;
43
+ const liveCallSession = parseLiveCallSession(sessionId);
52
44
  this.pc = new peer_connection_1.PeerConnection();
53
- this.ws = new ws_1.WebSocket('wss://api.prod.signalling.ring.devices.a2z.com:443/ws', {
45
+ this.ws = new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
54
46
  headers: {
55
- Authorization: `Bearer ${authToken}`,
56
- 'X-Sig-API-Version': '4.0',
57
- 'X-Sig-Client-ID': 'ring_android-aabb123',
58
- 'X-Sig-Client-Info': 'Ring/3.49.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
59
- 'X-Sig-Auth-Type': 'ring_oauth',
47
+ API_VERSION: '3.1',
48
+ API_TOKEN: sessionId,
49
+ CLIENT_INFO: 'Ring/3.49.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
60
50
  },
61
51
  });
62
52
  this.onAudioRtp = this.pc.onAudioRtp;
@@ -70,109 +60,28 @@ class LiveCall extends subscribed_1.Subscribed {
70
60
  }))
71
61
  .subscribe(), this.onWsOpen.subscribe(() => {
72
62
  (0, util_1.logDebug)(`WebSocket connected for ${this.camera.name}`);
73
- this.initiateCall().catch((e) => {
74
- (0, util_1.logError)(e);
75
- this.callEnded();
76
- });
77
63
  }), onError.subscribe((e) => {
78
64
  (0, util_1.logError)(e);
79
65
  this.callEnded();
80
66
  }), onClose.subscribe(() => {
81
67
  this.callEnded();
82
- }), this.onCallAnswered
83
- .pipe((0, operators_1.switchMap)(() => (0, rxjs_1.interval)(5000)))
84
- .subscribe(() => {
85
- if (!this.sessionId) {
86
- return;
87
- }
88
- this.sendSessionMessage('ping');
89
- }), this.pc.onIceCandidate.subscribe((iceCandidate) => __awaiter(this, void 0, void 0, function* () {
90
- yield (0, rxjs_1.firstValueFrom)(this.onOfferSent);
91
- this.sendMessage({
92
- method: 'ice',
93
- body: {
94
- doorbot_id: this.camera.id,
95
- ice: iceCandidate.candidate,
96
- mlineindex: 0,
97
- },
98
- });
99
- // HACK: send ice candidate with both mline indexes to convince ring edge to connect both audio and video
100
- // Without this, only audio will connect unless you connect from the network of the Ring Edge router
101
- this.sendMessage({
102
- method: 'ice',
103
- body: {
104
- doorbot_id: this.camera.id,
105
- ice: iceCandidate.candidate,
106
- mlineindex: 1,
107
- },
108
- });
109
- })), this.pc.onConnectionState.subscribe((state) => {
110
- if (state === 'failed') {
111
- (0, util_1.logError)('Stream connection failed');
112
- this.callEnded();
113
- }
114
- if (state === 'closed') {
115
- (0, util_1.logDebug)('Stream connection closed');
116
- this.callEnded();
117
- }
118
68
  }));
119
69
  }
120
- initiateCall() {
121
- return __awaiter(this, void 0, void 0, function* () {
122
- const offer = yield this.pc.createOffer();
123
- offer.sdp = offer.sdp.replace('\na=group:BUNDLE 0 1', '');
124
- this.sendMessage({
125
- method: 'live_view',
126
- body: {
127
- doorbot_id: this.camera.id,
128
- stream_options: { audio_enabled: true, video_enabled: true },
129
- sdp: offer.sdp,
130
- },
131
- });
132
- this.onOfferSent.next();
133
- });
134
- }
135
70
  handleMessage(message) {
136
71
  return __awaiter(this, void 0, void 0, function* () {
137
- if (message.body.doorbot_id !== this.camera.id) {
138
- // ignore messages for other cameras
139
- return;
140
- }
141
- if (['session_created', 'session_started'].includes(message.method) &&
142
- 'session_id' in message.body &&
143
- !this.sessionId) {
144
- this.sessionId = message.body.session_id;
145
- this.onSessionId.next(this.sessionId);
146
- }
147
- if (message.body.session_id && message.body.session_id !== this.sessionId) {
148
- // ignore messages for other sessions
149
- return;
150
- }
151
72
  switch (message.method) {
152
- case 'session_created':
153
- case 'session_started':
154
- // session already stored above
155
- return;
156
73
  case 'sdp':
157
- yield this.pc.acceptAnswer(message.body);
158
- this.onCallAnswered.next(message.body.sdp);
74
+ const answer = yield this.pc.createAnswer(message);
75
+ this.sendMessage(Object.assign({ method: 'sdp' }, answer));
76
+ this.onCallAnswered.next(message.sdp);
159
77
  return;
160
78
  case 'ice':
161
79
  yield this.pc.addIceCandidate({
162
- candidate: message.body.ice,
163
- sdpMLineIndex: message.body.mlineindex,
80
+ candidate: message.ice,
81
+ sdpMLineIndex: message.mlineindex,
164
82
  });
165
83
  return;
166
- case 'pong':
167
- return;
168
- case 'close':
169
- (0, util_1.logError)('Video stream closed');
170
- (0, util_1.logError)(message.body);
171
- this.callEnded();
172
- return;
173
84
  }
174
- (0, util_1.logError)('UNKNOWN MESSAGE');
175
- (0, util_1.logError)(message);
176
85
  });
177
86
  }
178
87
  reservePort(bufferPorts = 0) {
@@ -238,40 +147,31 @@ class LiveCall extends subscribed_1.Subscribed {
238
147
  this.activated = true;
239
148
  yield (0, rxjs_1.firstValueFrom)(this.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')));
240
149
  (0, util_1.logInfo)('Activating Session');
241
- this.sendSessionMessage('activate_session');
150
+ this.sendMessage({ method: 'activate_session' });
151
+ this.sendMessage({
152
+ video_enabled: true,
153
+ audio_enabled: true,
154
+ method: 'stream_options',
155
+ });
242
156
  });
243
157
  }
244
158
  activateCameraSpeaker() {
245
159
  return __awaiter(this, void 0, void 0, function* () {
246
160
  yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
247
- this.sendSessionMessage('camera_options', {
161
+ this.sendMessage({
248
162
  stealth_mode: false,
163
+ method: 'camera_options',
249
164
  });
250
165
  });
251
166
  }
252
- sendSessionMessage(method, body = {}) {
253
- const message = {
254
- method,
255
- body: Object.assign(Object.assign({}, body), { doorbot_id: this.camera.id, session_id: this.sessionId }),
256
- };
257
- if (this.sessionId) {
258
- // Send immediately if we already have a session id
259
- // This is needed to send `close` before closing the websocket
260
- this.sendMessage(message);
261
- }
262
- else {
263
- (0, rxjs_1.firstValueFrom)(this.onSessionId)
264
- .then(() => this.sendMessage(message))
265
- .catch((e) => (0, util_1.logError)(e));
266
- }
267
- }
268
167
  sendMessage(message) {
269
168
  this.ws.send(JSON.stringify(message));
270
169
  }
271
170
  callEnded() {
272
171
  try {
273
- this.sendSessionMessage('close', {
274
- reason: { code: CloseReasonCode.NormalClose, text: '' },
172
+ this.sendMessage({
173
+ reason: { code: 0, text: '' },
174
+ method: 'close',
275
175
  });
276
176
  this.ws.close();
277
177
  }
@@ -11,6 +11,10 @@ export declare class PeerConnection {
11
11
  returnAudioTrack: MediaStreamTrack;
12
12
  constructor();
13
13
  createOffer(): Promise<import("@koush/werift").RTCSessionDescription>;
14
+ createAnswer(offer: {
15
+ type: 'offer';
16
+ sdp: string;
17
+ }): Promise<import("@koush/werift").RTCSessionDescription>;
14
18
  acceptAnswer(answer: {
15
19
  type: 'answer';
16
20
  sdp: string;
@@ -13,7 +13,16 @@ exports.PeerConnection = void 0;
13
13
  const werift_1 = require("@koush/werift");
14
14
  const rxjs_1 = require("rxjs");
15
15
  const util_1 = require("./util");
16
- const debug = false;
16
+ const debug = false, ringIceServers = [
17
+ 'stun:stun.kinesisvideo.us-east-1.amazonaws.com:443',
18
+ 'stun:stun.kinesisvideo.us-east-2.amazonaws.com:443',
19
+ 'stun:stun.kinesisvideo.us-west-2.amazonaws.com:443',
20
+ 'stun:stun.l.google.com:19302',
21
+ 'stun:stun1.l.google.com:19302',
22
+ 'stun:stun2.l.google.com:19302',
23
+ 'stun:stun3.l.google.com:19302',
24
+ 'stun:stun4.l.google.com:19302',
25
+ ];
17
26
  class PeerConnection {
18
27
  constructor() {
19
28
  this.onAudioRtp = new rxjs_1.Subject();
@@ -43,10 +52,12 @@ class PeerConnection {
43
52
  { type: 'nack', parameter: 'pli' },
44
53
  { type: 'goog-remb' },
45
54
  ],
46
- parameters: 'profile-level-id=42801F',
55
+ parameters: 'packetization-mode=1;profile-level-id=640029;level-asymmetry-allowed=1',
47
56
  }),
48
57
  ],
49
58
  },
59
+ iceServers: ringIceServers.map((server) => ({ urls: server })),
60
+ iceTransportPolicy: 'all',
50
61
  })), audioTransceiver = pc.addTransceiver(this.returnAudioTrack, {
51
62
  direction: 'sendrecv',
52
63
  }), videoTransceiver = pc.addTransceiver('video', {
@@ -100,6 +111,14 @@ class PeerConnection {
100
111
  return offer;
101
112
  });
102
113
  }
114
+ createAnswer(offer) {
115
+ return __awaiter(this, void 0, void 0, function* () {
116
+ yield this.pc.setRemoteDescription(offer);
117
+ const answer = yield this.pc.createAnswer();
118
+ yield this.pc.setLocalDescription(answer);
119
+ return answer;
120
+ });
121
+ }
103
122
  acceptAnswer(answer) {
104
123
  return __awaiter(this, void 0, void 0, function* () {
105
124
  yield this.pc.setRemoteDescription(answer);
@@ -1,11 +1,14 @@
1
1
  /// <reference types="node" />
2
- import { ActiveDing, CameraData, CameraDeviceSettingsData, CameraEventOptions, CameraEventResponse, CameraHealth, HistoryOptions, PeriodicFootageResponse, VideoSearchResponse } from './ring-types';
2
+ import { CameraData, CameraDeviceSettingsData, CameraEventOptions, CameraEventResponse, CameraHealth, HistoryOptions, PeriodicFootageResponse, PushNotification, VideoSearchResponse } from './ring-types';
3
3
  import { RingRestClient } from './rest-client';
4
4
  import { BehaviorSubject, Subject } from 'rxjs';
5
5
  import { DeepPartial } from './util';
6
6
  import { Subscribed } from './subscribed';
7
7
  import { FfmpegOptions, LiveCall } from './live-call';
8
- export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'>): number | null;
8
+ import { LiveCallRingEdge } from './live-call-ring-edge';
9
+ export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'> & {
10
+ health?: Partial<CameraData['health']>;
11
+ }): number | null;
9
12
  export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
10
13
  accountId: string;
11
14
  })): string;
@@ -14,7 +17,6 @@ export declare class RingCamera extends Subscribed {
14
17
  isDoorbot: boolean;
15
18
  private restClient;
16
19
  private avoidSnapshotBatteryDrain;
17
- private treatKnockAsDing;
18
20
  id: number;
19
21
  deviceType: import("./ring-types").RingCameraKind;
20
22
  model: string;
@@ -22,20 +24,20 @@ export declare class RingCamera extends Subscribed {
22
24
  hasLight: boolean;
23
25
  hasSiren: boolean;
24
26
  onRequestUpdate: Subject<unknown>;
25
- onRequestActiveDings: Subject<unknown>;
26
- onNewDing: Subject<ActiveDing>;
27
- onActiveDings: BehaviorSubject<ActiveDing[]>;
28
- onDoorbellPressed: import("rxjs").Observable<ActiveDing>;
27
+ onNewNotification: Subject<PushNotification>;
28
+ onActiveNotifications: BehaviorSubject<PushNotification[]>;
29
+ onDoorbellPressed: import("rxjs").Observable<PushNotification>;
29
30
  onMotionDetected: import("rxjs").Observable<boolean>;
30
31
  onMotionStarted: import("rxjs").Observable<null>;
31
32
  onBatteryLevel: import("rxjs").Observable<number | null>;
32
33
  onInHomeDoorbellStatus: import("rxjs").Observable<boolean>;
33
- constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean, treatKnockAsDing: boolean);
34
+ constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean);
34
35
  updateData(update: CameraData): void;
35
36
  requestUpdate(): void;
36
37
  get data(): CameraData;
37
38
  get name(): string;
38
- get activeDings(): ActiveDing[];
39
+ get activeNotifications(): PushNotification[];
40
+ get latestNotification(): PushNotification | undefined;
39
41
  get batteryLevel(): number | null;
40
42
  get hasBattery(): boolean;
41
43
  get hasLowBattery(): boolean;
@@ -53,9 +55,9 @@ export declare class RingCamera extends Subscribed {
53
55
  getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
54
56
  setInHomeDoorbell(enable: boolean): Promise<boolean>;
55
57
  getHealth(): Promise<CameraHealth>;
56
- startLiveCall(): Promise<LiveCall>;
58
+ startLiveCall(): Promise<LiveCall | LiveCallRingEdge>;
57
59
  private removeDingById;
58
- processActiveDing(ding: ActiveDing): void;
60
+ processPushNotification(notification: PushNotification): void;
59
61
  getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
60
62
  videoSearch({ dateFrom, dateTo, order }?: {
61
63
  dateFrom: number;
@@ -80,14 +82,18 @@ export declare class RingCamera extends Subscribed {
80
82
  private checkIfSnapshotsAreBlocked;
81
83
  private shouldUseExistingSnapshotPromise;
82
84
  private fetchingSnapshot;
83
- getSnapshot(): Promise<Buffer>;
84
- getNextSnapshot({ afterMs, maxWaitMs, force, }: {
85
+ getSnapshot({ uuid }?: {
86
+ uuid?: string;
87
+ }): Promise<Buffer>;
88
+ getNextSnapshot({ afterMs, maxWaitMs, force, uuid, }: {
85
89
  afterMs?: number;
86
90
  maxWaitMs?: number;
87
91
  force?: boolean;
92
+ uuid?: string;
88
93
  }): Promise<Buffer & import("./rest-client").ExtendedResponse>;
94
+ getSnapshotByUuid(uuid: string): Promise<Buffer & import("./rest-client").ExtendedResponse>;
89
95
  recordToFile(outputPath: string, duration?: number): Promise<void>;
90
- streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall>;
96
+ streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall | LiveCallRingEdge>;
91
97
  subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
92
98
  unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
93
99
  subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
@@ -17,6 +17,7 @@ const operators_1 = require("rxjs/operators");
17
17
  const util_1 = require("./util");
18
18
  const subscribed_1 = require("./subscribed");
19
19
  const live_call_1 = require("./live-call");
20
+ const live_call_ring_edge_1 = require("./live-call-ring-edge");
20
21
  const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
21
22
  function parseBatteryLife(batteryLife) {
22
23
  if (batteryLife === null || batteryLife === undefined) {
@@ -40,8 +41,9 @@ function getBatteryLevel(data) {
40
41
  const levels = [
41
42
  parseBatteryLife(data.battery_life),
42
43
  parseBatteryLife(data.battery_life_2),
43
- ].filter((level) => level !== null);
44
- if (!levels.length) {
44
+ ].filter((level) => level !== null), { health } = data;
45
+ if (!levels.length ||
46
+ (health && !health.battery_percentage && !health.battery_present)) {
45
47
  return null;
46
48
  }
47
49
  return Math.min(...levels);
@@ -64,20 +66,17 @@ function getSearchQueryString(options) {
64
66
  }
65
67
  exports.getSearchQueryString = getSearchQueryString;
66
68
  class RingCamera extends subscribed_1.Subscribed {
67
- constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain, treatKnockAsDing) {
69
+ constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain) {
68
70
  super();
69
71
  this.initialData = initialData;
70
72
  this.isDoorbot = isDoorbot;
71
73
  this.restClient = restClient;
72
74
  this.avoidSnapshotBatteryDrain = avoidSnapshotBatteryDrain;
73
- this.treatKnockAsDing = treatKnockAsDing;
74
75
  this.onRequestUpdate = new rxjs_1.Subject();
75
- this.onRequestActiveDings = new rxjs_1.Subject();
76
- this.onNewDing = new rxjs_1.Subject();
77
- this.onActiveDings = new rxjs_1.BehaviorSubject([]);
78
- this.onDoorbellPressed = this.onNewDing.pipe((0, operators_1.filter)((ding) => ding.kind === 'ding' ||
79
- (this.treatKnockAsDing && ding.kind === 'door_activity')), (0, operators_1.share)());
80
- this.onMotionDetected = this.onActiveDings.pipe((0, operators_1.map)((dings) => dings.some((ding) => ding.motion || ding.kind === 'motion')), (0, operators_1.distinctUntilChanged)(), (0, operators_1.publishReplay)(1), (0, operators_1.refCount)());
76
+ this.onNewNotification = new rxjs_1.Subject();
77
+ this.onActiveNotifications = new rxjs_1.BehaviorSubject([]);
78
+ this.onDoorbellPressed = this.onNewNotification.pipe((0, operators_1.filter)((notification) => notification.action === ring_types_1.PushNotificationAction.Ding), (0, operators_1.share)());
79
+ this.onMotionDetected = this.onActiveNotifications.pipe((0, operators_1.map)((notifications) => notifications.some((notification) => notification.action === ring_types_1.PushNotificationAction.Motion)), (0, operators_1.distinctUntilChanged)(), (0, operators_1.publishReplay)(1), (0, operators_1.refCount)());
81
80
  this.onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.mapTo)(null), // no value needed, event is what matters
82
81
  (0, operators_1.share)());
83
82
  this.lastSnapshotTimestamp = 0;
@@ -118,20 +117,18 @@ class RingCamera extends subscribed_1.Subscribed {
118
117
  get name() {
119
118
  return this.data.description;
120
119
  }
121
- get activeDings() {
122
- return this.onActiveDings.getValue();
120
+ get activeNotifications() {
121
+ return this.onActiveNotifications.getValue();
122
+ }
123
+ get latestNotification() {
124
+ const notifications = this.activeNotifications;
125
+ return notifications[notifications.length - 1];
123
126
  }
124
127
  get batteryLevel() {
125
128
  return getBatteryLevel(this.data);
126
129
  }
127
130
  get hasBattery() {
128
- if (this.batteryLevel === null) {
129
- return false;
130
- }
131
- return ((0, ring_types_1.isBatteryCameraKind)(this.deviceType) ||
132
- (typeof this.initialData.battery_life === 'string' &&
133
- this.batteryLevel < 100 &&
134
- this.batteryLevel >= 0));
131
+ return this.batteryLevel !== null;
135
132
  }
136
133
  get hasLowBattery() {
137
134
  return this.data.alerts.battery === 'low';
@@ -234,20 +231,37 @@ class RingCamera extends subscribed_1.Subscribed {
234
231
  }
235
232
  startLiveCall() {
236
233
  return __awaiter(this, void 0, void 0, function* () {
237
- const auth = yield this.restClient.getCurrentAuth();
238
- return new live_call_1.LiveCall(auth.access_token, this);
234
+ if (this.isRingEdgeEnabled) {
235
+ const auth = yield this.restClient.getCurrentAuth();
236
+ return new live_call_ring_edge_1.LiveCallRingEdge(auth.access_token, this);
237
+ }
238
+ const liveCall = yield this.restClient
239
+ .request({
240
+ method: 'POST',
241
+ url: this.doorbotUrl('live_call'),
242
+ })
243
+ .catch((e) => {
244
+ var _a;
245
+ if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.statusCode) === 403) {
246
+ const errorMessage = `Camera ${this.name} returned 403 when starting a live stream. This usually indicates that live streaming is blocked by Modes settings. Check your Ring app and verify that you are able to stream from this camera with the current Modes settings.`;
247
+ (0, util_1.logError)(errorMessage);
248
+ throw new Error(errorMessage);
249
+ }
250
+ throw e;
251
+ });
252
+ return new live_call_1.LiveCall(liveCall.data.session_id, this);
239
253
  });
240
254
  }
241
255
  removeDingById(idToRemove) {
242
- const allActiveDings = this.activeDings, otherDings = allActiveDings.filter((ding) => ding.id_str !== idToRemove);
243
- this.onActiveDings.next(otherDings);
256
+ const allActiveDings = this.activeNotifications, otherDings = allActiveDings.filter(({ ding }) => ding.id !== idToRemove);
257
+ this.onActiveNotifications.next(otherDings);
244
258
  }
245
- processActiveDing(ding) {
246
- const activeDings = this.activeDings, dingId = ding.id_str;
247
- this.onActiveDings.next(activeDings.filter((d) => d.id_str !== dingId).concat([ding]));
248
- this.onNewDing.next(ding);
259
+ processPushNotification(notification) {
260
+ const activeDings = this.activeNotifications, dingId = notification.ding.id;
261
+ this.onActiveNotifications.next(activeDings.filter((d) => d.ding.id !== dingId).concat([notification]));
262
+ this.onNewNotification.next(notification);
249
263
  setTimeout(() => {
250
- this.removeDingById(ding.id_str);
264
+ this.removeDingById(dingId);
251
265
  }, 65 * 1000); // dings last ~1 minute
252
266
  }
253
267
  getEvents(options = {}) {
@@ -319,17 +333,19 @@ class RingCamera extends subscribed_1.Subscribed {
319
333
  return false;
320
334
  }
321
335
  }
322
- getSnapshot() {
336
+ getSnapshot({ uuid } = {}) {
323
337
  return __awaiter(this, void 0, void 0, function* () {
324
338
  if (this.lastSnapshotPromise && this.shouldUseExistingSnapshotPromise()) {
325
339
  return this.lastSnapshotPromise;
326
340
  }
327
341
  this.checkIfSnapshotsAreBlocked();
328
342
  this.lastSnapshotPromise = Promise.race([
329
- this.getNextSnapshot({
330
- afterMs: this.lastSnapshotTimestamp,
331
- force: true,
332
- }),
343
+ this.getNextSnapshot(uuid
344
+ ? { uuid }
345
+ : {
346
+ afterMs: this.lastSnapshotTimestamp,
347
+ force: true,
348
+ }),
333
349
  (0, util_1.delay)(maxSnapshotRefreshSeconds * 1000).then(() => {
334
350
  const extraMessageForBatteryCam = this.operatingOnBattery
335
351
  ? '. This is normal behavior since this camera is unable to capture snapshots while streaming'
@@ -349,7 +365,7 @@ class RingCamera extends subscribed_1.Subscribed {
349
365
  return this.lastSnapshotPromise;
350
366
  });
351
367
  }
352
- getNextSnapshot({ afterMs, maxWaitMs, force, }) {
368
+ getNextSnapshot({ afterMs, maxWaitMs, force, uuid, }) {
353
369
  return __awaiter(this, void 0, void 0, function* () {
354
370
  const response = yield this.restClient.request({
355
371
  url: `https://app-snaps.ring.com/snapshots/next/${this.id}`,
@@ -358,6 +374,7 @@ class RingCamera extends subscribed_1.Subscribed {
358
374
  'after-ms': afterMs,
359
375
  'max-wait-ms': maxWaitMs,
360
376
  extras: force ? 'force' : undefined,
377
+ uuid,
361
378
  },
362
379
  headers: {
363
380
  accept: 'image/jpeg',
@@ -368,6 +385,15 @@ class RingCamera extends subscribed_1.Subscribed {
368
385
  return response;
369
386
  });
370
387
  }
388
+ getSnapshotByUuid(uuid) {
389
+ return this.restClient.request({
390
+ url: (0, rest_client_1.clientApi)('snapshots/uuid?uuid=' + uuid),
391
+ responseType: 'buffer',
392
+ headers: {
393
+ accept: 'image/jpeg',
394
+ },
395
+ });
396
+ }
371
397
  recordToFile(outputPath, duration = 30) {
372
398
  return __awaiter(this, void 0, void 0, function* () {
373
399
  const liveCall = yield this.streamVideo({