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.
- package/CHANGELOG.md +41 -0
- package/README.md +0 -3
- package/lib/api/api.d.ts +2 -4
- package/lib/api/api.js +64 -23
- package/lib/api/index.js +5 -1
- package/lib/api/live-call-ring-edge.d.ts +41 -0
- package/lib/api/live-call-ring-edge.js +294 -0
- package/lib/api/live-call.d.ts +2 -6
- package/lib/api/live-call.js +27 -127
- package/lib/api/peer-connection.d.ts +4 -0
- package/lib/api/peer-connection.js +21 -2
- package/lib/api/ring-camera.d.ts +20 -14
- package/lib/api/ring-camera.js +60 -34
- package/lib/api/ring-types.d.ts +89 -24
- package/lib/api/ring-types.js +18 -7
- package/lib/api/util.js +8 -1
- package/lib/test/api/ring-camera.spec.js +3 -1
- package/package.json +23 -22
package/lib/api/live-call.js
CHANGED
|
@@ -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(
|
|
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(
|
|
45
|
+
this.ws = new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
|
|
54
46
|
headers: {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
'
|
|
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.
|
|
158
|
-
this.
|
|
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.
|
|
163
|
-
sdpMLineIndex: message.
|
|
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.
|
|
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.
|
|
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.
|
|
274
|
-
reason: { code:
|
|
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=
|
|
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);
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
84
|
-
|
|
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>;
|
package/lib/api/ring-camera.js
CHANGED
|
@@ -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
|
|
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.
|
|
76
|
-
this.
|
|
77
|
-
this.
|
|
78
|
-
this.
|
|
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
|
|
122
|
-
return this.
|
|
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
|
-
|
|
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
|
-
|
|
238
|
-
|
|
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.
|
|
243
|
-
this.
|
|
256
|
+
const allActiveDings = this.activeNotifications, otherDings = allActiveDings.filter(({ ding }) => ding.id !== idToRemove);
|
|
257
|
+
this.onActiveNotifications.next(otherDings);
|
|
244
258
|
}
|
|
245
|
-
|
|
246
|
-
const activeDings = this.
|
|
247
|
-
this.
|
|
248
|
-
this.
|
|
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(
|
|
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
|
-
|
|
331
|
-
|
|
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({
|