ring-client-api 10.0.0-beta.1 → 10.0.0-beta.4
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 +42 -0
- package/README.md +0 -3
- package/lib/api/api.d.ts +2 -4
- package/lib/api/api.js +64 -23
- 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.js +6 -3
- package/lib/api/peer-connection.d.ts +8 -1
- package/lib/api/peer-connection.js +40 -2
- package/lib/api/ring-camera.d.ts +19 -14
- package/lib/api/ring-camera.js +50 -51
- package/lib/api/ring-types.d.ts +83 -26
- package/lib/api/ring-types.js +18 -7
- package/lib/api/util.js +1 -1
- package/package.json +3 -2
- package/lib/api/rtp-utils.d.ts +0 -26
- package/lib/api/rtp-utils.js +0 -8
- package/lib/test/api/ring-camera.spec.d.ts +0 -1
- package/lib/test/api/ring-camera.spec.js +0 -23
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,48 @@
|
|
|
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.4](https://github.com/dgreif/ring/compare/v10.0.0-beta.3...v10.0.0-beta.4) (2022-04-03)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* **api:** All ding-related functionality will now use the `PushNotification` type, and the `ActiveDing` type has been removed
|
|
11
|
+
* **api:** The `cameraDingsPollingSeconds` option has been removed. Ding events will now be received via push notifications (no setup required)
|
|
12
|
+
* **api:** The `treatKnockAsDing` option has been removed. Knocks should now register as "dings" automatically, though I have not been able to test this functionality
|
|
13
|
+
|
|
14
|
+
### Features
|
|
15
|
+
|
|
16
|
+
* **api:** convert ding handling to use notifications ([5b62318](https://github.com/dgreif/ring/commit/5b62318b6a71bcc224a2b29cbd6e21162fc9891e))
|
|
17
|
+
* **api:** update notification types ([15d3e7e](https://github.com/dgreif/ring/commit/15d3e7e40744e2c3ed48140fa94fbc0c6f9d4088))
|
|
18
|
+
* **homebridge:** faster snapshots from camera notifications ([7fc821b](https://github.com/dgreif/ring/commit/7fc821b71013c36314d6d428b8152e631ea4b497))
|
|
19
|
+
* receive dings from push notifications ([c9f130f](https://github.com/dgreif/ring/commit/c9f130f0f1f21b6db2454d611641ef7c29c7e620))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* more closely match ring edge live streaming setup flow ([4bb3189](https://github.com/dgreif/ring/commit/4bb3189d6ce6614f7af64d04b410c3922957d555))
|
|
25
|
+
* update uuid namespace to force new client sessions ([bc3e943](https://github.com/dgreif/ring/commit/bc3e9432c41dda085845f72f332dcf480c92475d))
|
|
26
|
+
* use camera health to determine battery and charging status ([fac7033](https://github.com/dgreif/ring/commit/fac70331bdd4dd94bc6caa143eb1bcb9ead06923)), closes [#873](https://github.com/dgreif/ring/issues/873)
|
|
27
|
+
|
|
28
|
+
## [10.0.0-beta.3](https://github.com/dgreif/ring/compare/v10.0.0-beta.2...v10.0.0-beta.3) (2022-03-26)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* separate streaming setup for ring edge vs regular cameras ([6281cf9](https://github.com/dgreif/ring/commit/6281cf966de12dcea8acc6516cd9079634734dec))
|
|
34
|
+
|
|
35
|
+
## [10.0.0-beta.2](https://github.com/dgreif/ring/compare/v10.0.0-beta.1...v10.0.0-beta.2) (2022-03-26)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Features
|
|
39
|
+
|
|
40
|
+
* support ring edge camera streaming ([b30d99f](https://github.com/dgreif/ring/commit/b30d99f9ffaa1fabc1d09b830cf8ba0e69fe84a0))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
### Bug Fixes
|
|
44
|
+
|
|
45
|
+
* **api:** allow snapshot to be force-updated based on options ([fa0212a](https://github.com/dgreif/ring/commit/fa0212ab539cc268f8000c5b9ea39a00799cb8d7))
|
|
46
|
+
|
|
5
47
|
## [10.0.0-beta.1](https://github.com/dgreif/ring/compare/v10.0.0-beta.0...v10.0.0-beta.1) (2022-02-27)
|
|
6
48
|
|
|
7
49
|
|
package/README.md
CHANGED
|
@@ -27,7 +27,6 @@ const ringApi = new RingApi({
|
|
|
27
27
|
|
|
28
28
|
// The following are all optional. See below for details
|
|
29
29
|
cameraStatusPollingSeconds: 20,
|
|
30
|
-
cameraDingsPollingSeconds: 2,
|
|
31
30
|
locationIds: ['488e4800-fcde-4493-969b-d1a06f683102', '4bbed7a7-06df-4f18-b3af-291c89854d60']
|
|
32
31
|
});
|
|
33
32
|
```
|
|
@@ -37,7 +36,6 @@ const ringApi = new RingApi({
|
|
|
37
36
|
Option | Default | Explanation
|
|
38
37
|
--- | --- | ---
|
|
39
38
|
`cameraStatusPollingSeconds` | `undefined` (No Polling) | How frequently to poll for updates to your cameras and chimes (in seconds). Information like light/siren/volume/snooze status do not update in real time and need to be requested periodically.
|
|
40
|
-
`cameraDingsPollingSeconds` | `undefined` (No Polling) | How frequently to poll for new events from your cameras (in seconds). These include motion and doorbell presses. Without this option, cameras will not emit any information about motion and doorbell presses.
|
|
41
39
|
`locationModePollingSeconds` | `undefined` (No Polling) | How frequently to poll for location mode updates (in seconds). This is only useful if you are using location modes to control camera settings and want to keep an up-to-date reference of the current mode for each location. Polling is automatically disabled for locations equipped with a Ring Alarm.
|
|
42
40
|
`locationIds` | All Locations | Allows you to limit the results to a specific set of locations. This is mainly useful for the [homebridge-ring Plugin](./homebridge), but can also be used if you only care about listening for events at a subset of your locations and don't want to create websocket connections to _all_ of your locations. This will also limit the results for `ringApi.getCameras()` to the configured locations. If this option is not included, all locations will be returned.
|
|
43
41
|
`debug` | false | Turns on additional logging. In particular, ffmpeg logging.
|
|
@@ -45,7 +43,6 @@ Option | Default | Explanation
|
|
|
45
43
|
`ffmpegPath` | Uses `ffmpeg-for-homebridge` | A custom path to the `ffmpeg` executable. By default, the static binaries built in [ffmpeg-for-homebridge](https://github.com/oznu/ffmpeg-for-homebridg) will be used. If you prefer to use your own version of ffmpeg, you can pass a complete path, or simply `"ffmpeg"` to use ffmpeg from your `PATH`.
|
|
46
44
|
`controlCenterDisplayName` | 'ring-client-api' | This allows you to change the displayed name for the Authorized Device within Control Center in the Ring app
|
|
47
45
|
`avoidSnapshotBatteryDrain` | false | Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery.
|
|
48
|
-
`treatKnockAsDing` | false | Will cause "knock" (`door_activity`) dings to be emitted from `onDoorbellPressed`
|
|
49
46
|
|
|
50
47
|
## Locations
|
|
51
48
|
```typescript
|
package/lib/api/api.d.ts
CHANGED
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
import { RefreshTokenAuth, RingRestClient, SessionOptions } from './rest-client';
|
|
2
2
|
import { Location } from './location';
|
|
3
|
-
import {
|
|
3
|
+
import { BaseStation, BeamBridge, CameraData, ChimeData, ProfileResponse, UserLocation } from './ring-types';
|
|
4
4
|
import { RingCamera } from './ring-camera';
|
|
5
5
|
import { Subscribed } from './subscribed';
|
|
6
6
|
export interface RingApiOptions extends SessionOptions {
|
|
7
7
|
locationIds?: string[];
|
|
8
8
|
cameraStatusPollingSeconds?: number;
|
|
9
|
-
cameraDingsPollingSeconds?: number;
|
|
10
9
|
locationModePollingSeconds?: number;
|
|
11
10
|
avoidSnapshotBatteryDrain?: boolean;
|
|
12
|
-
treatKnockAsDing?: boolean;
|
|
13
11
|
debug?: boolean;
|
|
14
12
|
ffmpegPath?: string;
|
|
15
13
|
externalPorts?: {
|
|
@@ -34,8 +32,8 @@ export declare class RingApi extends Subscribed {
|
|
|
34
32
|
baseStations: BaseStation[];
|
|
35
33
|
beamBridges: BeamBridge[];
|
|
36
34
|
}>;
|
|
37
|
-
fetchActiveDings(): Promise<ActiveDing[] & import("./rest-client").ExtendedResponse>;
|
|
38
35
|
private listenForDeviceUpdates;
|
|
36
|
+
registerPushReceiver(cameras: RingCamera[]): Promise<void>;
|
|
39
37
|
fetchRawLocations(): Promise<UserLocation[]>;
|
|
40
38
|
fetchAmazonKeyLocks(): Promise<any[] & import("./rest-client").ExtendedResponse>;
|
|
41
39
|
fetchAndBuildLocations(): Promise<Location[]>;
|
package/lib/api/api.js
CHANGED
|
@@ -8,6 +8,9 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
exports.RingApi = void 0;
|
|
13
16
|
const rest_client_1 = require("./rest-client");
|
|
@@ -19,6 +22,7 @@ const operators_1 = require("rxjs/operators");
|
|
|
19
22
|
const util_1 = require("./util");
|
|
20
23
|
const ffmpeg_1 = require("./ffmpeg");
|
|
21
24
|
const subscribed_1 = require("./subscribed");
|
|
25
|
+
const push_receiver_1 = __importDefault(require("@eneris/push-receiver"));
|
|
22
26
|
class RingApi extends subscribed_1.Subscribed {
|
|
23
27
|
constructor(options) {
|
|
24
28
|
super();
|
|
@@ -51,16 +55,13 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
51
55
|
};
|
|
52
56
|
});
|
|
53
57
|
}
|
|
54
|
-
fetchActiveDings() {
|
|
55
|
-
return this.restClient.request({
|
|
56
|
-
url: (0, rest_client_1.clientApi)('dings/active'),
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
58
|
listenForDeviceUpdates(cameras, chimes) {
|
|
60
|
-
const { cameraStatusPollingSeconds
|
|
59
|
+
const { cameraStatusPollingSeconds } = this.options;
|
|
60
|
+
if (!cameraStatusPollingSeconds) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const onCamerasRequestUpdate = (0, rxjs_1.merge)(...cameras.map((camera) => camera.onRequestUpdate)), onChimesRequestUpdate = (0, rxjs_1.merge)(...chimes.map((chime) => chime.onRequestUpdate)), onUpdateReceived = new rxjs_1.Subject(), onPollForStatusUpdate = cameraStatusPollingSeconds
|
|
61
64
|
? onUpdateReceived.pipe((0, operators_1.debounceTime)(cameraStatusPollingSeconds * 1000))
|
|
62
|
-
: rxjs_1.EMPTY, onPollForActiveDings = cameraDingsPollingSeconds
|
|
63
|
-
? onActiveDingsReceived.pipe((0, operators_1.debounceTime)(cameraDingsPollingSeconds * 1000))
|
|
64
65
|
: rxjs_1.EMPTY, camerasById = cameras.reduce((byId, camera) => {
|
|
65
66
|
byId[camera.id] = camera;
|
|
66
67
|
return byId;
|
|
@@ -94,22 +95,59 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
94
95
|
if (cameraStatusPollingSeconds) {
|
|
95
96
|
onUpdateReceived.next(null); // kick off polling
|
|
96
97
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
98
|
+
}
|
|
99
|
+
registerPushReceiver(cameras) {
|
|
100
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
101
|
+
const pushReceiver = new push_receiver_1.default({
|
|
102
|
+
logLevel: 'NONE',
|
|
103
|
+
senderId: '876313859327', // for Ring android app. 703521446232 for ring-site
|
|
104
|
+
}), camerasById = cameras.reduce((byId, camera) => {
|
|
105
|
+
byId[camera.id] = camera;
|
|
106
|
+
return byId;
|
|
107
|
+
}, {});
|
|
108
|
+
pushReceiver.onCredentialsChanged(({ newCredentials: { fcm: { token }, }, }) => __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
try {
|
|
110
|
+
yield this.restClient.request({
|
|
111
|
+
url: (0, rest_client_1.clientApi)('device'),
|
|
112
|
+
method: 'PATCH',
|
|
113
|
+
json: {
|
|
114
|
+
device: {
|
|
115
|
+
metadata: {
|
|
116
|
+
pn_service: 'fcm',
|
|
117
|
+
},
|
|
118
|
+
os: 'android',
|
|
119
|
+
push_notification_token: token,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
catch (e) {
|
|
125
|
+
(0, util_1.logError)(e);
|
|
126
|
+
}
|
|
127
|
+
}));
|
|
128
|
+
pushReceiver.onNotification(({ message }) => {
|
|
129
|
+
var _a;
|
|
130
|
+
const dataJson = (_a = message.data) === null || _a === void 0 ? void 0 : _a.gcmData;
|
|
131
|
+
try {
|
|
132
|
+
const notification = JSON.parse(dataJson);
|
|
133
|
+
if (!notification.ding) {
|
|
134
|
+
// We may get notification that are not for a ding/motion, which can be ignored
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
camerasById[notification.ding.doorbot_id].processPushNotification(notification);
|
|
138
|
+
}
|
|
139
|
+
catch (e) {
|
|
140
|
+
(0, util_1.logError)(e);
|
|
107
141
|
}
|
|
108
142
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
try {
|
|
144
|
+
yield pushReceiver.connect();
|
|
145
|
+
}
|
|
146
|
+
catch (e) {
|
|
147
|
+
(0, util_1.logError)('Failed to connect push notification receiver');
|
|
148
|
+
(0, util_1.logError)(e);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
113
151
|
}
|
|
114
152
|
fetchRawLocations() {
|
|
115
153
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -129,7 +167,7 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
129
167
|
return __awaiter(this, void 0, void 0, function* () {
|
|
130
168
|
const rawLocations = yield this.fetchRawLocations(), { authorizedDoorbots, chimes, doorbots, allCameras, baseStations, beamBridges, } = yield this.fetchRingDevices(), locationIdsWithHubs = [...baseStations, ...beamBridges].map((x) => x.location_id), cameras = allCameras.map((data) => new ring_camera_1.RingCamera(data, doorbots.includes(data) ||
|
|
131
169
|
authorizedDoorbots.includes(data) ||
|
|
132
|
-
data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false
|
|
170
|
+
data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
|
|
133
171
|
.filter((location) => {
|
|
134
172
|
return (!Array.isArray(this.options.locationIds) ||
|
|
135
173
|
this.options.locationIds.includes(location.location_id));
|
|
@@ -140,6 +178,9 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
140
178
|
locationModePollingSeconds: this.options.locationModePollingSeconds,
|
|
141
179
|
}, this.restClient));
|
|
142
180
|
this.listenForDeviceUpdates(cameras, ringChimes);
|
|
181
|
+
this.registerPushReceiver(cameras).catch((e) => {
|
|
182
|
+
(0, util_1.logError)(e);
|
|
183
|
+
});
|
|
143
184
|
return locations;
|
|
144
185
|
});
|
|
145
186
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ReplaySubject } from 'rxjs';
|
|
2
|
+
import { RingCamera } from './ring-camera';
|
|
3
|
+
import { Subscribed } from './subscribed';
|
|
4
|
+
import { RtpPacket } from '@koush/werift';
|
|
5
|
+
declare type SpawnInput = string | number;
|
|
6
|
+
export interface FfmpegOptions {
|
|
7
|
+
input?: SpawnInput[];
|
|
8
|
+
video?: SpawnInput[] | false;
|
|
9
|
+
audio?: SpawnInput[];
|
|
10
|
+
output: SpawnInput[];
|
|
11
|
+
}
|
|
12
|
+
export declare class LiveCallRingEdge extends Subscribed {
|
|
13
|
+
private camera;
|
|
14
|
+
private readonly ws;
|
|
15
|
+
private readonly onWsOpen;
|
|
16
|
+
private readonly onMessage;
|
|
17
|
+
private readonly pc;
|
|
18
|
+
readonly onSessionId: ReplaySubject<string>;
|
|
19
|
+
readonly onCallAnswered: ReplaySubject<string>;
|
|
20
|
+
readonly onCallEnded: ReplaySubject<void>;
|
|
21
|
+
private readonly audioSplitter;
|
|
22
|
+
private readonly videoSplitter;
|
|
23
|
+
readonly onVideoRtp: import("rxjs").Subject<RtpPacket>;
|
|
24
|
+
readonly onAudioRtp: import("rxjs").Subject<RtpPacket>;
|
|
25
|
+
private readonly onOfferSent;
|
|
26
|
+
constructor(authToken: string, camera: RingCamera);
|
|
27
|
+
private initiateCall;
|
|
28
|
+
private sessionId;
|
|
29
|
+
private handleMessage;
|
|
30
|
+
reservePort(bufferPorts?: number): Promise<number>;
|
|
31
|
+
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
32
|
+
activated: boolean;
|
|
33
|
+
activate(): Promise<void>;
|
|
34
|
+
activateCameraSpeaker(): Promise<void>;
|
|
35
|
+
private sendSessionMessage;
|
|
36
|
+
private sendMessage;
|
|
37
|
+
private callEnded;
|
|
38
|
+
stop(): void;
|
|
39
|
+
sendAudioPacket(rtp: RtpPacket): void;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,294 @@
|
|
|
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.LiveCallRingEdge = void 0;
|
|
13
|
+
const ws_1 = require("ws");
|
|
14
|
+
const rxjs_1 = require("rxjs");
|
|
15
|
+
const peer_connection_1 = require("./peer-connection");
|
|
16
|
+
const util_1 = require("./util");
|
|
17
|
+
const operators_1 = require("rxjs/operators");
|
|
18
|
+
const subscribed_1 = require("./subscribed");
|
|
19
|
+
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
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
|
+
function getCleanSdp(sdp, includeVideo) {
|
|
33
|
+
return sdp
|
|
34
|
+
.split('\nm=')
|
|
35
|
+
.slice(1)
|
|
36
|
+
.map((section) => 'm=' + section)
|
|
37
|
+
.filter((section) => includeVideo || !section.startsWith('m=video'))
|
|
38
|
+
.join('\n');
|
|
39
|
+
}
|
|
40
|
+
class LiveCallRingEdge extends subscribed_1.Subscribed {
|
|
41
|
+
constructor(authToken, camera) {
|
|
42
|
+
super();
|
|
43
|
+
this.camera = camera;
|
|
44
|
+
this.onSessionId = new rxjs_1.ReplaySubject(1);
|
|
45
|
+
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
46
|
+
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
47
|
+
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
48
|
+
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
49
|
+
this.onOfferSent = new rxjs_1.ReplaySubject(1);
|
|
50
|
+
this.sessionId = null;
|
|
51
|
+
this.activated = false;
|
|
52
|
+
this.pc = new peer_connection_1.PeerConnection();
|
|
53
|
+
this.ws = new ws_1.WebSocket('wss://api.prod.signalling.ring.devices.a2z.com:443/ws', {
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${authToken}`,
|
|
56
|
+
'X-Sig-API-Version': '4.0',
|
|
57
|
+
'X-Sig-Client-ID': 'ring_android-aabb123',
|
|
58
|
+
'X-Sig-Client-Info': 'Ring/3.49.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
|
|
59
|
+
'X-Sig-Auth-Type': 'ring_oauth',
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
this.onAudioRtp = this.pc.onAudioRtp;
|
|
63
|
+
this.onVideoRtp = this.pc.onVideoRtp;
|
|
64
|
+
this.onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message');
|
|
65
|
+
this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
|
|
66
|
+
const onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
|
|
67
|
+
this.addSubscriptions(this.onMessage
|
|
68
|
+
.pipe((0, operators_1.concatMap)((message) => {
|
|
69
|
+
return this.handleMessage(JSON.parse(message.data));
|
|
70
|
+
}))
|
|
71
|
+
.subscribe(), this.onWsOpen.subscribe(() => {
|
|
72
|
+
(0, util_1.logDebug)(`WebSocket connected for ${this.camera.name} (Ring Edge)`);
|
|
73
|
+
this.initiateCall().catch((e) => {
|
|
74
|
+
(0, util_1.logError)(e);
|
|
75
|
+
this.callEnded();
|
|
76
|
+
});
|
|
77
|
+
}), onError.subscribe((e) => {
|
|
78
|
+
(0, util_1.logError)(e);
|
|
79
|
+
this.callEnded();
|
|
80
|
+
}), onClose.subscribe(() => {
|
|
81
|
+
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
|
+
}));
|
|
119
|
+
}
|
|
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
|
+
handleMessage(message) {
|
|
136
|
+
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
|
+
switch (message.method) {
|
|
152
|
+
case 'session_created':
|
|
153
|
+
case 'session_started':
|
|
154
|
+
// session already stored above
|
|
155
|
+
return;
|
|
156
|
+
case 'sdp':
|
|
157
|
+
yield this.pc.acceptAnswer(message.body);
|
|
158
|
+
this.onCallAnswered.next(message.body.sdp);
|
|
159
|
+
return;
|
|
160
|
+
case 'ice':
|
|
161
|
+
yield this.pc.addIceCandidate({
|
|
162
|
+
candidate: message.body.ice,
|
|
163
|
+
sdpMLineIndex: message.body.mlineindex,
|
|
164
|
+
});
|
|
165
|
+
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
|
+
}
|
|
174
|
+
(0, util_1.logError)('UNKNOWN MESSAGE');
|
|
175
|
+
(0, util_1.logError)(message);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
reservePort(bufferPorts = 0) {
|
|
179
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
180
|
+
const ports = yield (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
|
|
181
|
+
return ports[0];
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
startTranscoding(ffmpegOptions) {
|
|
185
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
186
|
+
const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, ffmpegInputArguments = [
|
|
187
|
+
'-hide_banner',
|
|
188
|
+
'-protocol_whitelist',
|
|
189
|
+
'pipe,udp,rtp,file,crypto',
|
|
190
|
+
'-acodec',
|
|
191
|
+
'libopus',
|
|
192
|
+
'-f',
|
|
193
|
+
'sdp',
|
|
194
|
+
...(ffmpegOptions.input || []),
|
|
195
|
+
'-i',
|
|
196
|
+
'pipe:',
|
|
197
|
+
], ringSdp = yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered), inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
|
|
198
|
+
.replace(/m=audio \d+/, `m=audio ${audioPort}`)
|
|
199
|
+
.replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
|
|
200
|
+
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
201
|
+
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
202
|
+
: []), ...(ffmpegOptions.output || [])),
|
|
203
|
+
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
204
|
+
exitCallback: () => this.callEnded(),
|
|
205
|
+
logLabel: `From Ring (${this.camera.name})`,
|
|
206
|
+
logger: {
|
|
207
|
+
error: util_1.logError,
|
|
208
|
+
info: util_1.logDebug,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
this.addSubscriptions(this.onAudioRtp
|
|
212
|
+
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
213
|
+
return this.audioSplitter.send(rtp.serialize(), {
|
|
214
|
+
port: audioPort,
|
|
215
|
+
});
|
|
216
|
+
}))
|
|
217
|
+
.subscribe());
|
|
218
|
+
if (transcodeVideoStream) {
|
|
219
|
+
this.addSubscriptions(this.onVideoRtp
|
|
220
|
+
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
221
|
+
return this.videoSplitter.send(rtp.serialize(), {
|
|
222
|
+
port: videoPort,
|
|
223
|
+
});
|
|
224
|
+
}))
|
|
225
|
+
.subscribe());
|
|
226
|
+
}
|
|
227
|
+
this.onCallEnded.subscribe(() => ff.stop());
|
|
228
|
+
ff.writeStdin(inputSdp);
|
|
229
|
+
// Activate the stream now that ffmpeg is ready to receive
|
|
230
|
+
yield this.activate();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
activate() {
|
|
234
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
235
|
+
if (this.activated) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
this.activated = true;
|
|
239
|
+
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
240
|
+
(0, util_1.logInfo)('Activating Session');
|
|
241
|
+
this.sendSessionMessage('activate_session');
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
activateCameraSpeaker() {
|
|
245
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
246
|
+
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
247
|
+
this.sendSessionMessage('camera_options', {
|
|
248
|
+
stealth_mode: false,
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
}
|
|
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
|
+
sendMessage(message) {
|
|
269
|
+
this.ws.send(JSON.stringify(message));
|
|
270
|
+
}
|
|
271
|
+
callEnded() {
|
|
272
|
+
try {
|
|
273
|
+
this.sendSessionMessage('close', {
|
|
274
|
+
reason: { code: CloseReasonCode.NormalClose, text: '' },
|
|
275
|
+
});
|
|
276
|
+
this.ws.close();
|
|
277
|
+
}
|
|
278
|
+
catch (_) {
|
|
279
|
+
// ignore any errors since we are stopping the call
|
|
280
|
+
}
|
|
281
|
+
this.unsubscribe();
|
|
282
|
+
this.onCallEnded.next();
|
|
283
|
+
this.pc.close();
|
|
284
|
+
this.audioSplitter.close();
|
|
285
|
+
this.videoSplitter.close();
|
|
286
|
+
}
|
|
287
|
+
stop() {
|
|
288
|
+
this.callEnded();
|
|
289
|
+
}
|
|
290
|
+
sendAudioPacket(rtp) {
|
|
291
|
+
this.pc.returnAudioTrack.writeRtp(rtp);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
exports.LiveCallRingEdge = LiveCallRingEdge;
|
package/lib/api/live-call.js
CHANGED
|
@@ -10,7 +10,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.LiveCall = void 0;
|
|
13
|
-
const rtp_utils_1 = require("./rtp-utils");
|
|
14
13
|
const ws_1 = require("ws");
|
|
15
14
|
const rxjs_1 = require("rxjs");
|
|
16
15
|
const peer_connection_1 = require("./peer-connection");
|
|
@@ -27,6 +26,10 @@ function getCleanSdp(sdp, includeVideo) {
|
|
|
27
26
|
.filter((section) => includeVideo || !section.startsWith('m=video'))
|
|
28
27
|
.join('\n');
|
|
29
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
|
+
}
|
|
30
33
|
class LiveCall extends subscribed_1.Subscribed {
|
|
31
34
|
constructor(sessionId, camera) {
|
|
32
35
|
super();
|
|
@@ -37,13 +40,13 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
37
40
|
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
38
41
|
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
39
42
|
this.activated = false;
|
|
40
|
-
const liveCallSession =
|
|
43
|
+
const liveCallSession = parseLiveCallSession(sessionId);
|
|
41
44
|
this.pc = new peer_connection_1.PeerConnection();
|
|
42
45
|
this.ws = new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
|
|
43
46
|
headers: {
|
|
44
47
|
API_VERSION: '3.1',
|
|
45
48
|
API_TOKEN: sessionId,
|
|
46
|
-
CLIENT_INFO: 'Ring/3.
|
|
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',
|
|
47
50
|
},
|
|
48
51
|
});
|
|
49
52
|
this.onAudioRtp = this.pc.onAudioRtp;
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { MediaStreamTrack, RTCIceCandidate, RTCPeerConnection, RtcpPacket, RtpPacket } from '@koush/werift';
|
|
2
|
-
import { Subject } from 'rxjs';
|
|
2
|
+
import { ReplaySubject, Subject } from 'rxjs';
|
|
3
3
|
export declare class PeerConnection {
|
|
4
4
|
pc: RTCPeerConnection;
|
|
5
5
|
onAudioRtp: Subject<RtpPacket>;
|
|
6
6
|
onAudioRtcp: Subject<RtcpPacket>;
|
|
7
7
|
onVideoRtp: Subject<RtpPacket>;
|
|
8
8
|
onVideoRtcp: Subject<RtcpPacket>;
|
|
9
|
+
onIceCandidate: Subject<RTCIceCandidate>;
|
|
10
|
+
onConnectionState: ReplaySubject<"failed" | "closed" | "connected" | "connecting" | "new" | "disconnected">;
|
|
9
11
|
returnAudioTrack: MediaStreamTrack;
|
|
10
12
|
constructor();
|
|
13
|
+
createOffer(): Promise<import("@koush/werift").RTCSessionDescription>;
|
|
11
14
|
createAnswer(offer: {
|
|
12
15
|
type: 'offer';
|
|
13
16
|
sdp: string;
|
|
14
17
|
}): Promise<import("@koush/werift").RTCSessionDescription>;
|
|
18
|
+
acceptAnswer(answer: {
|
|
19
|
+
type: 'answer';
|
|
20
|
+
sdp: string;
|
|
21
|
+
}): Promise<void>;
|
|
15
22
|
addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
|
|
16
23
|
close(): void;
|
|
17
24
|
}
|
|
@@ -13,13 +13,24 @@ 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();
|
|
20
29
|
this.onAudioRtcp = new rxjs_1.Subject();
|
|
21
30
|
this.onVideoRtp = new rxjs_1.Subject();
|
|
22
31
|
this.onVideoRtcp = new rxjs_1.Subject();
|
|
32
|
+
this.onIceCandidate = new rxjs_1.Subject();
|
|
33
|
+
this.onConnectionState = new rxjs_1.ReplaySubject(1);
|
|
23
34
|
this.returnAudioTrack = new werift_1.MediaStreamTrack({ kind: 'audio' });
|
|
24
35
|
const pc = (this.pc = new werift_1.RTCPeerConnection({
|
|
25
36
|
codecs: {
|
|
@@ -41,10 +52,12 @@ class PeerConnection {
|
|
|
41
52
|
{ type: 'nack', parameter: 'pli' },
|
|
42
53
|
{ type: 'goog-remb' },
|
|
43
54
|
],
|
|
44
|
-
parameters: '
|
|
55
|
+
parameters: 'packetization-mode=1;profile-level-id=640029;level-asymmetry-allowed=1',
|
|
45
56
|
}),
|
|
46
57
|
],
|
|
47
58
|
},
|
|
59
|
+
iceServers: ringIceServers.map((server) => ({ urls: server })),
|
|
60
|
+
iceTransportPolicy: 'all',
|
|
48
61
|
})), audioTransceiver = pc.addTransceiver(this.returnAudioTrack, {
|
|
49
62
|
direction: 'sendrecv',
|
|
50
63
|
}), videoTransceiver = pc.addTransceiver('video', {
|
|
@@ -77,6 +90,26 @@ class PeerConnection {
|
|
|
77
90
|
setInterval(() => videoTransceiver.receiver.sendRtcpPLI(track.ssrc), 2000);
|
|
78
91
|
});
|
|
79
92
|
});
|
|
93
|
+
this.pc.onIceCandidate.subscribe((iceCandidate) => {
|
|
94
|
+
this.onIceCandidate.next(iceCandidate);
|
|
95
|
+
});
|
|
96
|
+
pc.iceConnectionStateChange.subscribe(() => {
|
|
97
|
+
(0, util_1.logInfo)(`iceConnectionStateChange: ${pc.iceConnectionState}`);
|
|
98
|
+
if (pc.iceConnectionState === 'closed') {
|
|
99
|
+
this.onConnectionState.next('closed');
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
pc.connectionStateChange.subscribe(() => {
|
|
103
|
+
(0, util_1.logInfo)(`connectionStateChange: ${pc.connectionState}`);
|
|
104
|
+
this.onConnectionState.next(pc.connectionState);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
createOffer() {
|
|
108
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
109
|
+
const offer = yield this.pc.createOffer();
|
|
110
|
+
yield this.pc.setLocalDescription(offer);
|
|
111
|
+
return offer;
|
|
112
|
+
});
|
|
80
113
|
}
|
|
81
114
|
createAnswer(offer) {
|
|
82
115
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -86,6 +119,11 @@ class PeerConnection {
|
|
|
86
119
|
return answer;
|
|
87
120
|
});
|
|
88
121
|
}
|
|
122
|
+
acceptAnswer(answer) {
|
|
123
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
124
|
+
yield this.pc.setRemoteDescription(answer);
|
|
125
|
+
});
|
|
126
|
+
}
|
|
89
127
|
addIceCandidate(candidate) {
|
|
90
128
|
return this.pc.addIceCandidate(candidate);
|
|
91
129
|
}
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
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({ health }: Pick<CameraData, 'health'>): number | null;
|
|
9
10
|
export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
|
|
10
11
|
accountId: string;
|
|
11
12
|
})): string;
|
|
@@ -14,7 +15,6 @@ export declare class RingCamera extends Subscribed {
|
|
|
14
15
|
isDoorbot: boolean;
|
|
15
16
|
private restClient;
|
|
16
17
|
private avoidSnapshotBatteryDrain;
|
|
17
|
-
private treatKnockAsDing;
|
|
18
18
|
id: number;
|
|
19
19
|
deviceType: import("./ring-types").RingCameraKind;
|
|
20
20
|
model: string;
|
|
@@ -22,26 +22,27 @@ export declare class RingCamera extends Subscribed {
|
|
|
22
22
|
hasLight: boolean;
|
|
23
23
|
hasSiren: boolean;
|
|
24
24
|
onRequestUpdate: Subject<unknown>;
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
onDoorbellPressed: import("rxjs").Observable<ActiveDing>;
|
|
25
|
+
onNewNotification: Subject<PushNotification>;
|
|
26
|
+
onActiveNotifications: BehaviorSubject<PushNotification[]>;
|
|
27
|
+
onDoorbellPressed: import("rxjs").Observable<PushNotification>;
|
|
29
28
|
onMotionDetected: import("rxjs").Observable<boolean>;
|
|
30
29
|
onMotionStarted: import("rxjs").Observable<null>;
|
|
31
30
|
onBatteryLevel: import("rxjs").Observable<number | null>;
|
|
32
31
|
onInHomeDoorbellStatus: import("rxjs").Observable<boolean>;
|
|
33
|
-
constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean
|
|
32
|
+
constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean);
|
|
34
33
|
updateData(update: CameraData): void;
|
|
35
34
|
requestUpdate(): void;
|
|
36
35
|
get data(): CameraData;
|
|
37
36
|
get name(): string;
|
|
38
|
-
get
|
|
37
|
+
get activeNotifications(): PushNotification[];
|
|
38
|
+
get latestNotification(): PushNotification | undefined;
|
|
39
39
|
get batteryLevel(): number | null;
|
|
40
40
|
get hasBattery(): boolean;
|
|
41
41
|
get hasLowBattery(): boolean;
|
|
42
42
|
get isCharging(): boolean;
|
|
43
43
|
get operatingOnBattery(): boolean;
|
|
44
44
|
get isOffline(): boolean;
|
|
45
|
+
get isRingEdgeEnabled(): boolean;
|
|
45
46
|
get hasInHomeDoorbell(): boolean;
|
|
46
47
|
doorbotUrl(path?: string): string;
|
|
47
48
|
deviceUrl(path?: string): string;
|
|
@@ -52,9 +53,9 @@ export declare class RingCamera extends Subscribed {
|
|
|
52
53
|
getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
|
|
53
54
|
setInHomeDoorbell(enable: boolean): Promise<boolean>;
|
|
54
55
|
getHealth(): Promise<CameraHealth>;
|
|
55
|
-
startLiveCall(): Promise<LiveCall>;
|
|
56
|
+
startLiveCall(): Promise<LiveCall | LiveCallRingEdge>;
|
|
56
57
|
private removeDingById;
|
|
57
|
-
|
|
58
|
+
processPushNotification(notification: PushNotification): void;
|
|
58
59
|
getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
|
|
59
60
|
videoSearch({ dateFrom, dateTo, order }?: {
|
|
60
61
|
dateFrom: number;
|
|
@@ -79,14 +80,18 @@ export declare class RingCamera extends Subscribed {
|
|
|
79
80
|
private checkIfSnapshotsAreBlocked;
|
|
80
81
|
private shouldUseExistingSnapshotPromise;
|
|
81
82
|
private fetchingSnapshot;
|
|
82
|
-
getSnapshot(
|
|
83
|
-
|
|
83
|
+
getSnapshot({ uuid }?: {
|
|
84
|
+
uuid?: string;
|
|
85
|
+
}): Promise<Buffer>;
|
|
86
|
+
getNextSnapshot({ afterMs, maxWaitMs, force, uuid, }: {
|
|
84
87
|
afterMs?: number;
|
|
85
88
|
maxWaitMs?: number;
|
|
86
89
|
force?: boolean;
|
|
90
|
+
uuid?: string;
|
|
87
91
|
}): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
92
|
+
getSnapshotByUuid(uuid: string): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
88
93
|
recordToFile(outputPath: string, duration?: number): Promise<void>;
|
|
89
|
-
streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall>;
|
|
94
|
+
streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall | LiveCallRingEdge>;
|
|
90
95
|
subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
91
96
|
unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
92
97
|
subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
package/lib/api/ring-camera.js
CHANGED
|
@@ -17,34 +17,19 @@ 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
|
-
function parseBatteryLife(batteryLife) {
|
|
22
|
-
if (batteryLife === null || batteryLife === undefined) {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
const batteryLevel = typeof batteryLife === 'number'
|
|
26
|
-
? batteryLife
|
|
27
|
-
: Number.parseFloat(batteryLife);
|
|
28
|
-
if (isNaN(batteryLevel)) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
return batteryLevel;
|
|
32
|
-
}
|
|
33
22
|
function getStartOfToday() {
|
|
34
23
|
return new Date(new Date().toLocaleDateString()).getTime();
|
|
35
24
|
}
|
|
36
25
|
function getEndOfToday() {
|
|
37
26
|
return getStartOfToday() + fullDayMs - 1;
|
|
38
27
|
}
|
|
39
|
-
function getBatteryLevel(
|
|
40
|
-
|
|
41
|
-
parseBatteryLife(data.battery_life),
|
|
42
|
-
parseBatteryLife(data.battery_life_2),
|
|
43
|
-
].filter((level) => level !== null);
|
|
44
|
-
if (!levels.length) {
|
|
28
|
+
function getBatteryLevel({ health }) {
|
|
29
|
+
if (!health.battery_percentage && !health.battery_present) {
|
|
45
30
|
return null;
|
|
46
31
|
}
|
|
47
|
-
return
|
|
32
|
+
return health.battery_percentage || 0;
|
|
48
33
|
}
|
|
49
34
|
exports.getBatteryLevel = getBatteryLevel;
|
|
50
35
|
function getSearchQueryString(options) {
|
|
@@ -64,20 +49,17 @@ function getSearchQueryString(options) {
|
|
|
64
49
|
}
|
|
65
50
|
exports.getSearchQueryString = getSearchQueryString;
|
|
66
51
|
class RingCamera extends subscribed_1.Subscribed {
|
|
67
|
-
constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain
|
|
52
|
+
constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain) {
|
|
68
53
|
super();
|
|
69
54
|
this.initialData = initialData;
|
|
70
55
|
this.isDoorbot = isDoorbot;
|
|
71
56
|
this.restClient = restClient;
|
|
72
57
|
this.avoidSnapshotBatteryDrain = avoidSnapshotBatteryDrain;
|
|
73
|
-
this.treatKnockAsDing = treatKnockAsDing;
|
|
74
58
|
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)());
|
|
59
|
+
this.onNewNotification = new rxjs_1.Subject();
|
|
60
|
+
this.onActiveNotifications = new rxjs_1.BehaviorSubject([]);
|
|
61
|
+
this.onDoorbellPressed = this.onNewNotification.pipe((0, operators_1.filter)((notification) => notification.action === ring_types_1.PushNotificationAction.Ding), (0, operators_1.share)());
|
|
62
|
+
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
63
|
this.onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.mapTo)(null), // no value needed, event is what matters
|
|
82
64
|
(0, operators_1.share)());
|
|
83
65
|
this.lastSnapshotTimestamp = 0;
|
|
@@ -118,26 +100,24 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
118
100
|
get name() {
|
|
119
101
|
return this.data.description;
|
|
120
102
|
}
|
|
121
|
-
get
|
|
122
|
-
return this.
|
|
103
|
+
get activeNotifications() {
|
|
104
|
+
return this.onActiveNotifications.getValue();
|
|
105
|
+
}
|
|
106
|
+
get latestNotification() {
|
|
107
|
+
const notifications = this.activeNotifications;
|
|
108
|
+
return notifications[notifications.length - 1];
|
|
123
109
|
}
|
|
124
110
|
get batteryLevel() {
|
|
125
111
|
return getBatteryLevel(this.data);
|
|
126
112
|
}
|
|
127
113
|
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));
|
|
114
|
+
return this.batteryLevel !== null;
|
|
135
115
|
}
|
|
136
116
|
get hasLowBattery() {
|
|
137
117
|
return this.data.alerts.battery === 'low';
|
|
138
118
|
}
|
|
139
119
|
get isCharging() {
|
|
140
|
-
return this.
|
|
120
|
+
return Boolean(this.data.health.ac_power || this.data.health.ext_power_state);
|
|
141
121
|
}
|
|
142
122
|
get operatingOnBattery() {
|
|
143
123
|
return this.hasBattery && this.data.settings.power_mode !== 'wired';
|
|
@@ -145,6 +125,9 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
145
125
|
get isOffline() {
|
|
146
126
|
return this.data.alerts.connection === 'offline';
|
|
147
127
|
}
|
|
128
|
+
get isRingEdgeEnabled() {
|
|
129
|
+
return this.data.settings.sheila_settings.local_storage_enabled === true;
|
|
130
|
+
}
|
|
148
131
|
get hasInHomeDoorbell() {
|
|
149
132
|
const { chime_settings } = this.data.settings;
|
|
150
133
|
return (this.isDoorbot &&
|
|
@@ -231,6 +214,10 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
231
214
|
}
|
|
232
215
|
startLiveCall() {
|
|
233
216
|
return __awaiter(this, void 0, void 0, function* () {
|
|
217
|
+
if (this.isRingEdgeEnabled) {
|
|
218
|
+
const auth = yield this.restClient.getCurrentAuth();
|
|
219
|
+
return new live_call_ring_edge_1.LiveCallRingEdge(auth.access_token, this);
|
|
220
|
+
}
|
|
234
221
|
const liveCall = yield this.restClient
|
|
235
222
|
.request({
|
|
236
223
|
method: 'POST',
|
|
@@ -249,15 +236,15 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
249
236
|
});
|
|
250
237
|
}
|
|
251
238
|
removeDingById(idToRemove) {
|
|
252
|
-
const allActiveDings = this.
|
|
253
|
-
this.
|
|
239
|
+
const allActiveDings = this.activeNotifications, otherDings = allActiveDings.filter(({ ding }) => ding.id !== idToRemove);
|
|
240
|
+
this.onActiveNotifications.next(otherDings);
|
|
254
241
|
}
|
|
255
|
-
|
|
256
|
-
const activeDings = this.
|
|
257
|
-
this.
|
|
258
|
-
this.
|
|
242
|
+
processPushNotification(notification) {
|
|
243
|
+
const activeDings = this.activeNotifications, dingId = notification.ding.id;
|
|
244
|
+
this.onActiveNotifications.next(activeDings.filter((d) => d.ding.id !== dingId).concat([notification]));
|
|
245
|
+
this.onNewNotification.next(notification);
|
|
259
246
|
setTimeout(() => {
|
|
260
|
-
this.removeDingById(
|
|
247
|
+
this.removeDingById(dingId);
|
|
261
248
|
}, 65 * 1000); // dings last ~1 minute
|
|
262
249
|
}
|
|
263
250
|
getEvents(options = {}) {
|
|
@@ -329,17 +316,19 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
329
316
|
return false;
|
|
330
317
|
}
|
|
331
318
|
}
|
|
332
|
-
getSnapshot() {
|
|
319
|
+
getSnapshot({ uuid } = {}) {
|
|
333
320
|
return __awaiter(this, void 0, void 0, function* () {
|
|
334
321
|
if (this.lastSnapshotPromise && this.shouldUseExistingSnapshotPromise()) {
|
|
335
322
|
return this.lastSnapshotPromise;
|
|
336
323
|
}
|
|
337
324
|
this.checkIfSnapshotsAreBlocked();
|
|
338
325
|
this.lastSnapshotPromise = Promise.race([
|
|
339
|
-
this.getNextSnapshot(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
326
|
+
this.getNextSnapshot(uuid
|
|
327
|
+
? { uuid }
|
|
328
|
+
: {
|
|
329
|
+
afterMs: this.lastSnapshotTimestamp,
|
|
330
|
+
force: true,
|
|
331
|
+
}),
|
|
343
332
|
(0, util_1.delay)(maxSnapshotRefreshSeconds * 1000).then(() => {
|
|
344
333
|
const extraMessageForBatteryCam = this.operatingOnBattery
|
|
345
334
|
? '. This is normal behavior since this camera is unable to capture snapshots while streaming'
|
|
@@ -359,15 +348,16 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
359
348
|
return this.lastSnapshotPromise;
|
|
360
349
|
});
|
|
361
350
|
}
|
|
362
|
-
getNextSnapshot({ afterMs, maxWaitMs, force, }) {
|
|
351
|
+
getNextSnapshot({ afterMs, maxWaitMs, force, uuid, }) {
|
|
363
352
|
return __awaiter(this, void 0, void 0, function* () {
|
|
364
353
|
const response = yield this.restClient.request({
|
|
365
|
-
url: `https://app-snaps.ring.com/snapshots/next/${this.id}
|
|
354
|
+
url: `https://app-snaps.ring.com/snapshots/next/${this.id}`,
|
|
366
355
|
responseType: 'buffer',
|
|
367
356
|
searchParams: {
|
|
368
357
|
'after-ms': afterMs,
|
|
369
358
|
'max-wait-ms': maxWaitMs,
|
|
370
359
|
extras: force ? 'force' : undefined,
|
|
360
|
+
uuid,
|
|
371
361
|
},
|
|
372
362
|
headers: {
|
|
373
363
|
accept: 'image/jpeg',
|
|
@@ -378,6 +368,15 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
378
368
|
return response;
|
|
379
369
|
});
|
|
380
370
|
}
|
|
371
|
+
getSnapshotByUuid(uuid) {
|
|
372
|
+
return this.restClient.request({
|
|
373
|
+
url: (0, rest_client_1.clientApi)('snapshots/uuid?uuid=' + uuid),
|
|
374
|
+
responseType: 'buffer',
|
|
375
|
+
headers: {
|
|
376
|
+
accept: 'image/jpeg',
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
}
|
|
381
380
|
recordToFile(outputPath, duration = 30) {
|
|
382
381
|
return __awaiter(this, void 0, void 0, function* () {
|
|
383
382
|
const liveCall = yield this.streamVideo({
|
package/lib/api/ring-types.d.ts
CHANGED
|
@@ -87,7 +87,6 @@ export declare enum RingCameraKind {
|
|
|
87
87
|
cocoa_doorbell = "cocoa_doorbell",
|
|
88
88
|
stickup_cam_mini = "stickup_cam_mini"
|
|
89
89
|
}
|
|
90
|
-
export declare function isBatteryCameraKind(kind: RingCameraKind): boolean;
|
|
91
90
|
export declare const RingCameraModel: {
|
|
92
91
|
readonly [P in RingCameraKind]: string;
|
|
93
92
|
};
|
|
@@ -379,6 +378,13 @@ export interface CameraData {
|
|
|
379
378
|
live_view_disabled?: boolean;
|
|
380
379
|
motion_detection_enabled?: boolean;
|
|
381
380
|
power_mode?: 'battery' | 'wired';
|
|
381
|
+
sheila_settings: {
|
|
382
|
+
cv_processing_enabled: boolean | null;
|
|
383
|
+
local_storage_enabled: boolean | null;
|
|
384
|
+
};
|
|
385
|
+
server_settings: {
|
|
386
|
+
ring_media_server_enabled: boolean;
|
|
387
|
+
};
|
|
382
388
|
};
|
|
383
389
|
features: {
|
|
384
390
|
motions_enabled: boolean;
|
|
@@ -420,6 +426,44 @@ export interface CameraData {
|
|
|
420
426
|
ends_at?: string;
|
|
421
427
|
seconds_remaining: number;
|
|
422
428
|
};
|
|
429
|
+
health: {
|
|
430
|
+
device_type: RingCameraKind;
|
|
431
|
+
last_update_time: number;
|
|
432
|
+
connected: boolean;
|
|
433
|
+
rss_connected: boolean;
|
|
434
|
+
vod_enabled: boolean;
|
|
435
|
+
sidewalk_connection: boolean;
|
|
436
|
+
floodlight_on: boolean;
|
|
437
|
+
siren_on?: boolean;
|
|
438
|
+
white_led_on: boolean;
|
|
439
|
+
night_mode_on: boolean;
|
|
440
|
+
hatch_open: boolean;
|
|
441
|
+
packet_loss: number;
|
|
442
|
+
packet_loss_category: 'good' | string;
|
|
443
|
+
rssi: number;
|
|
444
|
+
battery_voltage: number;
|
|
445
|
+
wifi_is_ring_network: boolean;
|
|
446
|
+
supported_rpc_commands: string[];
|
|
447
|
+
ota_status: 'successful' | string;
|
|
448
|
+
ext_power_state?: 0 | number;
|
|
449
|
+
pref_run_mode?: 'low_power' | string;
|
|
450
|
+
run_mode?: 'low_power' | 'full_power' | string;
|
|
451
|
+
network_connection_value: 'wifi' | string;
|
|
452
|
+
ac_power: number;
|
|
453
|
+
battery_present?: boolean;
|
|
454
|
+
external_connection: boolean;
|
|
455
|
+
battery_percentage?: number;
|
|
456
|
+
battery_percentage_category: 'very_good' | 'unknown' | string;
|
|
457
|
+
firmware_version: string;
|
|
458
|
+
rssi_category: 'good' | string;
|
|
459
|
+
battery_voltage_category: 'very_good' | string;
|
|
460
|
+
second_battery_voltage_category: 'unknown' | string;
|
|
461
|
+
second_battery_percentage_category: 'unknown' | string;
|
|
462
|
+
battery_save: boolean;
|
|
463
|
+
firmware_version_status: 'Up to Date';
|
|
464
|
+
tx_rate: number;
|
|
465
|
+
ptz_connected?: 'penguin';
|
|
466
|
+
};
|
|
423
467
|
}
|
|
424
468
|
export interface CvDetectionType {
|
|
425
469
|
enabled: boolean;
|
|
@@ -663,31 +707,44 @@ export interface RingDeviceHistoryEvent {
|
|
|
663
707
|
body: any;
|
|
664
708
|
}
|
|
665
709
|
export declare type DingState = 'ringing' | 'connected' | 'timed_out' | 'completed';
|
|
666
|
-
export
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
710
|
+
export declare enum NotificationDetectionType {
|
|
711
|
+
Human = "human",
|
|
712
|
+
Loitering = "loitering",
|
|
713
|
+
Motion = "motion",
|
|
714
|
+
OtherMotion = "other_motion",
|
|
715
|
+
NotAvailable = "null",
|
|
716
|
+
StreamBroken = "stream_broken"
|
|
717
|
+
}
|
|
718
|
+
export declare enum PushNotificationAction {
|
|
719
|
+
Ding = "com.ring.push.HANDLE_NEW_DING",
|
|
720
|
+
Motion = "com.ring.push.HANDLE_NEW_motion"
|
|
721
|
+
}
|
|
722
|
+
export interface PushNotification {
|
|
723
|
+
ding: {
|
|
724
|
+
streaming_protocol: 'ring_media_server';
|
|
725
|
+
location_id: string;
|
|
726
|
+
device_name: string;
|
|
727
|
+
doorbot_id: number;
|
|
728
|
+
e2ee_enabled: boolean;
|
|
729
|
+
streaming_data_hash: string;
|
|
730
|
+
device_kind: RingCameraKind;
|
|
731
|
+
detection_type: NotificationDetectionType;
|
|
732
|
+
human_detected?: boolean;
|
|
733
|
+
id: number;
|
|
734
|
+
pod_id: number;
|
|
735
|
+
request_id: string;
|
|
736
|
+
image_uuid: string;
|
|
737
|
+
properties: {
|
|
738
|
+
active_streaming_profile: 'rms';
|
|
739
|
+
is_sidewalk: boolean;
|
|
740
|
+
};
|
|
741
|
+
};
|
|
742
|
+
aps: {
|
|
743
|
+
alert: string;
|
|
744
|
+
sound: string;
|
|
745
|
+
};
|
|
746
|
+
subtype: 'motion' | 'ding' | 'human' | string;
|
|
747
|
+
action: PushNotificationAction | string;
|
|
691
748
|
}
|
|
692
749
|
export interface LiveCallResponse {
|
|
693
750
|
ding_id: string;
|
package/lib/api/ring-types.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.isWebSocketSupportedAsset = exports.disabledLocationModes = exports.DispatchSignalType = exports.DoorbellType = exports.ChimeModel = exports.deviceTypesWithVolume = exports.allAlarmStates = exports.RingCameraModel = exports.
|
|
3
|
+
exports.isWebSocketSupportedAsset = exports.disabledLocationModes = exports.DispatchSignalType = exports.PushNotificationAction = exports.NotificationDetectionType = exports.DoorbellType = exports.ChimeModel = exports.deviceTypesWithVolume = exports.allAlarmStates = exports.RingCameraModel = exports.RingCameraKind = exports.RingDeviceCategory = exports.RingDeviceType = void 0;
|
|
4
4
|
// eslint-disable-next-line no-shadow
|
|
5
5
|
var RingDeviceType;
|
|
6
6
|
(function (RingDeviceType) {
|
|
@@ -95,12 +95,6 @@ var RingCameraKind;
|
|
|
95
95
|
RingCameraKind["cocoa_doorbell"] = "cocoa_doorbell";
|
|
96
96
|
RingCameraKind["stickup_cam_mini"] = "stickup_cam_mini";
|
|
97
97
|
})(RingCameraKind = exports.RingCameraKind || (exports.RingCameraKind = {}));
|
|
98
|
-
// RegExp taken from ring.com app
|
|
99
|
-
const isWiredCameraRegExp = new RegExp(/(^(lpd|jbox|stickup_cam_elite|stickup_cam_mini|hp_cam|spotlightw|floodlight|doorbell_graham_cracker))/);
|
|
100
|
-
function isBatteryCameraKind(kind) {
|
|
101
|
-
return !kind.match(isWiredCameraRegExp);
|
|
102
|
-
}
|
|
103
|
-
exports.isBatteryCameraKind = isBatteryCameraKind;
|
|
104
98
|
exports.RingCameraModel = {
|
|
105
99
|
doorbot: 'Doorbell',
|
|
106
100
|
doorbell: 'Doorbell',
|
|
@@ -158,6 +152,23 @@ var DoorbellType;
|
|
|
158
152
|
DoorbellType[DoorbellType["None"] = 2] = "None";
|
|
159
153
|
})(DoorbellType = exports.DoorbellType || (exports.DoorbellType = {}));
|
|
160
154
|
// eslint-disable-next-line no-shadow
|
|
155
|
+
var NotificationDetectionType;
|
|
156
|
+
(function (NotificationDetectionType) {
|
|
157
|
+
// Note, this list may not be complete
|
|
158
|
+
NotificationDetectionType["Human"] = "human";
|
|
159
|
+
NotificationDetectionType["Loitering"] = "loitering";
|
|
160
|
+
NotificationDetectionType["Motion"] = "motion";
|
|
161
|
+
NotificationDetectionType["OtherMotion"] = "other_motion";
|
|
162
|
+
NotificationDetectionType["NotAvailable"] = "null";
|
|
163
|
+
NotificationDetectionType["StreamBroken"] = "stream_broken";
|
|
164
|
+
})(NotificationDetectionType = exports.NotificationDetectionType || (exports.NotificationDetectionType = {}));
|
|
165
|
+
// eslint-disable-next-line no-shadow
|
|
166
|
+
var PushNotificationAction;
|
|
167
|
+
(function (PushNotificationAction) {
|
|
168
|
+
PushNotificationAction["Ding"] = "com.ring.push.HANDLE_NEW_DING";
|
|
169
|
+
PushNotificationAction["Motion"] = "com.ring.push.HANDLE_NEW_motion";
|
|
170
|
+
})(PushNotificationAction = exports.PushNotificationAction || (exports.PushNotificationAction = {}));
|
|
171
|
+
// eslint-disable-next-line no-shadow
|
|
161
172
|
var DispatchSignalType;
|
|
162
173
|
(function (DispatchSignalType) {
|
|
163
174
|
DispatchSignalType["Burglar"] = "user-verified-burglar-xa";
|
package/lib/api/util.js
CHANGED
|
@@ -18,7 +18,7 @@ const colors_1 = require("colors");
|
|
|
18
18
|
const readline_1 = require("readline");
|
|
19
19
|
const uuid_1 = require("uuid");
|
|
20
20
|
const systeminformation_1 = require("systeminformation");
|
|
21
|
-
const debugLogger = (0, debug_1.default)('ring'), uuidNamespace = 'e53ffdc0-e91d-4ce1-bec2-
|
|
21
|
+
const debugLogger = (0, debug_1.default)('ring'), uuidNamespace = 'e53ffdc0-e91d-4ce1-bec2-df939d94739d';
|
|
22
22
|
let logger = {
|
|
23
23
|
logInfo(message) {
|
|
24
24
|
debugLogger(message);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ring-client-api",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.4",
|
|
4
4
|
"description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
|
|
5
5
|
"main": "lib/api/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"scripts": {
|
|
11
11
|
"start": "concurrently -c yellow,blue --kill-others \"npm:hb\" \"npm:watch:ui\"",
|
|
12
12
|
"watch:ui": "npm start --prefix homebridge-ui",
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "eslint '**/*.ts'",
|
|
14
14
|
"watch:tests": "jest --watch",
|
|
15
15
|
"lint": "eslint '**/*.ts' --fix",
|
|
16
16
|
"build": "rm -rf lib && tsc --declaration",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"author": "dgreif",
|
|
56
56
|
"license": "MIT",
|
|
57
57
|
"dependencies": {
|
|
58
|
+
"@eneris/push-receiver": "^3.0.1",
|
|
58
59
|
"@homebridge/camera-utils": "2.1.0",
|
|
59
60
|
"@homebridge/plugin-ui-utils": "0.0.19",
|
|
60
61
|
"@koush/werift": "0.14.5-beta7",
|
package/lib/api/rtp-utils.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { DingKind } from './ring-types';
|
|
2
|
-
export interface LiveCallSession {
|
|
3
|
-
alexa_port: 18443;
|
|
4
|
-
app_session_token: string;
|
|
5
|
-
availability_zone: 'availability-zone';
|
|
6
|
-
custom_timer: {
|
|
7
|
-
max_sec: number;
|
|
8
|
-
};
|
|
9
|
-
ding_id: string;
|
|
10
|
-
ding_kind: DingKind;
|
|
11
|
-
doorbot_id: number;
|
|
12
|
-
exp: number;
|
|
13
|
-
ip: string;
|
|
14
|
-
port: number;
|
|
15
|
-
private_ip: string;
|
|
16
|
-
rms_fqdn: string;
|
|
17
|
-
rms_version: string;
|
|
18
|
-
rsp_port: number;
|
|
19
|
-
rtsp_port: number;
|
|
20
|
-
session_id: string;
|
|
21
|
-
sip_port: number;
|
|
22
|
-
webrtc_port: number;
|
|
23
|
-
webrtc_url: string;
|
|
24
|
-
wwr_port: number;
|
|
25
|
-
}
|
|
26
|
-
export declare function parseLiveCallSession(sessionId: string): LiveCallSession;
|
package/lib/api/rtp-utils.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseLiveCallSession = void 0;
|
|
4
|
-
function parseLiveCallSession(sessionId) {
|
|
5
|
-
const encodedSession = sessionId.split('.')[1], buff = Buffer.from(encodedSession, 'base64'), text = buff.toString('ascii');
|
|
6
|
-
return JSON.parse(text);
|
|
7
|
-
}
|
|
8
|
-
exports.parseLiveCallSession = parseLiveCallSession;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const api_1 = require("../../api");
|
|
4
|
-
describe('Ring Camera', () => {
|
|
5
|
-
describe('battery level', () => {
|
|
6
|
-
it('should handle string battery life', () => {
|
|
7
|
-
expect((0, api_1.getBatteryLevel)({ battery_life: '49' })).toEqual(49);
|
|
8
|
-
});
|
|
9
|
-
it('should handle null battery life', () => {
|
|
10
|
-
expect((0, api_1.getBatteryLevel)({ battery_life: null })).toEqual(null);
|
|
11
|
-
});
|
|
12
|
-
it('should handle right battery only', () => {
|
|
13
|
-
expect((0, api_1.getBatteryLevel)({ battery_life: null, battery_life_2: 24 })).toEqual(24);
|
|
14
|
-
});
|
|
15
|
-
it('should handle left battery only', () => {
|
|
16
|
-
expect((0, api_1.getBatteryLevel)({ battery_life: 76, battery_life_2: null })).toEqual(76);
|
|
17
|
-
});
|
|
18
|
-
it('should handle dual batteries', () => {
|
|
19
|
-
expect((0, api_1.getBatteryLevel)({ battery_life: '92', battery_life_2: 84 })).toEqual(84);
|
|
20
|
-
expect((0, api_1.getBatteryLevel)({ battery_life: '92', battery_life_2: 100 })).toEqual(92);
|
|
21
|
-
});
|
|
22
|
-
});
|
|
23
|
-
});
|