ring-client-api 10.0.0-beta.5 → 10.0.0-beta.8
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 +39 -0
- package/lib/api/api.js +1 -3
- package/lib/api/rest-client.d.ts +5 -0
- package/lib/api/rest-client.js +7 -6
- package/lib/api/ring-camera.d.ts +13 -4
- package/lib/api/ring-camera.js +22 -5
- package/lib/api/ring-types.d.ts +1 -0
- package/lib/api/ring-types.js +2 -0
- package/lib/api/{peer-connection.d.ts → streaming/peer-connection.d.ts} +21 -4
- package/lib/api/{peer-connection.js → streaming/peer-connection.js} +4 -4
- package/lib/api/streaming/ring-edge-connection.d.ts +65 -0
- package/lib/api/streaming/ring-edge-connection.js +155 -0
- package/lib/api/streaming/simple-webrtc-session.d.ts +11 -0
- package/lib/api/streaming/simple-webrtc-session.js +63 -0
- package/lib/api/streaming/streaming-connection-base.d.ts +30 -0
- package/lib/api/streaming/streaming-connection-base.js +111 -0
- package/lib/api/{live-call.d.ts → streaming/streaming-session.d.ts} +15 -18
- package/lib/api/{live-call.js → streaming/streaming-session.js} +32 -95
- package/lib/api/streaming/webrtc-connection.d.ts +23 -0
- package/lib/api/streaming/webrtc-connection.js +60 -0
- package/package.json +31 -30
- package/lib/api/live-call-ring-edge.d.ts +0 -41
- package/lib/api/live-call-ring-edge.js +0 -294
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,45 @@
|
|
|
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.8](https://github.com/dgreif/ring/compare/v10.0.0-beta.7...v10.0.0-beta.8) (2022-05-07)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* remove unnecessary dependencies ([0acf3c3](https://github.com/dgreif/ring/commit/0acf3c35ce2b62d98e3e50ba8489996400efb4e3))
|
|
11
|
+
|
|
12
|
+
## [10.0.0-beta.7](https://github.com/dgreif/ring/compare/v10.0.0-beta.6...v10.0.0-beta.7) (2022-05-07)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### ⚠ BREAKING CHANGES
|
|
16
|
+
|
|
17
|
+
* 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.
|
|
18
|
+
|
|
19
|
+
### Features
|
|
20
|
+
|
|
21
|
+
* add `cocoa_floodlight` ([c7a7e9c](https://github.com/dgreif/ring/commit/c7a7e9cabf45ec6579a7d9a1033fa01930923243)), closes [#893](https://github.com/dgreif/ring/issues/893)
|
|
22
|
+
* **api:** simple webrtc session ([bac7a49](https://github.com/dgreif/ring/commit/bac7a49178bb5a22914e53fda91c0fdd522b4a74))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Bug Fixes
|
|
26
|
+
|
|
27
|
+
* **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)
|
|
28
|
+
* include device name when setting push token ([2466a25](https://github.com/dgreif/ring/commit/2466a2519341771d8c79557b5a61c339f31eddf6)), closes [#920](https://github.com/dgreif/ring/issues/920)
|
|
29
|
+
* npm audit fix ([c1fe605](https://github.com/dgreif/ring/commit/c1fe6051e46dac92b00bb2fd1202746fd576b15a))
|
|
30
|
+
* remove doorbell press simulation options ([e1d1225](https://github.com/dgreif/ring/commit/e1d12257b2acbdafdc6995a53d9052822c8a3460))
|
|
31
|
+
|
|
32
|
+
## [10.0.0-beta.6](https://github.com/dgreif/ring/compare/v10.0.0-beta.5...v10.0.0-beta.6) (2022-05-06)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
### Features
|
|
36
|
+
|
|
37
|
+
* **api:** allow custom peer connections for streaming ([3e9e784](https://github.com/dgreif/ring/commit/3e9e78458afe2849a8c414a6b50eec8718bf77cc))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
### Bug Fixes
|
|
41
|
+
|
|
42
|
+
* update dependencies ([4b25595](https://github.com/dgreif/ring/commit/4b255950bcfbf94486192d55cfc90d966cd79d24))
|
|
43
|
+
|
|
5
44
|
## [10.0.0-beta.5](https://github.com/dgreif/ring/compare/v10.0.0-beta.4...v10.0.0-beta.5) (2022-04-10)
|
|
6
45
|
|
|
7
46
|
|
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
|
},
|
package/lib/api/rest-client.d.ts
CHANGED
|
@@ -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;
|
package/lib/api/rest-client.js
CHANGED
|
@@ -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
|
},
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -4,8 +4,9 @@ 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,
|
|
8
|
-
import {
|
|
7
|
+
import { FfmpegOptions, StreamingSession } from './streaming/streaming-session';
|
|
8
|
+
import { StreamingConnectionOptions } from './streaming/streaming-connection-base';
|
|
9
|
+
import { SimpleWebRtcSession } from './streaming/simple-webrtc-session';
|
|
9
10
|
export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'> & {
|
|
10
11
|
health?: Partial<CameraData['health']>;
|
|
11
12
|
}): number | null;
|
|
@@ -55,7 +56,8 @@ export declare class RingCamera extends Subscribed {
|
|
|
55
56
|
getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
|
|
56
57
|
setInHomeDoorbell(enable: boolean): Promise<boolean>;
|
|
57
58
|
getHealth(): Promise<CameraHealth>;
|
|
58
|
-
|
|
59
|
+
private createStreamingConnection;
|
|
60
|
+
startLiveCall(options?: StreamingConnectionOptions): Promise<StreamingSession>;
|
|
59
61
|
private removeDingById;
|
|
60
62
|
processPushNotification(notification: PushNotification): void;
|
|
61
63
|
getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
|
|
@@ -93,7 +95,14 @@ export declare class RingCamera extends Subscribed {
|
|
|
93
95
|
}): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
94
96
|
getSnapshotByUuid(uuid: string): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
95
97
|
recordToFile(outputPath: string, duration?: number): Promise<void>;
|
|
96
|
-
streamVideo(ffmpegOptions: FfmpegOptions): Promise<
|
|
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;
|
|
97
106
|
subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
98
107
|
unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
99
108
|
subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
package/lib/api/ring-camera.js
CHANGED
|
@@ -16,8 +16,10 @@ 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
|
|
20
|
-
const
|
|
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;
|
|
22
24
|
function parseBatteryLife(batteryLife) {
|
|
23
25
|
if (batteryLife === null || batteryLife === undefined) {
|
|
@@ -229,11 +231,11 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
229
231
|
return response.device_health;
|
|
230
232
|
});
|
|
231
233
|
}
|
|
232
|
-
|
|
234
|
+
createStreamingConnection(options) {
|
|
233
235
|
return __awaiter(this, void 0, void 0, function* () {
|
|
234
236
|
if (this.isRingEdgeEnabled) {
|
|
235
237
|
const auth = yield this.restClient.getCurrentAuth();
|
|
236
|
-
return new
|
|
238
|
+
return new ring_edge_connection_1.RingEdgeConnection(auth.access_token, this, options);
|
|
237
239
|
}
|
|
238
240
|
const liveCall = yield this.restClient
|
|
239
241
|
.request({
|
|
@@ -249,7 +251,13 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
249
251
|
}
|
|
250
252
|
throw e;
|
|
251
253
|
});
|
|
252
|
-
return new
|
|
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);
|
|
253
261
|
});
|
|
254
262
|
}
|
|
255
263
|
removeDingById(idToRemove) {
|
|
@@ -409,6 +417,15 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
409
417
|
return liveCall;
|
|
410
418
|
});
|
|
411
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
|
+
}
|
|
412
429
|
subscribeToDingEvents() {
|
|
413
430
|
return this.restClient.request({
|
|
414
431
|
method: 'POST',
|
package/lib/api/ring-types.d.ts
CHANGED
|
@@ -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: {
|
package/lib/api/ring-types.js
CHANGED
|
@@ -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,
|
|
2
|
-
import { ReplaySubject, Subject } from 'rxjs';
|
|
3
|
-
export
|
|
4
|
-
|
|
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.
|
|
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("
|
|
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
|
|
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.
|
|
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
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
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.SimpleWebRtcSession = void 0;
|
|
13
|
+
const util_1 = require("../util");
|
|
14
|
+
function liveViewUrl(path) {
|
|
15
|
+
return `https://api.ring.com/integrations/v1/liveview/${path}`;
|
|
16
|
+
}
|
|
17
|
+
class SimpleWebRtcSession {
|
|
18
|
+
constructor(camera, restClient) {
|
|
19
|
+
this.camera = camera;
|
|
20
|
+
this.restClient = restClient;
|
|
21
|
+
this.sessionId = (0, util_1.generateUuid)();
|
|
22
|
+
}
|
|
23
|
+
start(sdp) {
|
|
24
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
25
|
+
const response = yield this.restClient.request({
|
|
26
|
+
method: 'POST',
|
|
27
|
+
url: liveViewUrl('start'),
|
|
28
|
+
json: {
|
|
29
|
+
session_id: this.sessionId,
|
|
30
|
+
device_id: this.camera.id,
|
|
31
|
+
sdp,
|
|
32
|
+
protocol: 'webrtc',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return response.sdp;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
end() {
|
|
39
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
40
|
+
const resp = yield this.restClient.request({
|
|
41
|
+
method: 'POST',
|
|
42
|
+
url: liveViewUrl('end'),
|
|
43
|
+
json: {
|
|
44
|
+
session_id: this.sessionId,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
return resp;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
activateCameraSpeaker() {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
yield this.restClient.request({
|
|
53
|
+
method: 'PATCH',
|
|
54
|
+
url: liveViewUrl('options'),
|
|
55
|
+
json: {
|
|
56
|
+
session_id: this.sessionId,
|
|
57
|
+
actions: ['turn_off_stealth_mode'],
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
exports.SimpleWebRtcSession = SimpleWebRtcSession;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { BasicPeerConnection } from './peer-connection';
|
|
2
|
+
import { Subscribed } from '../subscribed';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
import { RtpPacket } from '@koush/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 onWsOpen: import("rxjs").Observable<unknown>;
|
|
15
|
+
protected readonly pc: BasicPeerConnection;
|
|
16
|
+
readonly onAudioRtp: Subject<RtpPacket>;
|
|
17
|
+
readonly onVideoRtp: Subject<RtpPacket>;
|
|
18
|
+
constructor(ws: WebSocket, options?: StreamingConnectionOptions);
|
|
19
|
+
protected abstract handleMessage(message: {
|
|
20
|
+
method: string;
|
|
21
|
+
}): Promise<void>;
|
|
22
|
+
protected abstract sendSessionMessage(method: string, body?: Record<any, any>): void;
|
|
23
|
+
private activated;
|
|
24
|
+
activate(): Promise<void>;
|
|
25
|
+
activateCameraSpeaker(): Promise<void>;
|
|
26
|
+
protected sendMessage(message: Record<any, any>): void;
|
|
27
|
+
sendAudioPacket(rtp: RtpPacket): void;
|
|
28
|
+
protected callEnded(): void;
|
|
29
|
+
stop(): void;
|
|
30
|
+
}
|