ring-client-api 10.0.0-beta.4 → 10.0.0-beta.7

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,49 @@
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.7](https://github.com/dgreif/ring/compare/v10.0.0-beta.6...v10.0.0-beta.7) (2022-05-07)
6
+
7
+
8
+ ### ⚠ BREAKING CHANGES
9
+
10
+ * The `sendDoorbellMotionNotificationsToTv` and `sendCameraMotionNotificationsToTv` options have been removed. These options were originally inteded as a workaround for the fact that tvOS would only automatically start streaming for doorbell presses and not for motion events. tvOS 15 now supports this features as an option in the UI, so we no longer need this option at the plugin level.
11
+
12
+ ### Features
13
+
14
+ * add `cocoa_floodlight` ([c7a7e9c](https://github.com/dgreif/ring/commit/c7a7e9cabf45ec6579a7d9a1033fa01930923243)), closes [#893](https://github.com/dgreif/ring/issues/893)
15
+ * **api:** simple webrtc session ([bac7a49](https://github.com/dgreif/ring/commit/bac7a49178bb5a22914e53fda91c0fdd522b4a74))
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **homebridge:** add `name` option to schema so that it won't be removed by config-ui ([af6755a](https://github.com/dgreif/ring/commit/af6755aa09cf4aaad0963499ad0d1afd0beeab96)), closes [#870](https://github.com/dgreif/ring/issues/870)
21
+ * include device name when setting push token ([2466a25](https://github.com/dgreif/ring/commit/2466a2519341771d8c79557b5a61c339f31eddf6)), closes [#920](https://github.com/dgreif/ring/issues/920)
22
+ * npm audit fix ([c1fe605](https://github.com/dgreif/ring/commit/c1fe6051e46dac92b00bb2fd1202746fd576b15a))
23
+ * remove doorbell press simulation options ([e1d1225](https://github.com/dgreif/ring/commit/e1d12257b2acbdafdc6995a53d9052822c8a3460))
24
+
25
+ ## [10.0.0-beta.6](https://github.com/dgreif/ring/compare/v10.0.0-beta.5...v10.0.0-beta.6) (2022-05-06)
26
+
27
+
28
+ ### Features
29
+
30
+ * **api:** allow custom peer connections for streaming ([3e9e784](https://github.com/dgreif/ring/commit/3e9e78458afe2849a8c414a6b50eec8718bf77cc))
31
+
32
+
33
+ ### Bug Fixes
34
+
35
+ * update dependencies ([4b25595](https://github.com/dgreif/ring/commit/4b255950bcfbf94486192d55cfc90d966cd79d24))
36
+
37
+ ## [10.0.0-beta.5](https://github.com/dgreif/ring/compare/v10.0.0-beta.4...v10.0.0-beta.5) (2022-04-10)
38
+
39
+
40
+ ### Bug Fixes
41
+
42
+ * **cameras:** wait to activate session until connected ([9a49389](https://github.com/dgreif/ring/commit/9a49389aa434cc5df4cfb4ba039bad0f2a30f756))
43
+ * handle push notification for unknown camera ([cc8fc2a](https://github.com/dgreif/ring/commit/cc8fc2a6377084515d3b169a85644b2a2b13235e))
44
+ * hide ECONNRESET errors from push receiver ([1196d96](https://github.com/dgreif/ring/commit/1196d96e67e26952f18dfbe75103419a7261bd1e))
45
+ * restore dual battery level calculations ([26fa84d](https://github.com/dgreif/ring/commit/26fa84dbd4bbcea8208fd0ade81c54c407aabf63))
46
+ * update dependencies ([4b97fb3](https://github.com/dgreif/ring/commit/4b97fb33623248147997944bab3de4ea3c2e5ee2))
47
+
5
48
  ## [10.0.0-beta.4](https://github.com/dgreif/ring/compare/v10.0.0-beta.3...v10.0.0-beta.4) (2022-04-03)
6
49
 
7
50
 
package/lib/api/api.js CHANGED
@@ -112,9 +112,7 @@ class RingApi extends subscribed_1.Subscribed {
112
112
  method: 'PATCH',
113
113
  json: {
114
114
  device: {
115
- metadata: {
116
- pn_service: 'fcm',
117
- },
115
+ metadata: Object.assign(Object.assign({}, this.restClient.baseSessionMetadata), { pn_service: 'fcm' }),
118
116
  os: 'android',
119
117
  push_notification_token: token,
120
118
  },
@@ -126,7 +124,7 @@ class RingApi extends subscribed_1.Subscribed {
126
124
  }
127
125
  }));
128
126
  pushReceiver.onNotification(({ message }) => {
129
- var _a;
127
+ var _a, _b;
130
128
  const dataJson = (_a = message.data) === null || _a === void 0 ? void 0 : _a.gcmData;
131
129
  try {
132
130
  const notification = JSON.parse(dataJson);
@@ -134,7 +132,7 @@ class RingApi extends subscribed_1.Subscribed {
134
132
  // We may get notification that are not for a ding/motion, which can be ignored
135
133
  return;
136
134
  }
137
- camerasById[notification.ding.doorbot_id].processPushNotification(notification);
135
+ (_b = camerasById[notification.ding.doorbot_id]) === null || _b === void 0 ? void 0 : _b.processPushNotification(notification);
138
136
  }
139
137
  catch (e) {
140
138
  (0, util_1.logError)(e);
package/lib/api/index.js CHANGED
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
3
  if (k2 === undefined) k2 = k;
4
- Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
5
9
  }) : (function(o, m, k, k2) {
6
10
  if (k2 === undefined) k2 = k;
7
11
  o[k2] = m[k];
@@ -35,6 +35,10 @@ export declare class RingRestClient {
35
35
  oldRefreshToken?: string | undefined;
36
36
  newRefreshToken: string;
37
37
  }>;
38
+ readonly baseSessionMetadata: {
39
+ api_version: number;
40
+ device_model: string;
41
+ };
38
42
  constructor(authOptions: (EmailAuth | RefreshTokenAuth) & SessionOptions);
39
43
  private getGrantData;
40
44
  getAuth(twoFactorAuthCode?: string): Promise<AuthTokenResponse>;
@@ -44,6 +48,7 @@ export declare class RingRestClient {
44
48
  private refreshSession;
45
49
  request<T = void>(options: RequestOptions & {
46
50
  url: string;
51
+ allowNoResponse?: boolean;
47
52
  }): Promise<T & ExtendedResponse>;
48
53
  getCurrentAuth(): Promise<AuthTokenResponse>;
49
54
  clearTimeouts(): void;
@@ -55,7 +55,7 @@ function requestWithRetry(requestOptions) {
55
55
  return data;
56
56
  }
57
57
  catch (e) {
58
- if (!e.response) {
58
+ if (!e.response && !requestOptions.allowNoResponse) {
59
59
  (0, util_1.logError)(`Failed to reach Ring server at ${requestOptions.url}. ${e.message}. Trying again in 5 seconds...`);
60
60
  if (e.message.includes('NGHTTP2_ENHANCE_YOUR_CALM')) {
61
61
  (0, util_1.logError)(`There is a known issue with your current NodeJS version (${process.version}). Please see https://github.com/dgreif/ring/wiki/NGHTTP2_ENHANCE_YOUR_CALM-Error for details`);
@@ -70,11 +70,16 @@ function requestWithRetry(requestOptions) {
70
70
  }
71
71
  class RingRestClient {
72
72
  constructor(authOptions) {
73
+ var _a;
73
74
  this.authOptions = authOptions;
74
75
  this.timeouts = [];
75
76
  this.sessionPromise = undefined;
76
77
  this.using2fa = false;
77
78
  this.onRefreshTokenUpdated = new rxjs_1.ReplaySubject(1);
79
+ this.baseSessionMetadata = {
80
+ api_version: apiVersion,
81
+ device_model: (_a = this.authOptions.controlCenterDisplayName) !== null && _a !== void 0 ? _a : 'ring-client-api',
82
+ };
78
83
  this.refreshToken =
79
84
  'refreshToken' in this.authOptions
80
85
  ? this.authOptions.refreshToken
@@ -184,17 +189,13 @@ class RingRestClient {
184
189
  });
185
190
  }
186
191
  fetchNewSession(authToken) {
187
- var _a;
188
192
  return __awaiter(this, void 0, void 0, function* () {
189
193
  return requestWithRetry({
190
194
  url: clientApi('session'),
191
195
  json: {
192
196
  device: {
193
197
  hardware_id: yield this.hardwareIdPromise,
194
- metadata: {
195
- api_version: apiVersion,
196
- device_model: (_a = this.authOptions.controlCenterDisplayName) !== null && _a !== void 0 ? _a : 'ring-client-api',
197
- },
198
+ metadata: this.baseSessionMetadata,
198
199
  os: 'android', // can use android, ios, ring-site, windows for sure
199
200
  },
200
201
  },
@@ -4,9 +4,12 @@ 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
- import { FfmpegOptions, LiveCall } from './live-call';
8
- import { LiveCallRingEdge } from './live-call-ring-edge';
9
- export declare function getBatteryLevel({ health }: Pick<CameraData, 'health'>): number | null;
7
+ import { FfmpegOptions, StreamingSession } from './streaming/streaming-session';
8
+ import { StreamingConnectionOptions } from './streaming/streaming-connection-base';
9
+ import { SimpleWebRtcSession } from './streaming/simple-webrtc-session';
10
+ export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'> & {
11
+ health?: Partial<CameraData['health']>;
12
+ }): number | null;
10
13
  export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
11
14
  accountId: string;
12
15
  })): string;
@@ -53,7 +56,8 @@ export declare class RingCamera extends Subscribed {
53
56
  getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
54
57
  setInHomeDoorbell(enable: boolean): Promise<boolean>;
55
58
  getHealth(): Promise<CameraHealth>;
56
- startLiveCall(): Promise<LiveCall | LiveCallRingEdge>;
59
+ private createStreamingConnection;
60
+ startLiveCall(options?: StreamingConnectionOptions): Promise<StreamingSession>;
57
61
  private removeDingById;
58
62
  processPushNotification(notification: PushNotification): void;
59
63
  getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
@@ -91,7 +95,14 @@ export declare class RingCamera extends Subscribed {
91
95
  }): Promise<Buffer & import("./rest-client").ExtendedResponse>;
92
96
  getSnapshotByUuid(uuid: string): Promise<Buffer & import("./rest-client").ExtendedResponse>;
93
97
  recordToFile(outputPath: string, duration?: number): Promise<void>;
94
- streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall | LiveCallRingEdge>;
98
+ streamVideo(ffmpegOptions: FfmpegOptions): Promise<StreamingSession>;
99
+ /**
100
+ * Returns a SimpleWebRtcSession, which can be initiated with an sdp offer.
101
+ * This session has no backplane for trickle ICE, and is designed for use in a
102
+ * browser setting. Note, cameras with Ring Edge enabled will stream with the speaker
103
+ * enabled as soon as the stream starts, which can drain the battery more quickly.
104
+ */
105
+ createSimpleWebRtcSession(): SimpleWebRtcSession;
95
106
  subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
96
107
  unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
97
108
  subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
@@ -16,20 +16,39 @@ const rxjs_1 = require("rxjs");
16
16
  const operators_1 = require("rxjs/operators");
17
17
  const util_1 = require("./util");
18
18
  const subscribed_1 = require("./subscribed");
19
- const live_call_1 = require("./live-call");
20
- const live_call_ring_edge_1 = require("./live-call-ring-edge");
19
+ const webrtc_connection_1 = require("./streaming/webrtc-connection");
20
+ const ring_edge_connection_1 = require("./streaming/ring-edge-connection");
21
+ const streaming_session_1 = require("./streaming/streaming-session");
22
+ const simple_webrtc_session_1 = require("./streaming/simple-webrtc-session");
21
23
  const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
24
+ function parseBatteryLife(batteryLife) {
25
+ if (batteryLife === null || batteryLife === undefined) {
26
+ return null;
27
+ }
28
+ const batteryLevel = typeof batteryLife === 'number'
29
+ ? batteryLife
30
+ : Number.parseFloat(batteryLife);
31
+ if (isNaN(batteryLevel)) {
32
+ return null;
33
+ }
34
+ return batteryLevel;
35
+ }
22
36
  function getStartOfToday() {
23
37
  return new Date(new Date().toLocaleDateString()).getTime();
24
38
  }
25
39
  function getEndOfToday() {
26
40
  return getStartOfToday() + fullDayMs - 1;
27
41
  }
28
- function getBatteryLevel({ health }) {
29
- if (!health.battery_percentage && !health.battery_present) {
42
+ function getBatteryLevel(data) {
43
+ const levels = [
44
+ parseBatteryLife(data.battery_life),
45
+ parseBatteryLife(data.battery_life_2),
46
+ ].filter((level) => level !== null), { health } = data;
47
+ if (!levels.length ||
48
+ (health && !health.battery_percentage && !health.battery_present)) {
30
49
  return null;
31
50
  }
32
- return health.battery_percentage || 0;
51
+ return Math.min(...levels);
33
52
  }
34
53
  exports.getBatteryLevel = getBatteryLevel;
35
54
  function getSearchQueryString(options) {
@@ -117,7 +136,7 @@ class RingCamera extends subscribed_1.Subscribed {
117
136
  return this.data.alerts.battery === 'low';
118
137
  }
119
138
  get isCharging() {
120
- return Boolean(this.data.health.ac_power || this.data.health.ext_power_state);
139
+ return this.initialData.external_connection;
121
140
  }
122
141
  get operatingOnBattery() {
123
142
  return this.hasBattery && this.data.settings.power_mode !== 'wired';
@@ -212,11 +231,11 @@ class RingCamera extends subscribed_1.Subscribed {
212
231
  return response.device_health;
213
232
  });
214
233
  }
215
- startLiveCall() {
234
+ createStreamingConnection(options) {
216
235
  return __awaiter(this, void 0, void 0, function* () {
217
236
  if (this.isRingEdgeEnabled) {
218
237
  const auth = yield this.restClient.getCurrentAuth();
219
- return new live_call_ring_edge_1.LiveCallRingEdge(auth.access_token, this);
238
+ return new ring_edge_connection_1.RingEdgeConnection(auth.access_token, this, options);
220
239
  }
221
240
  const liveCall = yield this.restClient
222
241
  .request({
@@ -232,7 +251,13 @@ class RingCamera extends subscribed_1.Subscribed {
232
251
  }
233
252
  throw e;
234
253
  });
235
- return new live_call_1.LiveCall(liveCall.data.session_id, this);
254
+ return new webrtc_connection_1.WebrtcConnection(liveCall.data.session_id, this, options);
255
+ });
256
+ }
257
+ startLiveCall(options = {}) {
258
+ return __awaiter(this, void 0, void 0, function* () {
259
+ const connection = yield this.createStreamingConnection(options);
260
+ return new streaming_session_1.StreamingSession(this, connection);
236
261
  });
237
262
  }
238
263
  removeDingById(idToRemove) {
@@ -392,6 +417,15 @@ class RingCamera extends subscribed_1.Subscribed {
392
417
  return liveCall;
393
418
  });
394
419
  }
420
+ /**
421
+ * Returns a SimpleWebRtcSession, which can be initiated with an sdp offer.
422
+ * This session has no backplane for trickle ICE, and is designed for use in a
423
+ * browser setting. Note, cameras with Ring Edge enabled will stream with the speaker
424
+ * enabled as soon as the stream starts, which can drain the battery more quickly.
425
+ */
426
+ createSimpleWebRtcSession() {
427
+ return new simple_webrtc_session_1.SimpleWebRtcSession(this, this.restClient);
428
+ }
395
429
  subscribeToDingEvents() {
396
430
  return this.restClient.request({
397
431
  method: 'POST',
@@ -85,6 +85,7 @@ export declare enum RingCameraKind {
85
85
  floodlight_pro = "floodlight_pro",
86
86
  cocoa_camera = "cocoa_camera",
87
87
  cocoa_doorbell = "cocoa_doorbell",
88
+ cocoa_floodlight = "cocoa_floodlight",
88
89
  stickup_cam_mini = "stickup_cam_mini"
89
90
  }
90
91
  export declare const RingCameraModel: {
@@ -93,6 +93,7 @@ var RingCameraKind;
93
93
  RingCameraKind["floodlight_pro"] = "floodlight_pro";
94
94
  RingCameraKind["cocoa_camera"] = "cocoa_camera";
95
95
  RingCameraKind["cocoa_doorbell"] = "cocoa_doorbell";
96
+ RingCameraKind["cocoa_floodlight"] = "cocoa_floodlight";
96
97
  RingCameraKind["stickup_cam_mini"] = "stickup_cam_mini";
97
98
  })(RingCameraKind = exports.RingCameraKind || (exports.RingCameraKind = {}));
98
99
  exports.RingCameraModel = {
@@ -121,6 +122,7 @@ exports.RingCameraModel = {
121
122
  floodlight_pro: 'Floodlight Pro',
122
123
  cocoa_camera: 'Stick Up Cam',
123
124
  cocoa_doorbell: 'Doorbell Gen 2',
125
+ cocoa_floodlight: 'Floodlight Cam Plus',
124
126
  stickup_cam_mini: 'Indoor Cam',
125
127
  };
126
128
  exports.allAlarmStates = [
@@ -1,7 +1,24 @@
1
- import { MediaStreamTrack, RTCIceCandidate, RTCPeerConnection, RtcpPacket, RtpPacket } from '@koush/werift';
2
- import { ReplaySubject, Subject } from 'rxjs';
3
- export declare class PeerConnection {
4
- pc: RTCPeerConnection;
1
+ import { ConnectionState, MediaStreamTrack, RTCIceCandidate, RtcpPacket, RtpPacket } from '@koush/werift';
2
+ import { Observable, ReplaySubject, Subject } from 'rxjs';
3
+ export interface BasicPeerConnection {
4
+ createOffer(): Promise<{
5
+ sdp: string;
6
+ }>;
7
+ createAnswer(offer: {
8
+ type: 'offer';
9
+ sdp: string;
10
+ }): Promise<RTCSessionDescriptionInit>;
11
+ acceptAnswer(answer: {
12
+ type: 'answer';
13
+ sdp: string;
14
+ }): Promise<void>;
15
+ addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
16
+ onIceCandidate: Observable<RTCIceCandidate>;
17
+ onConnectionState: Observable<ConnectionState>;
18
+ close(): void;
19
+ }
20
+ export declare class WeriftPeerConnection implements BasicPeerConnection {
21
+ private pc;
5
22
  onAudioRtp: Subject<RtpPacket>;
6
23
  onAudioRtcp: Subject<RtcpPacket>;
7
24
  onVideoRtp: Subject<RtpPacket>;
@@ -9,10 +9,10 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.PeerConnection = void 0;
12
+ exports.WeriftPeerConnection = void 0;
13
13
  const werift_1 = require("@koush/werift");
14
14
  const rxjs_1 = require("rxjs");
15
- const util_1 = require("./util");
15
+ const util_1 = require("../util");
16
16
  const debug = false, ringIceServers = [
17
17
  'stun:stun.kinesisvideo.us-east-1.amazonaws.com:443',
18
18
  'stun:stun.kinesisvideo.us-east-2.amazonaws.com:443',
@@ -23,7 +23,7 @@ const debug = false, ringIceServers = [
23
23
  'stun:stun3.l.google.com:19302',
24
24
  'stun:stun4.l.google.com:19302',
25
25
  ];
26
- class PeerConnection {
26
+ class WeriftPeerConnection {
27
27
  constructor() {
28
28
  this.onAudioRtp = new rxjs_1.Subject();
29
29
  this.onAudioRtcp = new rxjs_1.Subject();
@@ -131,4 +131,4 @@ class PeerConnection {
131
131
  this.pc.close().catch(util_1.logError);
132
132
  }
133
133
  }
134
- exports.PeerConnection = PeerConnection;
134
+ exports.WeriftPeerConnection = WeriftPeerConnection;
@@ -0,0 +1,65 @@
1
+ import { RingCamera } from '../ring-camera';
2
+ import { StreamingConnectionBase, StreamingConnectionOptions } from './streaming-connection-base';
3
+ interface SessionBody {
4
+ doorbot_id: number;
5
+ session_id: string;
6
+ }
7
+ interface AnswerMessage {
8
+ method: 'sdp';
9
+ body: {
10
+ sdp: string;
11
+ type: 'answer';
12
+ } & SessionBody;
13
+ }
14
+ interface IceCandidateMessage {
15
+ method: 'ice';
16
+ body: {
17
+ ice: string;
18
+ mlineindex: number;
19
+ } & SessionBody;
20
+ }
21
+ interface SessionCreatedMessage {
22
+ method: 'session_created';
23
+ body: SessionBody;
24
+ }
25
+ interface SessionStartedMessage {
26
+ method: 'session_started';
27
+ body: SessionBody;
28
+ }
29
+ interface PongMessage {
30
+ method: 'pong';
31
+ body: SessionBody;
32
+ }
33
+ interface NotificationMessage {
34
+ method: 'notification';
35
+ body: {
36
+ is_ok: boolean;
37
+ text: string;
38
+ } & SessionBody;
39
+ }
40
+ declare enum CloseReasonCode {
41
+ NormalClose = 0,
42
+ AuthenticationFailed = 5,
43
+ Timeout = 6
44
+ }
45
+ interface CloseMessage {
46
+ method: 'close';
47
+ body: {
48
+ reason: {
49
+ code: CloseReasonCode;
50
+ text: string;
51
+ };
52
+ } & SessionBody;
53
+ }
54
+ declare type IncomingMessage = AnswerMessage | IceCandidateMessage | SessionCreatedMessage | SessionStartedMessage | PongMessage | CloseMessage | NotificationMessage;
55
+ export declare class RingEdgeConnection extends StreamingConnectionBase {
56
+ private camera;
57
+ private readonly onSessionId;
58
+ private readonly onOfferSent;
59
+ constructor(authToken: string, camera: RingCamera, options: StreamingConnectionOptions);
60
+ private initiateCall;
61
+ private sessionId;
62
+ protected handleMessage(message: IncomingMessage): Promise<void>;
63
+ protected sendSessionMessage(method: string, body?: Record<any, any>): void;
64
+ }
65
+ export {};
@@ -0,0 +1,155 @@
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.RingEdgeConnection = void 0;
13
+ const ws_1 = require("ws");
14
+ const rxjs_1 = require("rxjs");
15
+ const util_1 = require("../util");
16
+ const operators_1 = require("rxjs/operators");
17
+ const streaming_connection_base_1 = require("./streaming-connection-base");
18
+ // eslint-disable-next-line no-shadow
19
+ var CloseReasonCode;
20
+ (function (CloseReasonCode) {
21
+ CloseReasonCode[CloseReasonCode["NormalClose"] = 0] = "NormalClose";
22
+ // 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' }
23
+ // reason: { code: 5, text: 'Authentication failed: -1' }
24
+ // reason: { code: 5, text: 'Sessions with the provided ID not found' }
25
+ CloseReasonCode[CloseReasonCode["AuthenticationFailed"] = 5] = "AuthenticationFailed";
26
+ // reason: { code: 6, text: 'Timeout waiting for ping' }
27
+ CloseReasonCode[CloseReasonCode["Timeout"] = 6] = "Timeout";
28
+ })(CloseReasonCode || (CloseReasonCode = {}));
29
+ class RingEdgeConnection extends streaming_connection_base_1.StreamingConnectionBase {
30
+ constructor(authToken, camera, options) {
31
+ super(new ws_1.WebSocket('wss://api.prod.signalling.ring.devices.a2z.com:443/ws', {
32
+ headers: {
33
+ Authorization: `Bearer ${authToken}`,
34
+ 'X-Sig-API-Version': '4.0',
35
+ 'X-Sig-Client-ID': 'ring_android-aabb123',
36
+ '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',
37
+ 'X-Sig-Auth-Type': 'ring_oauth',
38
+ },
39
+ }), options);
40
+ this.camera = camera;
41
+ this.onSessionId = new rxjs_1.ReplaySubject(1);
42
+ this.onOfferSent = new rxjs_1.ReplaySubject(1);
43
+ this.sessionId = null;
44
+ this.addSubscriptions(this.onWsOpen.subscribe(() => {
45
+ (0, util_1.logDebug)(`WebSocket connected for ${camera.name} (Ring Edge)`);
46
+ this.initiateCall().catch((e) => {
47
+ (0, util_1.logError)(e);
48
+ this.callEnded();
49
+ });
50
+ }),
51
+ // The ring-edge session needs a ping every 5 seconds to keep the connection alive
52
+ this.onCallAnswered
53
+ .pipe((0, operators_1.switchMap)(() => (0, rxjs_1.interval)(5000)))
54
+ .subscribe(() => {
55
+ if (!this.sessionId) {
56
+ return;
57
+ }
58
+ this.sendSessionMessage('ping');
59
+ }), this.pc.onIceCandidate.subscribe((iceCandidate) => __awaiter(this, void 0, void 0, function* () {
60
+ yield (0, rxjs_1.firstValueFrom)(this.onOfferSent);
61
+ this.sendMessage({
62
+ method: 'ice',
63
+ body: {
64
+ doorbot_id: camera.id,
65
+ ice: iceCandidate.candidate,
66
+ mlineindex: 0,
67
+ },
68
+ });
69
+ // HACK: send ice candidate with both mline indexes to convince ring edge to connect both audio and video
70
+ // Without this, only audio will connect unless you connect from the network of the Ring Edge router
71
+ this.sendMessage({
72
+ method: 'ice',
73
+ body: {
74
+ doorbot_id: camera.id,
75
+ ice: iceCandidate.candidate,
76
+ mlineindex: 1,
77
+ },
78
+ });
79
+ })));
80
+ }
81
+ initiateCall() {
82
+ return __awaiter(this, void 0, void 0, function* () {
83
+ const { sdp } = yield this.pc.createOffer();
84
+ this.sendMessage({
85
+ method: 'live_view',
86
+ body: {
87
+ doorbot_id: this.camera.id,
88
+ stream_options: { audio_enabled: true, video_enabled: true },
89
+ sdp: sdp.replace('\na=group:BUNDLE 0 1', ''),
90
+ },
91
+ });
92
+ this.onOfferSent.next();
93
+ });
94
+ }
95
+ handleMessage(message) {
96
+ return __awaiter(this, void 0, void 0, function* () {
97
+ if (message.body.doorbot_id !== this.camera.id) {
98
+ // ignore messages for other cameras
99
+ return;
100
+ }
101
+ if (['session_created', 'session_started'].includes(message.method) &&
102
+ 'session_id' in message.body &&
103
+ !this.sessionId) {
104
+ this.sessionId = message.body.session_id;
105
+ this.onSessionId.next(this.sessionId);
106
+ }
107
+ if (message.body.session_id && message.body.session_id !== this.sessionId) {
108
+ // ignore messages for other sessions
109
+ return;
110
+ }
111
+ switch (message.method) {
112
+ case 'session_created':
113
+ case 'session_started':
114
+ // session already stored above
115
+ return;
116
+ case 'sdp':
117
+ yield this.pc.acceptAnswer(message.body);
118
+ this.onCallAnswered.next(message.body.sdp);
119
+ return;
120
+ case 'ice':
121
+ yield this.pc.addIceCandidate({
122
+ candidate: message.body.ice,
123
+ sdpMLineIndex: message.body.mlineindex,
124
+ });
125
+ return;
126
+ case 'pong':
127
+ return;
128
+ case 'close':
129
+ (0, util_1.logError)('Video stream closed');
130
+ (0, util_1.logError)(message.body);
131
+ this.callEnded();
132
+ return;
133
+ }
134
+ (0, util_1.logError)('UNKNOWN MESSAGE');
135
+ (0, util_1.logError)(message);
136
+ });
137
+ }
138
+ sendSessionMessage(method, body = {}) {
139
+ const message = {
140
+ method,
141
+ body: Object.assign(Object.assign({}, body), { doorbot_id: this.camera.id, session_id: this.sessionId }),
142
+ };
143
+ if (this.sessionId) {
144
+ // Send immediately if we already have a session id
145
+ // This is needed to send `close` before closing the websocket
146
+ this.sendMessage(message);
147
+ }
148
+ else {
149
+ (0, rxjs_1.firstValueFrom)(this.onSessionId)
150
+ .then(() => this.sendMessage(message))
151
+ .catch((e) => (0, util_1.logError)(e));
152
+ }
153
+ }
154
+ }
155
+ exports.RingEdgeConnection = RingEdgeConnection;
@@ -0,0 +1,11 @@
1
+ import { RingCamera } from '../ring-camera';
2
+ import { RingRestClient } from '../rest-client';
3
+ export declare class SimpleWebRtcSession {
4
+ private readonly camera;
5
+ private restClient;
6
+ readonly sessionId: string;
7
+ constructor(camera: RingCamera, restClient: RingRestClient);
8
+ start(sdp: string): Promise<string>;
9
+ end(): Promise<any>;
10
+ activateCameraSpeaker(): Promise<void>;
11
+ }