homebridge-ring 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/config.schema.json +0 -14
- 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/homebridge/camera-source.d.ts +2 -1
- package/lib/homebridge/camera-source.js +7 -6
- package/lib/homebridge/camera.js +6 -7
- package/lib/homebridge/ring-platform.js +2 -3
- package/lib/test/api/ring-camera.spec.js +3 -1
- package/package.json +23 -22
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
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.5](https://github.com/dgreif/ring/compare/v10.0.0-beta.4...v10.0.0-beta.5) (2022-04-10)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* **cameras:** wait to activate session until connected ([9a49389](https://github.com/dgreif/ring/commit/9a49389aa434cc5df4cfb4ba039bad0f2a30f756))
|
|
11
|
+
* handle push notification for unknown camera ([cc8fc2a](https://github.com/dgreif/ring/commit/cc8fc2a6377084515d3b169a85644b2a2b13235e))
|
|
12
|
+
* hide ECONNRESET errors from push receiver ([1196d96](https://github.com/dgreif/ring/commit/1196d96e67e26952f18dfbe75103419a7261bd1e))
|
|
13
|
+
* restore dual battery level calculations ([26fa84d](https://github.com/dgreif/ring/commit/26fa84dbd4bbcea8208fd0ade81c54c407aabf63))
|
|
14
|
+
* update dependencies ([4b97fb3](https://github.com/dgreif/ring/commit/4b97fb33623248147997944bab3de4ea3c2e5ee2))
|
|
15
|
+
|
|
16
|
+
## [10.0.0-beta.4](https://github.com/dgreif/ring/compare/v10.0.0-beta.3...v10.0.0-beta.4) (2022-04-03)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
### ⚠ BREAKING CHANGES
|
|
20
|
+
|
|
21
|
+
* **api:** All ding-related functionality will now use the `PushNotification` type, and the `ActiveDing` type has been removed
|
|
22
|
+
* **api:** The `cameraDingsPollingSeconds` option has been removed. Ding events will now be received via push notifications (no setup required)
|
|
23
|
+
* **api:** The `treatKnockAsDing` option has been removed. Knocks should now register as "dings" automatically, though I have not been able to test this functionality
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* **api:** convert ding handling to use notifications ([5b62318](https://github.com/dgreif/ring/commit/5b62318b6a71bcc224a2b29cbd6e21162fc9891e))
|
|
28
|
+
* **api:** update notification types ([15d3e7e](https://github.com/dgreif/ring/commit/15d3e7e40744e2c3ed48140fa94fbc0c6f9d4088))
|
|
29
|
+
* **homebridge:** faster snapshots from camera notifications ([7fc821b](https://github.com/dgreif/ring/commit/7fc821b71013c36314d6d428b8152e631ea4b497))
|
|
30
|
+
* receive dings from push notifications ([c9f130f](https://github.com/dgreif/ring/commit/c9f130f0f1f21b6db2454d611641ef7c29c7e620))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
### Bug Fixes
|
|
34
|
+
|
|
35
|
+
* more closely match ring edge live streaming setup flow ([4bb3189](https://github.com/dgreif/ring/commit/4bb3189d6ce6614f7af64d04b410c3922957d555))
|
|
36
|
+
* update uuid namespace to force new client sessions ([bc3e943](https://github.com/dgreif/ring/commit/bc3e9432c41dda085845f72f332dcf480c92475d))
|
|
37
|
+
* 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)
|
|
38
|
+
|
|
39
|
+
## [10.0.0-beta.3](https://github.com/dgreif/ring/compare/v10.0.0-beta.2...v10.0.0-beta.3) (2022-03-26)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
### Bug Fixes
|
|
43
|
+
|
|
44
|
+
* separate streaming setup for ring edge vs regular cameras ([6281cf9](https://github.com/dgreif/ring/commit/6281cf966de12dcea8acc6516cd9079634734dec))
|
|
45
|
+
|
|
5
46
|
## [10.0.0-beta.2](https://github.com/dgreif/ring/compare/v10.0.0-beta.1...v10.0.0-beta.2) (2022-03-26)
|
|
6
47
|
|
|
7
48
|
|
package/README.md
CHANGED
|
@@ -68,7 +68,6 @@ Only include an optional parameter if you actually need it. Default behavior wi
|
|
|
68
68
|
"avoidSnapshotBatteryDrain": true,
|
|
69
69
|
"treatKnockAsDing": true,
|
|
70
70
|
"cameraStatusPollingSeconds": 20,
|
|
71
|
-
"cameraDingsPollingSeconds": 2,
|
|
72
71
|
"locationModePollingSeconds": 20,
|
|
73
72
|
"locationIds": ["488e4800-fcde-4493-969b-d1a06f683102", "4bbed7a7-06df-4f18-b3af-291c89854d60"]
|
|
74
73
|
}
|
|
@@ -92,9 +91,7 @@ Option | Default | Explanation
|
|
|
92
91
|
`showPanicButtons` | `false` | Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk. I do not guarantee functionality in case of emergency, nor do I take responsibility for any false alarms**. These function just like the SOS sliders in the Ring app.
|
|
93
92
|
`nightModeBypassFor` | no Night Mode | Allows you to use Night mode to "Bypass and Arm" a Ring Alarm. Can be set to `all` for Away or `some` for Home. If set and Night mode is activated from HomeKit, any open contact sensors will automatically be bypassed.
|
|
94
93
|
`avoidSnapshotBatteryDrain` | false | Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery.
|
|
95
|
-
`treatKnockAsDing` | false | When someone knocks on your door, it will be recognized as a doorbell press in HomeKit. Limited to select camera models (Peephole Cam).
|
|
96
94
|
`cameraStatusPollingSeconds` | `20` | 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.
|
|
97
|
-
`cameraDingsPollingSeconds` | `2` | How frequently to poll for new events from your cameras. These include motion and doorbell presses.
|
|
98
95
|
`locationModePollingSeconds` | `20` | 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. Will hide Location Mode switch if set to `0`
|
|
99
96
|
`locationIds` | All Locations | Use this option if you only want a subset of your locations to appear in HomeKit. If this option is not included, all of your locations will be added to HomeKit (which is what most users will want to do).
|
|
100
97
|
`onlyDeviceTypes` | All Device Types | A subset of device types to be shown. If set, all other device types will be hidden in HomeKit.
|
package/config.schema.json
CHANGED
|
@@ -86,11 +86,6 @@
|
|
|
86
86
|
"type": "boolean",
|
|
87
87
|
"description": "Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery. This is useful if have the Home app open constantly, causing frequent snapshot requests."
|
|
88
88
|
},
|
|
89
|
-
"treatKnockAsDing": {
|
|
90
|
-
"title": "Treat Knock as Ding",
|
|
91
|
-
"type": "boolean",
|
|
92
|
-
"description": "When someone knocks on your door, it will be recognized as a doorbell press in HomeKit. Limited to select camera models (Peephole Cam)"
|
|
93
|
-
},
|
|
94
89
|
"sendCameraMotionNotificationsToTv": {
|
|
95
90
|
"title": "Create a 'Doorbell' accessory triggered by non-doorbell camera motion alerts for tvOS 14 notification support",
|
|
96
91
|
"type": "boolean"
|
|
@@ -118,13 +113,6 @@
|
|
|
118
113
|
"placeholder": "Get camera status every __ seconds",
|
|
119
114
|
"description": "Camera/Chime status (such as light/siren/volume/snooze state) does not update live and must be fetched periodically. By default this is done every 20 seconds"
|
|
120
115
|
},
|
|
121
|
-
"cameraDingsPollingSeconds": {
|
|
122
|
-
"title": "Camera Event Polling",
|
|
123
|
-
"type": "integer",
|
|
124
|
-
"minimum": 1,
|
|
125
|
-
"placeholder": "Get camera events every __ seconds",
|
|
126
|
-
"description": "Camera motion and ding events does not update live and must be fetched periodically. By default this is done every 2 seconds"
|
|
127
|
-
},
|
|
128
116
|
"locationModePollingSeconds": {
|
|
129
117
|
"title": "Location Mode Polling",
|
|
130
118
|
"type": "integer",
|
|
@@ -179,13 +167,11 @@
|
|
|
179
167
|
"showPanicButtons",
|
|
180
168
|
"nightModeBypassFor",
|
|
181
169
|
"avoidSnapshotBatteryDrain",
|
|
182
|
-
"treatKnockAsDing",
|
|
183
170
|
"sendCameraMotionNotificationsToTv",
|
|
184
171
|
"sendDoorbellMotionNotificationsToTv",
|
|
185
172
|
"beamDurationSeconds",
|
|
186
173
|
"debug",
|
|
187
174
|
"cameraStatusPollingSeconds",
|
|
188
|
-
"cameraDingsPollingSeconds",
|
|
189
175
|
"locationModePollingSeconds",
|
|
190
176
|
{
|
|
191
177
|
"key": "hideDeviceIds",
|
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, _b;
|
|
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
|
+
(_b = camerasById[notification.ding.doorbot_id]) === null || _b === void 0 ? void 0 : _b.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
|
}
|
package/lib/api/index.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
3
|
if (k2 === undefined) k2 = k;
|
|
4
|
-
Object.
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
5
9
|
}) : (function(o, m, k, k2) {
|
|
6
10
|
if (k2 === undefined) k2 = k;
|
|
7
11
|
o[k2] = m[k];
|
|
@@ -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.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')));
|
|
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.d.ts
CHANGED
|
@@ -10,29 +10,25 @@ export interface FfmpegOptions {
|
|
|
10
10
|
output: SpawnInput[];
|
|
11
11
|
}
|
|
12
12
|
export declare class LiveCall extends Subscribed {
|
|
13
|
+
private sessionId;
|
|
13
14
|
private camera;
|
|
14
15
|
private readonly ws;
|
|
15
16
|
private readonly onWsOpen;
|
|
16
17
|
private readonly onMessage;
|
|
17
18
|
private readonly pc;
|
|
18
|
-
readonly onSessionId: ReplaySubject<string>;
|
|
19
19
|
readonly onCallAnswered: ReplaySubject<string>;
|
|
20
20
|
readonly onCallEnded: ReplaySubject<void>;
|
|
21
21
|
private readonly audioSplitter;
|
|
22
22
|
private readonly videoSplitter;
|
|
23
23
|
readonly onVideoRtp: import("rxjs").Subject<RtpPacket>;
|
|
24
24
|
readonly onAudioRtp: import("rxjs").Subject<RtpPacket>;
|
|
25
|
-
|
|
26
|
-
constructor(authToken: string, camera: RingCamera);
|
|
27
|
-
private initiateCall;
|
|
28
|
-
private sessionId;
|
|
25
|
+
constructor(sessionId: string, camera: RingCamera);
|
|
29
26
|
private handleMessage;
|
|
30
27
|
reservePort(bufferPorts?: number): Promise<number>;
|
|
31
28
|
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
32
29
|
activated: boolean;
|
|
33
30
|
activate(): Promise<void>;
|
|
34
31
|
activateCameraSpeaker(): Promise<void>;
|
|
35
|
-
private sendSessionMessage;
|
|
36
32
|
private sendMessage;
|
|
37
33
|
private callEnded;
|
|
38
34
|
stop(): void;
|