ring-client-api 11.8.0 → 12.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.
@@ -4,8 +4,8 @@ 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 { StreamingConnectionOptions } from './streaming/webrtc-connection';
7
8
  import { FfmpegOptions, StreamingSession } from './streaming/streaming-session';
8
- import { StreamingConnectionOptions } from './streaming/streaming-connection-base';
9
9
  import { SimpleWebRtcSession } from './streaming/simple-webrtc-session';
10
10
  export type AnyCameraData = CameraData | OnvifCameraData;
11
11
  export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'> & {
@@ -8,7 +8,6 @@ const operators_1 = require("rxjs/operators");
8
8
  const util_1 = require("./util");
9
9
  const subscribed_1 = require("./subscribed");
10
10
  const webrtc_connection_1 = require("./streaming/webrtc-connection");
11
- const ring_edge_connection_1 = require("./streaming/ring-edge-connection");
12
11
  const streaming_session_1 = require("./streaming/streaming-session");
13
12
  const simple_webrtc_session_1 = require("./streaming/simple-webrtc-session");
14
13
  const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
@@ -239,24 +238,15 @@ class RingCamera extends subscribed_1.Subscribed {
239
238
  return response.device_health;
240
239
  }
241
240
  async createStreamingConnection(options) {
242
- if (this.isRingEdgeEnabled) {
243
- const auth = await this.restClient.getCurrentAuth();
244
- return new ring_edge_connection_1.RingEdgeConnection(auth.access_token, this, options);
245
- }
246
- const liveCall = await this.restClient
241
+ const response = await this.restClient
247
242
  .request({
248
243
  method: 'POST',
249
- url: this.doorbotUrl('live_call'),
244
+ url: (0, rest_client_1.appApi)('clap/ticket/request/signalsocket'),
250
245
  })
251
246
  .catch((e) => {
252
- if (e.response?.statusCode === 403) {
253
- 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.`;
254
- (0, util_1.logError)(errorMessage);
255
- throw new Error(errorMessage);
256
- }
257
247
  throw e;
258
248
  });
259
- return new webrtc_connection_1.WebrtcConnection(liveCall.data.session_id, this, options);
249
+ return new webrtc_connection_1.WebrtcConnection(response.ticket, this, options);
260
250
  }
261
251
  async startLiveCall(options = {}) {
262
252
  const connection = await this.createStreamingConnection(options);
@@ -919,20 +919,9 @@ export interface PushNotificationLowBattery {
919
919
  action: PushNotificationAction.LowBattery;
920
920
  }
921
921
  export type PushNotification = PushNotificationDing | PushNotificationAlarm | PushNotificationLowBattery;
922
- export interface LiveCallResponse {
923
- ding_id: string;
924
- ding_kind: DingKind;
925
- device_id: number;
926
- device_kind: RingCameraKind;
927
- device_description: string;
928
- state: DingState;
929
- protocol: 'ring_media_server' | unknown;
930
- now: number;
931
- video_jitter_buffer_ms: number;
932
- audio_jitter_buffer_ms: number;
933
- data: {
934
- session_id: string;
935
- };
922
+ export interface SocketTicketResponse {
923
+ ticket: string;
924
+ responseTimestampe: number;
936
925
  }
937
926
  export interface AuthTokenResponse {
938
927
  access_token: string;
@@ -5,10 +5,6 @@ export interface BasicPeerConnection {
5
5
  createOffer(): Promise<{
6
6
  sdp: string;
7
7
  }>;
8
- createAnswer(offer: {
9
- type: 'offer';
10
- sdp: string;
11
- }): Promise<RTCSessionDescriptionInit>;
12
8
  acceptAnswer(answer: {
13
9
  type: 'answer';
14
10
  sdp: string;
@@ -31,10 +27,6 @@ export declare class WeriftPeerConnection extends Subscribed implements BasicPee
31
27
  private onRequestKeyFrame;
32
28
  constructor();
33
29
  createOffer(): Promise<import("werift").RTCSessionDescription>;
34
- createAnswer(offer: {
35
- type: 'offer';
36
- sdp: string;
37
- }): Promise<import("werift").RTCSessionDescription>;
38
30
  acceptAnswer(answer: {
39
31
  type: 'answer';
40
32
  sdp: string;
@@ -116,12 +116,6 @@ class WeriftPeerConnection extends subscribed_1.Subscribed {
116
116
  await this.pc.setLocalDescription(offer);
117
117
  return offer;
118
118
  }
119
- async createAnswer(offer) {
120
- await this.pc.setRemoteDescription(offer);
121
- const answer = await this.pc.createAnswer();
122
- await this.pc.setLocalDescription(answer);
123
- return answer;
124
- }
125
119
  async acceptAnswer(answer) {
126
120
  await this.pc.setRemoteDescription(answer);
127
121
  }
@@ -1,5 +1,3 @@
1
- import { RingCamera } from '../ring-camera';
2
- import { StreamingConnectionBase, StreamingConnectionOptions } from './streaming-connection-base';
3
1
  interface SessionBody {
4
2
  doorbot_id: number;
5
3
  session_id: string;
@@ -51,15 +49,5 @@ interface CloseMessage {
51
49
  };
52
50
  } & SessionBody;
53
51
  }
54
- 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
- }
52
+ export type IncomingMessage = AnswerMessage | IceCandidateMessage | SessionCreatedMessage | SessionStartedMessage | PongMessage | CloseMessage | NotificationMessage;
65
53
  export {};
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ // eslint-disable-next-line no-shadow
4
+ var CloseReasonCode;
5
+ (function (CloseReasonCode) {
6
+ CloseReasonCode[CloseReasonCode["NormalClose"] = 0] = "NormalClose";
7
+ // 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' }
8
+ // reason: { code: 5, text: 'Authentication failed: -1' }
9
+ // reason: { code: 5, text: 'Sessions with the provided ID not found' }
10
+ CloseReasonCode[CloseReasonCode["AuthenticationFailed"] = 5] = "AuthenticationFailed";
11
+ // reason: { code: 6, text: 'Timeout waiting for ping' }
12
+ CloseReasonCode[CloseReasonCode["Timeout"] = 6] = "Timeout";
13
+ })(CloseReasonCode || (CloseReasonCode = {}));
@@ -1,6 +1,5 @@
1
1
  import { RtpPacket } from 'werift';
2
2
  import { ReplaySubject, Subject } from 'rxjs';
3
- import { RingEdgeConnection } from './ring-edge-connection';
4
3
  import { WebrtcConnection } from './webrtc-connection';
5
4
  import { RingCamera } from '../ring-camera';
6
5
  import { Subscribed } from '../subscribed';
@@ -21,7 +20,7 @@ export declare class StreamingSession extends Subscribed {
21
20
  private readonly audioSplitter;
22
21
  private readonly videoSplitter;
23
22
  private readonly returnAudioSplitter;
24
- constructor(camera: RingCamera, connection: RingEdgeConnection | WebrtcConnection);
23
+ constructor(camera: RingCamera, connection: WebrtcConnection);
25
24
  private bindToConnection;
26
25
  /**
27
26
  * @deprecated
@@ -1,23 +1,38 @@
1
+ import { ReplaySubject, Subject } from 'rxjs';
1
2
  import { RingCamera } from '../ring-camera';
2
- import { StreamingConnectionBase, StreamingConnectionOptions } from './streaming-connection-base';
3
- interface InitializationMessage {
4
- method: 'initialization';
5
- text: 'Done';
3
+ import { BasicPeerConnection } from './peer-connection';
4
+ import { Subscribed } from '../subscribed';
5
+ import { RtpPacket } from 'werift';
6
+ export interface StreamingConnectionOptions {
7
+ createPeerConnection?: () => BasicPeerConnection;
6
8
  }
7
- interface OfferMessage {
8
- method: 'sdp';
9
- sdp: string;
10
- type: 'offer';
11
- }
12
- interface IceCandidateMessage {
13
- method: 'ice';
14
- ice: string;
15
- mlineindex: number;
16
- }
17
- export declare class WebrtcConnection extends StreamingConnectionBase {
9
+ export declare class WebrtcConnection extends Subscribed {
10
+ private camera;
11
+ private readonly onSessionId;
12
+ private readonly onOfferSent;
13
+ private readonly dialogId;
14
+ readonly onCallAnswered: ReplaySubject<string>;
15
+ readonly onCallEnded: ReplaySubject<void>;
16
+ readonly onError: ReplaySubject<void>;
17
+ readonly onMessage: ReplaySubject<{
18
+ method: string;
19
+ }>;
20
+ readonly onWsOpen: import("rxjs").Observable<unknown>;
21
+ readonly onAudioRtp: Subject<RtpPacket>;
22
+ readonly onVideoRtp: Subject<RtpPacket>;
23
+ private readonly pc;
24
+ private readonly ws;
25
+ constructor(ticket: string, camera: RingCamera, options: StreamingConnectionOptions);
26
+ private initiateCall;
18
27
  private sessionId;
19
- constructor(sessionId: string, camera: RingCamera, options: StreamingConnectionOptions);
20
- protected handleMessage(message: InitializationMessage | OfferMessage | IceCandidateMessage): Promise<void>;
21
- protected sendSessionMessage(method: string, body?: Record<any, any>): void;
28
+ private handleMessage;
29
+ private sendSessionMessage;
30
+ private sendMessage;
31
+ sendAudioPacket(rtp: RtpPacket): void;
32
+ private activate;
33
+ activateCameraSpeaker(): void;
34
+ private hasEnded;
35
+ private callEnded;
36
+ stop(): void;
37
+ requestKeyFrame(): void;
22
38
  }
23
- export {};
@@ -2,48 +2,244 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WebrtcConnection = void 0;
4
4
  const ws_1 = require("ws");
5
- const streaming_connection_base_1 = require("./streaming-connection-base");
5
+ const rxjs_1 = require("rxjs");
6
+ const operators_1 = require("rxjs/operators");
6
7
  const util_1 = require("../util");
7
- function parseLiveCallSession(sessionId) {
8
- const encodedSession = sessionId.split('.')[1], text = (0, util_1.fromBase64)(encodedSession);
9
- return JSON.parse(text);
10
- }
11
- class WebrtcConnection extends streaming_connection_base_1.StreamingConnectionBase {
12
- constructor(sessionId, camera, options) {
13
- const liveCallSession = parseLiveCallSession(sessionId);
14
- super(new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
8
+ const peer_connection_1 = require("./peer-connection");
9
+ const subscribed_1 = require("../subscribed");
10
+ class WebrtcConnection extends subscribed_1.Subscribed {
11
+ constructor(ticket, camera, options) {
12
+ super();
13
+ this.camera = camera;
14
+ this.onSessionId = new rxjs_1.ReplaySubject(1);
15
+ this.onOfferSent = new rxjs_1.ReplaySubject(1);
16
+ this.dialogId = (0, util_1.generateUuid)();
17
+ this.onCallAnswered = new rxjs_1.ReplaySubject(1);
18
+ this.onCallEnded = new rxjs_1.ReplaySubject(1);
19
+ this.onError = new rxjs_1.ReplaySubject(1);
20
+ this.onMessage = new rxjs_1.ReplaySubject();
21
+ this.sessionId = null;
22
+ this.hasEnded = false;
23
+ this.ws = new ws_1.WebSocket(`wss://api.prod.signalling.ring.devices.a2z.com:443/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-${(0, util_1.generateUuid)()}&token=${ticket}`, {
15
24
  headers: {
16
- API_VERSION: '3.1',
17
- API_TOKEN: sessionId,
18
- 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',
25
+ // This must exist or the socket will close immediately but content does not seem to matter
26
+ 'User-Agent': 'android:com.ringapp',
19
27
  },
20
- }), options);
21
- this.sessionId = sessionId;
22
- this.addSubscriptions(this.onWsOpen.subscribe(() => {
23
- (0, util_1.logDebug)(`WebSocket connected for ${camera.name}`);
28
+ });
29
+ if (options.createPeerConnection) {
30
+ // we were passed a custom peer connection factory
31
+ this.pc = options.createPeerConnection();
32
+ // passing rtp packets is not supported for custom peer connections
33
+ this.onAudioRtp = new rxjs_1.Subject();
34
+ this.onVideoRtp = new rxjs_1.Subject();
35
+ }
36
+ else {
37
+ // no custom peer connection factory, use the werift and pass along rtp packets
38
+ const pc = new peer_connection_1.WeriftPeerConnection();
39
+ this.pc = pc;
40
+ this.onAudioRtp = pc.onAudioRtp;
41
+ this.onVideoRtp = pc.onVideoRtp;
42
+ }
43
+ this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
44
+ const onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message'), onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
45
+ this.addSubscriptions(onMessage
46
+ .pipe((0, operators_1.concatMap)((event) => {
47
+ const message = JSON.parse(event.data);
48
+ this.onMessage.next(message);
49
+ return this.handleMessage(message).catch((e) => {
50
+ if (e instanceof Error &&
51
+ e.message.includes('negotiate codecs failed')) {
52
+ e = new Error('Failed to negotiate codecs. This is a known issue with Ring cameras. Please see https://github.com/dgreif/ring/wiki/Streaming-Legacy-Mode');
53
+ }
54
+ this.onError.next(e);
55
+ throw e;
56
+ });
57
+ }))
58
+ .subscribe(), onError.subscribe((e) => {
59
+ (0, util_1.logError)(e);
60
+ this.callEnded();
61
+ }), onClose.subscribe(() => {
62
+ this.callEnded();
63
+ }), this.pc.onConnectionState.subscribe((state) => {
64
+ if (state === 'failed') {
65
+ (0, util_1.logError)('Stream connection failed');
66
+ this.callEnded();
67
+ }
68
+ if (state === 'closed') {
69
+ (0, util_1.logDebug)('Stream connection closed');
70
+ this.callEnded();
71
+ }
72
+ }), this.onError.subscribe((e) => {
73
+ (0, util_1.logError)(e);
74
+ this.callEnded();
75
+ }), this.onWsOpen.subscribe(() => {
76
+ const connectionType = camera.isRingEdgeEnabled ? 'Ring Edge' : 'Cloud';
77
+ (0, util_1.logDebug)(`WebSocket connected for ${camera.name} (${connectionType})`);
78
+ this.initiateCall().catch((e) => {
79
+ (0, util_1.logError)(e);
80
+ this.callEnded();
81
+ });
82
+ }),
83
+ // The ring-edge session needs a ping every 5 seconds to keep the connection alive
84
+ (0, rxjs_1.interval)(5000).subscribe(() => {
85
+ this.sendSessionMessage('ping');
86
+ }), this.pc.onIceCandidate.subscribe(async (iceCandidate) => {
87
+ await (0, rxjs_1.firstValueFrom)(this.onOfferSent);
88
+ this.sendMessage({
89
+ method: 'ice',
90
+ dialog_id: this.dialogId,
91
+ body: {
92
+ doorbot_id: camera.id,
93
+ ice: iceCandidate.candidate,
94
+ mlineindex: iceCandidate.sdpMLineIndex,
95
+ },
96
+ });
24
97
  }));
25
98
  }
99
+ async initiateCall() {
100
+ const { sdp } = await this.pc.createOffer();
101
+ this.sendMessage({
102
+ method: 'live_view',
103
+ dialog_id: this.dialogId,
104
+ body: {
105
+ doorbot_id: this.camera.id,
106
+ stream_options: { audio_enabled: true, video_enabled: true },
107
+ sdp,
108
+ },
109
+ });
110
+ this.onOfferSent.next();
111
+ }
26
112
  async handleMessage(message) {
113
+ if (message.body.doorbot_id !== this.camera.id) {
114
+ // ignore messages for other cameras
115
+ return;
116
+ }
117
+ if (['session_created', 'session_started'].includes(message.method) &&
118
+ 'session_id' in message.body &&
119
+ !this.sessionId) {
120
+ this.sessionId = message.body.session_id;
121
+ this.onSessionId.next(this.sessionId);
122
+ }
123
+ if (message.body.session_id && message.body.session_id !== this.sessionId) {
124
+ // ignore messages for other sessions
125
+ return;
126
+ }
27
127
  switch (message.method) {
128
+ case 'session_created':
129
+ case 'session_started':
130
+ // session already stored above
131
+ return;
28
132
  case 'sdp':
29
- const answer = await this.pc.createAnswer(message);
30
- this.sendSessionMessage('sdp', answer);
31
- this.onCallAnswered.next(message.sdp);
133
+ await this.pc.acceptAnswer(message.body);
134
+ this.onCallAnswered.next(message.body.sdp);
32
135
  this.activate();
33
136
  return;
34
137
  case 'ice':
35
138
  await this.pc.addIceCandidate({
36
- candidate: message.ice,
37
- sdpMLineIndex: message.mlineindex,
139
+ candidate: message.body.ice,
140
+ sdpMLineIndex: message.body.mlineindex,
38
141
  });
39
142
  return;
143
+ case 'pong':
144
+ return;
145
+ case 'notification':
146
+ const { text } = message.body;
147
+ if (text === 'PeerConnectionState::kConnecting' ||
148
+ text === 'PeerConnectionState::kConnected' ||
149
+ text === 'camera_connected') {
150
+ return;
151
+ }
152
+ break;
153
+ case 'close':
154
+ (0, util_1.logError)('Video stream closed');
155
+ (0, util_1.logError)(message.body);
156
+ this.callEnded();
157
+ return;
40
158
  }
159
+ (0, util_1.logError)('UNKNOWN MESSAGE');
160
+ (0, util_1.logError)(message);
41
161
  }
42
162
  sendSessionMessage(method, body = {}) {
43
- this.sendMessage({
44
- ...body,
45
- method,
163
+ const sendSessionMessage = (sessionId) => {
164
+ const message = {
165
+ method,
166
+ dialog_id: this.dialogId,
167
+ body: {
168
+ ...body,
169
+ doorbot_id: this.camera.id,
170
+ session_id: sessionId,
171
+ },
172
+ };
173
+ this.sendMessage(message);
174
+ };
175
+ if (this.sessionId) {
176
+ // Send immediately if we already have a session id
177
+ // This is needed to send `close` before closing the websocket
178
+ sendSessionMessage(this.sessionId);
179
+ }
180
+ else {
181
+ // Otherwise wait for the session id to be set and then send the message
182
+ this.addSubscriptions(this.onSessionId.pipe((0, operators_1.take)(1)).subscribe(sendSessionMessage));
183
+ }
184
+ }
185
+ sendMessage(message) {
186
+ if (this.hasEnded) {
187
+ return;
188
+ }
189
+ this.ws.send(JSON.stringify(message));
190
+ }
191
+ sendAudioPacket(rtp) {
192
+ if (this.hasEnded) {
193
+ return;
194
+ }
195
+ if (this.pc instanceof peer_connection_1.WeriftPeerConnection) {
196
+ this.pc.returnAudioTrack.writeRtp(rtp);
197
+ }
198
+ else {
199
+ throw new Error('Cannot send audio packets to a custom peer connection implementation');
200
+ }
201
+ }
202
+ activate() {
203
+ (0, util_1.logInfo)('Activating Session');
204
+ // the activate_session message is required to keep the stream alive longer than 70 seconds
205
+ this.sendSessionMessage('activate_session');
206
+ this.sendSessionMessage('stream_options', {
207
+ audio_enabled: true,
208
+ video_enabled: true,
46
209
  });
47
210
  }
211
+ activateCameraSpeaker() {
212
+ // Fire and forget this call so that callers don't get hung up waiting for answer (which might not happen)
213
+ this.addSubscriptions(this.onCallAnswered.pipe((0, operators_1.take)(1)).subscribe(() => {
214
+ this.sendSessionMessage('camera_options', {
215
+ stealth_mode: false,
216
+ });
217
+ }));
218
+ }
219
+ callEnded() {
220
+ if (this.hasEnded) {
221
+ return;
222
+ }
223
+ try {
224
+ this.sendMessage({
225
+ reason: { code: 0, text: '' },
226
+ method: 'close',
227
+ });
228
+ this.ws.close();
229
+ }
230
+ catch (_) {
231
+ // ignore any errors since we are stopping the call
232
+ }
233
+ this.hasEnded = true;
234
+ this.unsubscribe();
235
+ this.onCallEnded.next();
236
+ this.pc.close();
237
+ }
238
+ stop() {
239
+ this.callEnded();
240
+ }
241
+ requestKeyFrame() {
242
+ this.pc.requestKeyFrame?.();
243
+ }
48
244
  }
49
245
  exports.WebrtcConnection = WebrtcConnection;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ring-client-api",
3
- "version": "11.8.0",
3
+ "version": "12.0.0-beta.0",
4
4
  "description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -26,30 +26,30 @@
26
26
  "debug": "^4.3.4",
27
27
  "got": "^11.8.5",
28
28
  "json-bigint": "^1.0.0",
29
- "msw": "^1.2.1",
29
+ "msw": "^1.2.3",
30
30
  "rxjs": "^7.8.1",
31
31
  "socket.io-client": "^2.5.0",
32
- "systeminformation": "^5.18.2",
32
+ "systeminformation": "^5.18.10",
33
33
  "uuid": "^9.0.0",
34
34
  "webcrypto-core": "^1.7.7",
35
- "werift": "0.18.3",
35
+ "werift": "0.18.5",
36
36
  "ws": "^8.13.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/debug": "4.1.8",
40
- "@types/jest": "29.5.2",
40
+ "@types/jest": "29.5.3",
41
41
  "@types/json-bigint": "^1.0.1",
42
- "@types/node": "20.2.5",
42
+ "@types/node": "20.4.5",
43
43
  "@types/uuid": "9.0.2",
44
44
  "@types/ws": "^8.5.5",
45
45
  "eslint-config-shared": "*",
46
- "jest": "29.5.0",
47
- "ts-jest": "29.1.0",
46
+ "jest": "29.6.2",
47
+ "ts-jest": "29.1.1",
48
48
  "tsconfig": "*",
49
- "typescript": "5.1.3"
49
+ "typescript": "5.1.6"
50
50
  },
51
51
  "engines": {
52
- "node": "^16 || ^18 || ^20"
52
+ "node": "^18 || ^20"
53
53
  },
54
54
  "repository": {
55
55
  "type": "git",
@@ -1,147 +0,0 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.RingEdgeConnection = void 0;
7
- const ws_1 = require("ws");
8
- const rxjs_1 = require("rxjs");
9
- const util_1 = require("../util");
10
- const streaming_connection_base_1 = require("./streaming-connection-base");
11
- const crypto_1 = __importDefault(require("crypto"));
12
- // eslint-disable-next-line no-shadow
13
- var CloseReasonCode;
14
- (function (CloseReasonCode) {
15
- CloseReasonCode[CloseReasonCode["NormalClose"] = 0] = "NormalClose";
16
- // 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' }
17
- // reason: { code: 5, text: 'Authentication failed: -1' }
18
- // reason: { code: 5, text: 'Sessions with the provided ID not found' }
19
- CloseReasonCode[CloseReasonCode["AuthenticationFailed"] = 5] = "AuthenticationFailed";
20
- // reason: { code: 6, text: 'Timeout waiting for ping' }
21
- CloseReasonCode[CloseReasonCode["Timeout"] = 6] = "Timeout";
22
- })(CloseReasonCode || (CloseReasonCode = {}));
23
- class RingEdgeConnection extends streaming_connection_base_1.StreamingConnectionBase {
24
- constructor(authToken, camera, options) {
25
- super(new ws_1.WebSocket('wss://api.prod.signalling.ring.devices.a2z.com:443/ws', {
26
- headers: {
27
- Authorization: `Bearer ${authToken}`,
28
- 'X-Sig-API-Version': '4.0',
29
- 'X-Sig-Client-ID': `ring_android-${crypto_1.default
30
- .randomBytes(4)
31
- .toString('hex')}`,
32
- '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',
33
- 'X-Sig-Auth-Type': 'ring_oauth',
34
- },
35
- }), options);
36
- this.camera = camera;
37
- this.onSessionId = new rxjs_1.ReplaySubject(1);
38
- this.onOfferSent = new rxjs_1.ReplaySubject(1);
39
- this.sessionId = null;
40
- this.addSubscriptions(this.onWsOpen.subscribe(() => {
41
- (0, util_1.logDebug)(`WebSocket connected for ${camera.name} (Ring Edge)`);
42
- this.initiateCall().catch((e) => {
43
- (0, util_1.logError)(e);
44
- this.callEnded();
45
- });
46
- }),
47
- // The ring-edge session needs a ping every 5 seconds to keep the connection alive
48
- (0, rxjs_1.interval)(5000).subscribe(() => {
49
- this.sendSessionMessage('ping');
50
- }), this.pc.onIceCandidate.subscribe(async (iceCandidate) => {
51
- await (0, rxjs_1.firstValueFrom)(this.onOfferSent);
52
- this.sendMessage({
53
- method: 'ice',
54
- body: {
55
- doorbot_id: camera.id,
56
- ice: iceCandidate.candidate,
57
- mlineindex: iceCandidate.sdpMLineIndex,
58
- },
59
- });
60
- }));
61
- }
62
- async initiateCall() {
63
- const { sdp } = await this.pc.createOffer();
64
- this.sendMessage({
65
- method: 'live_view',
66
- body: {
67
- doorbot_id: this.camera.id,
68
- stream_options: { audio_enabled: true, video_enabled: true },
69
- sdp,
70
- },
71
- });
72
- this.onOfferSent.next();
73
- }
74
- async handleMessage(message) {
75
- if (message.body.doorbot_id !== this.camera.id) {
76
- // ignore messages for other cameras
77
- return;
78
- }
79
- if (['session_created', 'session_started'].includes(message.method) &&
80
- 'session_id' in message.body &&
81
- !this.sessionId) {
82
- this.sessionId = message.body.session_id;
83
- this.onSessionId.next(this.sessionId);
84
- }
85
- if (message.body.session_id && message.body.session_id !== this.sessionId) {
86
- // ignore messages for other sessions
87
- return;
88
- }
89
- switch (message.method) {
90
- case 'session_created':
91
- case 'session_started':
92
- // session already stored above
93
- return;
94
- case 'sdp':
95
- await this.pc.acceptAnswer(message.body);
96
- this.onCallAnswered.next(message.body.sdp);
97
- this.activate();
98
- return;
99
- case 'ice':
100
- await this.pc.addIceCandidate({
101
- candidate: message.body.ice,
102
- sdpMLineIndex: message.body.mlineindex,
103
- });
104
- return;
105
- case 'pong':
106
- return;
107
- case 'notification':
108
- const { text } = message.body;
109
- if (text === 'PeerConnectionState::kConnecting' ||
110
- text === 'PeerConnectionState::kConnected') {
111
- return;
112
- }
113
- break;
114
- case 'close':
115
- (0, util_1.logError)('Video stream closed');
116
- (0, util_1.logError)(message.body);
117
- this.callEnded();
118
- return;
119
- }
120
- (0, util_1.logError)('UNKNOWN MESSAGE');
121
- (0, util_1.logError)(message);
122
- }
123
- sendSessionMessage(method, body = {}) {
124
- const sendSessionMessage = () => {
125
- const message = {
126
- method,
127
- body: {
128
- ...body,
129
- doorbot_id: this.camera.id,
130
- session_id: this.sessionId,
131
- },
132
- };
133
- this.sendMessage(message);
134
- };
135
- if (this.sessionId) {
136
- // Send immediately if we already have a session id
137
- // This is needed to send `close` before closing the websocket
138
- sendSessionMessage();
139
- }
140
- else {
141
- (0, rxjs_1.firstValueFrom)(this.onSessionId)
142
- .then(sendSessionMessage)
143
- .catch((e) => (0, util_1.logError)(e));
144
- }
145
- }
146
- }
147
- exports.RingEdgeConnection = RingEdgeConnection;
@@ -1,35 +0,0 @@
1
- import { BasicPeerConnection } from './peer-connection';
2
- import { Subscribed } from '../subscribed';
3
- import { WebSocket } from 'ws';
4
- import { RtpPacket } from 'werift';
5
- import { ReplaySubject, Subject } from 'rxjs';
6
- export interface StreamingConnectionOptions {
7
- createPeerConnection?: () => BasicPeerConnection;
8
- }
9
- export declare abstract class StreamingConnectionBase extends Subscribed {
10
- protected ws: WebSocket;
11
- protected options: StreamingConnectionOptions;
12
- readonly onCallAnswered: ReplaySubject<string>;
13
- readonly onCallEnded: ReplaySubject<void>;
14
- readonly onError: ReplaySubject<void>;
15
- readonly onMessage: ReplaySubject<{
16
- method: string;
17
- }>;
18
- readonly onWsOpen: import("rxjs").Observable<unknown>;
19
- protected readonly pc: BasicPeerConnection;
20
- readonly onAudioRtp: Subject<RtpPacket>;
21
- readonly onVideoRtp: Subject<RtpPacket>;
22
- constructor(ws: WebSocket, options?: StreamingConnectionOptions);
23
- protected abstract handleMessage(message: {
24
- method: string;
25
- }): Promise<void>;
26
- protected abstract sendSessionMessage(method: string, body?: Record<any, any>): void;
27
- protected activate(): void;
28
- activateCameraSpeaker(): void;
29
- protected sendMessage(message: Record<any, any>): void;
30
- sendAudioPacket(rtp: RtpPacket): void;
31
- private hasEnded;
32
- protected callEnded(): void;
33
- stop(): void;
34
- requestKeyFrame(): void;
35
- }
@@ -1,131 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.StreamingConnectionBase = void 0;
4
- const peer_connection_1 = require("./peer-connection");
5
- const subscribed_1 = require("../subscribed");
6
- const rxjs_1 = require("rxjs");
7
- const operators_1 = require("rxjs/operators");
8
- const util_1 = require("../util");
9
- class StreamingConnectionBase extends subscribed_1.Subscribed {
10
- constructor(ws, options = {}) {
11
- super();
12
- this.ws = ws;
13
- this.options = options;
14
- this.onCallAnswered = new rxjs_1.ReplaySubject(1);
15
- this.onCallEnded = new rxjs_1.ReplaySubject(1);
16
- this.onError = new rxjs_1.ReplaySubject(1);
17
- this.onMessage = new rxjs_1.ReplaySubject();
18
- this.hasEnded = false;
19
- if (options.createPeerConnection) {
20
- // we were passed a custom peer connection factory
21
- this.pc = options.createPeerConnection();
22
- // passing rtp packets is not supported for custom peer connections
23
- this.onAudioRtp = new rxjs_1.Subject();
24
- this.onVideoRtp = new rxjs_1.Subject();
25
- }
26
- else {
27
- // no custom peer connection factory, use the werift and pass along rtp packets
28
- const pc = new peer_connection_1.WeriftPeerConnection();
29
- this.pc = pc;
30
- this.onAudioRtp = pc.onAudioRtp;
31
- this.onVideoRtp = pc.onVideoRtp;
32
- }
33
- this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
34
- const onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message'), onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
35
- this.addSubscriptions(onMessage
36
- .pipe((0, operators_1.concatMap)((event) => {
37
- const message = JSON.parse(event.data);
38
- this.onMessage.next(message);
39
- return this.handleMessage(message).catch((e) => {
40
- if (e instanceof Error &&
41
- e.message.includes('negotiate codecs failed')) {
42
- e = new Error('Failed to negotiate codecs. This is a known issue with Ring cameras. Please see https://github.com/dgreif/ring/wiki/Streaming-Legacy-Mode');
43
- }
44
- this.onError.next(e);
45
- throw e;
46
- });
47
- }))
48
- .subscribe(), onError.subscribe((e) => {
49
- (0, util_1.logError)(e);
50
- this.callEnded();
51
- }), onClose.subscribe(() => {
52
- this.callEnded();
53
- }), this.pc.onConnectionState.subscribe((state) => {
54
- if (state === 'failed') {
55
- (0, util_1.logError)('Stream connection failed');
56
- this.callEnded();
57
- }
58
- if (state === 'closed') {
59
- (0, util_1.logDebug)('Stream connection closed');
60
- this.callEnded();
61
- }
62
- }), this.onError.subscribe((e) => {
63
- (0, util_1.logError)(e);
64
- this.callEnded();
65
- }));
66
- }
67
- activate() {
68
- (0, util_1.logInfo)('Activating Session');
69
- // the activate_session message is required to keep the stream alive longer than 70 seconds
70
- this.sendSessionMessage('activate_session');
71
- this.sendSessionMessage('stream_options', {
72
- audio_enabled: true,
73
- video_enabled: true,
74
- });
75
- }
76
- activateCameraSpeaker() {
77
- // Fire and forget this call so that callers don't get hung up waiting for answer (which might not happen)
78
- (0, rxjs_1.firstValueFrom)(this.onCallAnswered)
79
- .then(() => {
80
- this.sendSessionMessage('camera_options', {
81
- stealth_mode: false,
82
- });
83
- })
84
- .catch((e) => {
85
- (0, util_1.logError)(e);
86
- });
87
- }
88
- sendMessage(message) {
89
- if (this.hasEnded) {
90
- return;
91
- }
92
- this.ws.send(JSON.stringify(message));
93
- }
94
- sendAudioPacket(rtp) {
95
- if (this.hasEnded) {
96
- return;
97
- }
98
- if (this.pc instanceof peer_connection_1.WeriftPeerConnection) {
99
- this.pc.returnAudioTrack.writeRtp(rtp);
100
- }
101
- else {
102
- throw new Error('Cannot send audio packets to a custom peer connection implementation');
103
- }
104
- }
105
- callEnded() {
106
- if (this.hasEnded) {
107
- return;
108
- }
109
- try {
110
- this.sendMessage({
111
- reason: { code: 0, text: '' },
112
- method: 'close',
113
- });
114
- this.ws.close();
115
- }
116
- catch (_) {
117
- // ignore any errors since we are stopping the call
118
- }
119
- this.hasEnded = true;
120
- this.unsubscribe();
121
- this.onCallEnded.next();
122
- this.pc.close();
123
- }
124
- stop() {
125
- this.callEnded();
126
- }
127
- requestKeyFrame() {
128
- this.pc.requestKeyFrame?.();
129
- }
130
- }
131
- exports.StreamingConnectionBase = StreamingConnectionBase;