homebridge-ring 9.22.1 → 9.25.0-beta.1
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 +43 -0
- package/README.md +8 -0
- package/config.schema.json +11 -0
- package/lib/api/api.d.ts +1 -0
- package/lib/api/api.js +4 -2
- package/lib/api/live-call.d.ts +41 -0
- package/lib/api/live-call.js +201 -0
- package/lib/api/peer-connection.d.ts +17 -0
- package/lib/api/peer-connection.js +96 -0
- package/lib/api/rest-client.d.ts +3 -0
- package/lib/api/rest-client.js +18 -6
- package/lib/api/ring-camera.d.ts +14 -8
- package/lib/api/ring-camera.js +101 -99
- package/lib/api/ring-chime.d.ts +1 -1
- package/lib/api/ring-chime.js +1 -1
- package/lib/api/ring-types.d.ts +17 -4
- package/lib/api/ring-types.js +4 -1
- package/lib/api/rtp-utils.d.ts +26 -0
- package/lib/api/rtp-utils.js +6 -1
- package/lib/api/sip-session.d.ts +4 -0
- package/lib/api/sip-session.js +26 -22
- package/lib/api/sip.d.ts +1 -0
- package/lib/api/sip.js +1 -0
- package/lib/api/util.js +5 -2
- package/lib/homebridge/camera-source.d.ts +1 -1
- package/lib/homebridge/camera-source.js +239 -218
- package/lib/homebridge/camera.js +1 -1
- package/lib/homebridge/config.d.ts +1 -0
- package/lib/homebridge/ring-platform.js +1 -0
- package/package.json +27 -23
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,49 @@
|
|
|
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
|
+
## [9.25.0-beta.1](https://github.com/dgreif/ring/compare/v9.25.0-beta.0...v9.25.0-beta.1) (2022-02-14)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* use new WebRTC endpoint for live calls ([6e7779f](https://github.com/dgreif/ring/commit/6e7779feef9789583921fc44bab32c099f3026d2))
|
|
11
|
+
|
|
12
|
+
## [9.25.0-beta.0](https://github.com/dgreif/ring/compare/v9.24.0...v9.25.0-beta.0) (2022-01-29)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **api:** decouple ffmpeg prep from execution ([#859](https://github.com/dgreif/ring/issues/859)) ([e3cc9d1](https://github.com/dgreif/ring/commit/e3cc9d15584377bf2be94a9d457efc3dfcb4bd8b))
|
|
18
|
+
* handle new camera types with no battery ([11ae286](https://github.com/dgreif/ring/commit/11ae28697d8cb0e2da71e880c82e1883f04047b9))
|
|
19
|
+
* slightly faster snapshots via new endpoint ([b840471](https://github.com/dgreif/ring/commit/b840471bc6b7b66e5873370251bd57c1a35b267a))
|
|
20
|
+
|
|
21
|
+
## [9.24.0](https://github.com/dgreif/ring/compare/v9.23.0...v9.24.0) (2022-01-22)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Features
|
|
25
|
+
|
|
26
|
+
* support glassbreak sensor ([98271f9](https://github.com/dgreif/ring/commit/98271f91f13aaae051fccaab7165cd17f2a2377c))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Bug Fixes
|
|
30
|
+
|
|
31
|
+
* allow process to exit when RingApi.disconnect() is called ([#851](https://github.com/dgreif/ring/issues/851)) ([8ef25ac](https://github.com/dgreif/ring/commit/8ef25acca66c533873b7fd43c7c899e34b4a7617))
|
|
32
|
+
* avoid error when ring terminates video stream ([7717d5e](https://github.com/dgreif/ring/commit/7717d5eaa40afed3d34f0e8e6eea1402e21afef7)), closes [#853](https://github.com/dgreif/ring/issues/853)
|
|
33
|
+
* update dependencies ([c80d052](https://github.com/dgreif/ring/commit/c80d052f9eaf4c0a50a495166177daa40299ffa7))
|
|
34
|
+
|
|
35
|
+
## [9.23.0](https://github.com/dgreif/ring/compare/v9.22.1...v9.23.0) (2021-10-17)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Features
|
|
39
|
+
|
|
40
|
+
* **homebridge:** config option to hide camera lights ([58be17c](https://github.com/dgreif/ring/commit/58be17cfee524bfb158566e59d60438e9b6f0f5d)), closes [#802](https://github.com/dgreif/ring/issues/802)
|
|
41
|
+
* **homebridge:** option to treat "knock" as a ding ([7ece8b2](https://github.com/dgreif/ring/commit/7ece8b25314ef403a6c2bab6a28476a3b7229507)), closes [#514](https://github.com/dgreif/ring/issues/514)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
### Bug Fixes
|
|
45
|
+
|
|
46
|
+
* update dependencies ([b685e27](https://github.com/dgreif/ring/commit/b685e27e952a94635a277848821ebfa640d50ee2))
|
|
47
|
+
|
|
5
48
|
### [9.22.1](https://github.com/dgreif/ring/compare/v9.22.0...v9.22.1) (2021-10-13)
|
|
6
49
|
|
|
7
50
|
|
package/README.md
CHANGED
|
@@ -57,6 +57,7 @@ Only include an optional parameter if you actually need it. Default behavior wi
|
|
|
57
57
|
"hideDeviceIds": ["477e4800-fcde-4493-969b-d1a06f683102", "5aaed7a7-06df-4f18-b3af-291c89854d60"],
|
|
58
58
|
"hideLightGroups": true,
|
|
59
59
|
"hideDoorbellSwitch": true,
|
|
60
|
+
"hideCameraLight": true,
|
|
60
61
|
"hideCameraMotionSensor": true,
|
|
61
62
|
"sendCameraMotionNotificationsToTv": true,
|
|
62
63
|
"sendDoorbellMotionNotificationsToTv": true,
|
|
@@ -65,6 +66,7 @@ Only include an optional parameter if you actually need it. Default behavior wi
|
|
|
65
66
|
"hideAlarmSirenSwitch": true,
|
|
66
67
|
"hideUnsupportedServices": true,
|
|
67
68
|
"avoidSnapshotBatteryDrain": true,
|
|
69
|
+
"treatKnockAsDing": true,
|
|
68
70
|
"cameraStatusPollingSeconds": 20,
|
|
69
71
|
"cameraDingsPollingSeconds": 2,
|
|
70
72
|
"locationModePollingSeconds": 20,
|
|
@@ -79,6 +81,7 @@ Option | Default | Explanation
|
|
|
79
81
|
`hideDeviceIds` | `[]` | Allows you to hide specific devices by an array of ids. The id for each device is logged when homebridge starts.
|
|
80
82
|
`hideLightGroups` | `false` | Ring smart lighting allows you to create lighting groups within the Ring app. These groups are convenient for detecting motion in an area of your yard and turning on/off all lights in the group. However, you may wish to group the lights differently in HomeKit and ignore the groups you have configured in Ring. If this option is `true`, your Ring groups (and their associated motion sensor) will be ignored and will not show up in HomeKit.
|
|
81
83
|
`hideDoorbellSwitch` | `false` | If you have a Ring video doorbell, you will see a Programmable Switch associated with it. This switch can be used to perform actions on when the doorbell is pressed using "Single Press" actions. If you do not care to perform actions when the doorbell is pressed, you can hide the Programmable Switch by setting this option to `true`. You will still be able to receive _notifications_ from the doorbell even if the Programmable Switch is hidden (notifications can be configured in the settings for the doorbell camera in the Home app)
|
|
84
|
+
`hideCameraLight` | `false` | If `true`, hides the light for Ring cameras in HomeKit.
|
|
82
85
|
`hideCameraMotionSensor` | `false` | If `true`, hides the motion sensor for Ring cameras in HomeKit.
|
|
83
86
|
`sendCameraMotionNotificationsToTv` | `false` | If `true`, creates a 'Doorbell' accessory triggered by non-doorbell camera motion alerts for tvOS 14 notification support.
|
|
84
87
|
`sendDoorbellMotionNotificationsToTv` | `false` | If `true`, motion events on doorbell cameras will trigger a doorbell press event for tvOS 14 notification support.
|
|
@@ -89,6 +92,7 @@ Option | Default | Explanation
|
|
|
89
92
|
`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.
|
|
90
93
|
`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.
|
|
91
94
|
`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).
|
|
92
96
|
`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.
|
|
93
97
|
`cameraDingsPollingSeconds` | `2` | How frequently to poll for new events from your cameras. These include motion and doorbell presses.
|
|
94
98
|
`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`
|
|
@@ -161,6 +165,10 @@ If you are having issues with your cameras in the Home app, please see the [Came
|
|
|
161
165
|
* Detect water leak
|
|
162
166
|
* Tamper status
|
|
163
167
|
* Battery status
|
|
168
|
+
* Glassbreak Sensor (**untested**)
|
|
169
|
+
* Detect glass breaking
|
|
170
|
+
* Tamper status
|
|
171
|
+
* Battery status
|
|
164
172
|
* Ring Smart Lights (Motion Detector, Flood/Path/Step/Spot Lights, Bulbs, Transformer, Outdoor Smart Plug, Mailbox Sensor)
|
|
165
173
|
* On/Off
|
|
166
174
|
* Brightness Level
|
package/config.schema.json
CHANGED
|
@@ -31,6 +31,10 @@
|
|
|
31
31
|
"type": "boolean",
|
|
32
32
|
"description": "Hides the Programmable Switch for Ring doorbells. This switch is only useful for triggering an automation when the doorbell is pressed, so most users can safely hide it"
|
|
33
33
|
},
|
|
34
|
+
"hideCameraLight": {
|
|
35
|
+
"title": "Hide Camera Light",
|
|
36
|
+
"type": "boolean"
|
|
37
|
+
},
|
|
34
38
|
"hideCameraMotionSensor": {
|
|
35
39
|
"title": "Hide Camera Motion Sensors",
|
|
36
40
|
"type": "boolean"
|
|
@@ -82,6 +86,11 @@
|
|
|
82
86
|
"type": "boolean",
|
|
83
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."
|
|
84
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
|
+
},
|
|
85
94
|
"sendCameraMotionNotificationsToTv": {
|
|
86
95
|
"title": "Create a 'Doorbell' accessory triggered by non-doorbell camera motion alerts for tvOS 14 notification support",
|
|
87
96
|
"type": "boolean"
|
|
@@ -161,6 +170,7 @@
|
|
|
161
170
|
"alarmOnEntryDelay",
|
|
162
171
|
"hideLightGroups",
|
|
163
172
|
"hideDoorbellSwitch",
|
|
173
|
+
"hideCameraLight",
|
|
164
174
|
"hideCameraMotionSensor",
|
|
165
175
|
"hideCameraSirenSwitch",
|
|
166
176
|
"hideInHomeDoorbellSwitch",
|
|
@@ -169,6 +179,7 @@
|
|
|
169
179
|
"showPanicButtons",
|
|
170
180
|
"nightModeBypassFor",
|
|
171
181
|
"avoidSnapshotBatteryDrain",
|
|
182
|
+
"treatKnockAsDing",
|
|
172
183
|
"sendCameraMotionNotificationsToTv",
|
|
173
184
|
"sendDoorbellMotionNotificationsToTv",
|
|
174
185
|
"beamDurationSeconds",
|
package/lib/api/api.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface RingApiOptions extends SessionOptions {
|
|
|
9
9
|
cameraDingsPollingSeconds?: number;
|
|
10
10
|
locationModePollingSeconds?: number;
|
|
11
11
|
avoidSnapshotBatteryDrain?: boolean;
|
|
12
|
+
treatKnockAsDing?: boolean;
|
|
12
13
|
debug?: boolean;
|
|
13
14
|
ffmpegPath?: string;
|
|
14
15
|
externalPorts?: {
|
package/lib/api/api.js
CHANGED
|
@@ -24,7 +24,8 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
24
24
|
super();
|
|
25
25
|
this.options = options;
|
|
26
26
|
this.restClient = new rest_client_1.RingRestClient(this.options);
|
|
27
|
-
this.onRefreshTokenUpdated =
|
|
27
|
+
this.onRefreshTokenUpdated =
|
|
28
|
+
this.restClient.onRefreshTokenUpdated.asObservable();
|
|
28
29
|
if (options.debug) {
|
|
29
30
|
(0, util_1.enableDebug)();
|
|
30
31
|
}
|
|
@@ -128,7 +129,7 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
128
129
|
return __awaiter(this, void 0, void 0, function* () {
|
|
129
130
|
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) ||
|
|
130
131
|
authorizedDoorbots.includes(data) ||
|
|
131
|
-
data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
|
|
132
|
+
data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false, this.options.treatKnockAsDing || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
|
|
132
133
|
.filter((location) => {
|
|
133
134
|
return (!Array.isArray(this.options.locationIds) ||
|
|
134
135
|
this.options.locationIds.includes(location.location_id));
|
|
@@ -169,6 +170,7 @@ class RingApi extends subscribed_1.Subscribed {
|
|
|
169
170
|
.catch((e) => {
|
|
170
171
|
(0, util_1.logError)(e);
|
|
171
172
|
});
|
|
173
|
+
this.restClient.clearTimeouts();
|
|
172
174
|
}
|
|
173
175
|
}
|
|
174
176
|
exports.RingApi = RingApi;
|
|
@@ -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 LiveCall extends Subscribed {
|
|
13
|
+
private sessionId;
|
|
14
|
+
private camera;
|
|
15
|
+
private readonly ws;
|
|
16
|
+
private readonly onWsOpen;
|
|
17
|
+
private readonly onMessage;
|
|
18
|
+
private readonly pc;
|
|
19
|
+
readonly onCallAnswered: ReplaySubject<void>;
|
|
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
|
+
constructor(sessionId: string, camera: RingCamera);
|
|
26
|
+
private handleMessage;
|
|
27
|
+
reservePort(bufferPorts?: number): Promise<number>;
|
|
28
|
+
prepareTranscoder(transcodeVideoStream: boolean, ffmpegInputOptions: SpawnInput[] | undefined, audioPort: number, videoPort: number, sdpInput: string): {
|
|
29
|
+
ffmpegInputArguments: SpawnInput[];
|
|
30
|
+
inputSdpLines: string[];
|
|
31
|
+
};
|
|
32
|
+
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
33
|
+
activated: boolean;
|
|
34
|
+
activate(): Promise<void>;
|
|
35
|
+
activateCameraSpeaker(): Promise<void>;
|
|
36
|
+
private sendMessage;
|
|
37
|
+
private callEnded;
|
|
38
|
+
stop(): void;
|
|
39
|
+
sendAudioPacket(rtp: RtpPacket): void;
|
|
40
|
+
}
|
|
41
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
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.LiveCall = void 0;
|
|
13
|
+
const rtp_utils_1 = require("./rtp-utils");
|
|
14
|
+
const ws_1 = require("ws");
|
|
15
|
+
const rxjs_1 = require("rxjs");
|
|
16
|
+
const peer_connection_1 = require("./peer-connection");
|
|
17
|
+
const util_1 = require("./util");
|
|
18
|
+
const operators_1 = require("rxjs/operators");
|
|
19
|
+
const subscribed_1 = require("./subscribed");
|
|
20
|
+
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
21
|
+
const ffmpeg_1 = require("./ffmpeg");
|
|
22
|
+
class LiveCall extends subscribed_1.Subscribed {
|
|
23
|
+
constructor(sessionId, camera) {
|
|
24
|
+
super();
|
|
25
|
+
this.sessionId = sessionId;
|
|
26
|
+
this.camera = camera;
|
|
27
|
+
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
28
|
+
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
29
|
+
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
30
|
+
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
31
|
+
this.activated = false;
|
|
32
|
+
const liveCallSession = (0, rtp_utils_1.parseLiveCallSession)(sessionId);
|
|
33
|
+
this.pc = new peer_connection_1.PeerConnection();
|
|
34
|
+
this.ws = new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
|
|
35
|
+
headers: {
|
|
36
|
+
API_VERSION: '3.1',
|
|
37
|
+
API_TOKEN: sessionId,
|
|
38
|
+
CLIENT_INFO: 'Ring/3.48.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
this.onAudioRtp = this.pc.onAudioRtp;
|
|
42
|
+
this.onVideoRtp = this.pc.onVideoRtp;
|
|
43
|
+
this.onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message');
|
|
44
|
+
this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
|
|
45
|
+
const onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
|
|
46
|
+
this.addSubscriptions(this.onMessage
|
|
47
|
+
.pipe((0, operators_1.concatMap)((message) => {
|
|
48
|
+
return this.handleMessage(JSON.parse(message.data));
|
|
49
|
+
}))
|
|
50
|
+
.subscribe(), this.onWsOpen.subscribe(() => {
|
|
51
|
+
(0, util_1.logDebug)(`WebSocket connected for ${this.camera.name}`);
|
|
52
|
+
}), onError.subscribe((e) => {
|
|
53
|
+
(0, util_1.logError)(e);
|
|
54
|
+
this.callEnded();
|
|
55
|
+
}), onClose.subscribe(() => {
|
|
56
|
+
this.callEnded();
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
handleMessage(message) {
|
|
60
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
61
|
+
switch (message.method) {
|
|
62
|
+
case 'sdp':
|
|
63
|
+
const answer = yield this.pc.createAnswer(message);
|
|
64
|
+
this.sendMessage(Object.assign({ method: 'sdp' }, answer));
|
|
65
|
+
this.onCallAnswered.next();
|
|
66
|
+
return;
|
|
67
|
+
case 'ice':
|
|
68
|
+
yield this.pc.addIceCandidate({
|
|
69
|
+
candidate: message.ice,
|
|
70
|
+
sdpMLineIndex: message.mlineindex,
|
|
71
|
+
});
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
reservePort(bufferPorts = 0) {
|
|
77
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
78
|
+
const ports = yield (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
|
|
79
|
+
return ports[0];
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
prepareTranscoder(transcodeVideoStream, ffmpegInputOptions, audioPort, videoPort, sdpInput) {
|
|
83
|
+
const ffmpegInputArguments = [
|
|
84
|
+
'-hide_banner',
|
|
85
|
+
'-protocol_whitelist',
|
|
86
|
+
'pipe,udp,rtp,file,crypto',
|
|
87
|
+
'-acodec',
|
|
88
|
+
'libopus',
|
|
89
|
+
'-f',
|
|
90
|
+
'sdp',
|
|
91
|
+
...(ffmpegInputOptions || []),
|
|
92
|
+
'-i',
|
|
93
|
+
sdpInput,
|
|
94
|
+
], inputSdpLines = [
|
|
95
|
+
'v=0',
|
|
96
|
+
'o=105202070 3747 461 IN IP4 127.0.0.1',
|
|
97
|
+
's=Talk',
|
|
98
|
+
'c=IN IP4 127.0.0.1',
|
|
99
|
+
'b=AS:380',
|
|
100
|
+
't=0 0',
|
|
101
|
+
'a=rtcp-xr:rcvr-rtt=all:10000 stat-summary=loss,dup,jitt,TTL voip-metrics',
|
|
102
|
+
`m=audio ${audioPort} RTP/SAVP 101`,
|
|
103
|
+
'a=rtpmap:101 OPUS/48000/2',
|
|
104
|
+
'a=rtcp-fb:101 nack pli',
|
|
105
|
+
'a=fmtp:101 useinbandfec=1;sprop-stereo=0',
|
|
106
|
+
'a=rtcp-mux',
|
|
107
|
+
];
|
|
108
|
+
if (transcodeVideoStream) {
|
|
109
|
+
inputSdpLines.push(`m=video ${videoPort} RTP/SAVP 96`, 'a=rtpmap:96 H264/90000', 'a=rtcp-fb:96 nack', 'a=rtcp-fb:96 nack pli', 'a=fmtp:96 packetization-mode=1;profile-level-id=640029;level-asymmetry-allowed=1', 'a=rtcp-mux');
|
|
110
|
+
this.addSubscriptions(this.onVideoRtp
|
|
111
|
+
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
112
|
+
return this.videoSplitter.send(rtp.serialize(), {
|
|
113
|
+
port: videoPort,
|
|
114
|
+
});
|
|
115
|
+
}))
|
|
116
|
+
.subscribe());
|
|
117
|
+
}
|
|
118
|
+
this.addSubscriptions(this.onAudioRtp
|
|
119
|
+
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
120
|
+
return this.audioSplitter.send(rtp.serialize(), {
|
|
121
|
+
port: audioPort,
|
|
122
|
+
});
|
|
123
|
+
}))
|
|
124
|
+
.subscribe());
|
|
125
|
+
return {
|
|
126
|
+
ffmpegInputArguments,
|
|
127
|
+
inputSdpLines,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
startTranscoding(ffmpegOptions) {
|
|
131
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
132
|
+
const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, { ffmpegInputArguments, inputSdpLines } = this.prepareTranscoder(transcodeVideoStream, ffmpegOptions.input, audioPort, videoPort, 'pipe:'), ff = new camera_utils_1.FfmpegProcess({
|
|
133
|
+
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
134
|
+
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
135
|
+
: []), ...(ffmpegOptions.output || [])),
|
|
136
|
+
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
137
|
+
exitCallback: () => this.callEnded(),
|
|
138
|
+
logLabel: `From Ring (${this.camera.name})`,
|
|
139
|
+
logger: {
|
|
140
|
+
error: util_1.logError,
|
|
141
|
+
info: util_1.logDebug,
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
this.onCallEnded.subscribe(() => ff.stop());
|
|
145
|
+
ff.writeStdin(inputSdpLines.filter((x) => Boolean(x)).join('\n'));
|
|
146
|
+
// Activate the stream now that ffmpeg is ready to receive
|
|
147
|
+
yield this.activate();
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
activate() {
|
|
151
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
152
|
+
if (this.activated) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
this.activated = true;
|
|
156
|
+
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
157
|
+
this.sendMessage({ method: 'activate_session' });
|
|
158
|
+
this.sendMessage({
|
|
159
|
+
video_enabled: true,
|
|
160
|
+
audio_enabled: true,
|
|
161
|
+
method: 'stream_options',
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
activateCameraSpeaker() {
|
|
166
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
167
|
+
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
168
|
+
this.sendMessage({
|
|
169
|
+
stealth_mode: false,
|
|
170
|
+
method: 'camera_options',
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
sendMessage(message) {
|
|
175
|
+
this.ws.send(JSON.stringify(message));
|
|
176
|
+
}
|
|
177
|
+
callEnded() {
|
|
178
|
+
try {
|
|
179
|
+
this.sendMessage({
|
|
180
|
+
reason: { code: 0, text: '' },
|
|
181
|
+
method: 'close',
|
|
182
|
+
});
|
|
183
|
+
this.ws.close();
|
|
184
|
+
}
|
|
185
|
+
catch (_) {
|
|
186
|
+
// ignore any errors since we are stopping the call
|
|
187
|
+
}
|
|
188
|
+
this.unsubscribe();
|
|
189
|
+
this.onCallEnded.next();
|
|
190
|
+
this.pc.close();
|
|
191
|
+
this.audioSplitter.close();
|
|
192
|
+
this.videoSplitter.close();
|
|
193
|
+
}
|
|
194
|
+
stop() {
|
|
195
|
+
this.callEnded();
|
|
196
|
+
}
|
|
197
|
+
sendAudioPacket(rtp) {
|
|
198
|
+
this.pc.returnAudioTrack.writeRtp(rtp);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
exports.LiveCall = LiveCall;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MediaStreamTrack, RTCIceCandidate, RTCPeerConnection, RtcpPacket, RtpPacket } from '@koush/werift';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
export declare class PeerConnection {
|
|
4
|
+
pc: RTCPeerConnection;
|
|
5
|
+
onAudioRtp: Subject<RtpPacket>;
|
|
6
|
+
onAudioRtcp: Subject<RtcpPacket>;
|
|
7
|
+
onVideoRtp: Subject<RtpPacket>;
|
|
8
|
+
onVideoRtcp: Subject<RtcpPacket>;
|
|
9
|
+
returnAudioTrack: MediaStreamTrack;
|
|
10
|
+
constructor();
|
|
11
|
+
createAnswer(offer: {
|
|
12
|
+
type: 'offer';
|
|
13
|
+
sdp: string;
|
|
14
|
+
}): Promise<import("@koush/werift").RTCSessionDescription>;
|
|
15
|
+
addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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.PeerConnection = void 0;
|
|
13
|
+
const werift_1 = require("@koush/werift");
|
|
14
|
+
const rxjs_1 = require("rxjs");
|
|
15
|
+
const util_1 = require("./util");
|
|
16
|
+
const debug = false;
|
|
17
|
+
class PeerConnection {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.onAudioRtp = new rxjs_1.Subject();
|
|
20
|
+
this.onAudioRtcp = new rxjs_1.Subject();
|
|
21
|
+
this.onVideoRtp = new rxjs_1.Subject();
|
|
22
|
+
this.onVideoRtcp = new rxjs_1.Subject();
|
|
23
|
+
this.returnAudioTrack = new werift_1.MediaStreamTrack({ kind: 'audio' });
|
|
24
|
+
const pc = (this.pc = new werift_1.RTCPeerConnection({
|
|
25
|
+
codecs: {
|
|
26
|
+
audio: [
|
|
27
|
+
new werift_1.RTCRtpCodecParameters({
|
|
28
|
+
mimeType: 'audio/opus',
|
|
29
|
+
clockRate: 48000,
|
|
30
|
+
channels: 2,
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
video: [
|
|
34
|
+
new werift_1.RTCRtpCodecParameters({
|
|
35
|
+
mimeType: 'video/H264',
|
|
36
|
+
clockRate: 90000,
|
|
37
|
+
rtcpFeedback: [
|
|
38
|
+
{ type: 'transport-cc' },
|
|
39
|
+
{ type: 'ccm', parameter: 'fir' },
|
|
40
|
+
{ type: 'nack' },
|
|
41
|
+
{ type: 'nack', parameter: 'pli' },
|
|
42
|
+
{ type: 'goog-remb' },
|
|
43
|
+
],
|
|
44
|
+
parameters: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
})), audioTransceiver = pc.addTransceiver(this.returnAudioTrack, {
|
|
49
|
+
direction: 'sendrecv',
|
|
50
|
+
}), videoTransceiver = pc.addTransceiver('video', {
|
|
51
|
+
direction: 'recvonly',
|
|
52
|
+
});
|
|
53
|
+
audioTransceiver.onTrack.subscribe((track) => {
|
|
54
|
+
track.onReceiveRtp.subscribe((rtp) => {
|
|
55
|
+
this.onAudioRtp.next(rtp);
|
|
56
|
+
});
|
|
57
|
+
track.onReceiveRtcp.subscribe((rtcp) => {
|
|
58
|
+
this.onAudioRtcp.next(rtcp);
|
|
59
|
+
});
|
|
60
|
+
if (debug) {
|
|
61
|
+
track.onReceiveRtp.once(() => {
|
|
62
|
+
(0, util_1.logInfo)('received first audio packet');
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
videoTransceiver.onTrack.subscribe((track) => {
|
|
67
|
+
track.onReceiveRtp.subscribe((rtp) => {
|
|
68
|
+
this.onVideoRtp.next(rtp);
|
|
69
|
+
});
|
|
70
|
+
track.onReceiveRtcp.subscribe((rtcp) => {
|
|
71
|
+
this.onVideoRtcp.next(rtcp);
|
|
72
|
+
});
|
|
73
|
+
track.onReceiveRtp.once(() => {
|
|
74
|
+
if (debug) {
|
|
75
|
+
(0, util_1.logInfo)('received first video packet');
|
|
76
|
+
}
|
|
77
|
+
setInterval(() => videoTransceiver.receiver.sendRtcpPLI(track.ssrc), 2000);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
createAnswer(offer) {
|
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
yield this.pc.setRemoteDescription(offer);
|
|
84
|
+
const answer = yield this.pc.createAnswer();
|
|
85
|
+
yield this.pc.setLocalDescription(answer);
|
|
86
|
+
return answer;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
addIceCandidate(candidate) {
|
|
90
|
+
return this.pc.addIceCandidate(candidate);
|
|
91
|
+
}
|
|
92
|
+
close() {
|
|
93
|
+
this.pc.close().catch(util_1.logError);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.PeerConnection = PeerConnection;
|
package/lib/api/rest-client.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export declare function deviceApi(path: string): string;
|
|
|
6
6
|
export declare function appApi(path: string): string;
|
|
7
7
|
export interface ExtendedResponse {
|
|
8
8
|
responseTimestamp: number;
|
|
9
|
+
timeMillis: number;
|
|
9
10
|
}
|
|
10
11
|
export interface EmailAuth {
|
|
11
12
|
email: string;
|
|
@@ -24,6 +25,7 @@ export declare class RingRestClient {
|
|
|
24
25
|
refreshToken: string | undefined;
|
|
25
26
|
private hardwareIdPromise;
|
|
26
27
|
private _authPromise;
|
|
28
|
+
private timeouts;
|
|
27
29
|
private clearPreviousAuth;
|
|
28
30
|
private get authPromise();
|
|
29
31
|
private sessionPromise?;
|
|
@@ -44,4 +46,5 @@ export declare class RingRestClient {
|
|
|
44
46
|
url: string;
|
|
45
47
|
}): Promise<T & ExtendedResponse>;
|
|
46
48
|
getCurrentAuth(): Promise<AuthTokenResponse>;
|
|
49
|
+
clearTimeouts(): void;
|
|
47
50
|
}
|
package/lib/api/rest-client.js
CHANGED
|
@@ -44,8 +44,13 @@ function requestWithRetry(requestOptions) {
|
|
|
44
44
|
return __awaiter(this, void 0, void 0, function* () {
|
|
45
45
|
try {
|
|
46
46
|
const options = Object.assign(Object.assign({}, defaultRequestOptions), requestOptions), { headers, body } = (yield (0, got_1.default)(options)), data = body;
|
|
47
|
-
if (data !== null && typeof data === 'object'
|
|
48
|
-
|
|
47
|
+
if (data !== null && typeof data === 'object') {
|
|
48
|
+
if (headers.date) {
|
|
49
|
+
data.responseTimestamp = new Date(headers.date).getTime();
|
|
50
|
+
}
|
|
51
|
+
if (headers['x-time-millis']) {
|
|
52
|
+
data.timeMillis = Number(headers['x-time-millis']);
|
|
53
|
+
}
|
|
49
54
|
}
|
|
50
55
|
return data;
|
|
51
56
|
}
|
|
@@ -66,12 +71,15 @@ function requestWithRetry(requestOptions) {
|
|
|
66
71
|
class RingRestClient {
|
|
67
72
|
constructor(authOptions) {
|
|
68
73
|
this.authOptions = authOptions;
|
|
69
|
-
|
|
70
|
-
this.refreshToken = ('refreshToken' in this.authOptions ? this.authOptions.refreshToken : undefined);
|
|
71
|
-
this.hardwareIdPromise = (0, util_1.getHardwareId)(this.authOptions.systemId);
|
|
74
|
+
this.timeouts = [];
|
|
72
75
|
this.sessionPromise = undefined;
|
|
73
76
|
this.using2fa = false;
|
|
74
77
|
this.onRefreshTokenUpdated = new rxjs_1.ReplaySubject(1);
|
|
78
|
+
this.refreshToken =
|
|
79
|
+
'refreshToken' in this.authOptions
|
|
80
|
+
? this.authOptions.refreshToken
|
|
81
|
+
: undefined;
|
|
82
|
+
this.hardwareIdPromise = (0, util_1.getHardwareId)(this.authOptions.systemId);
|
|
75
83
|
}
|
|
76
84
|
clearPreviousAuth() {
|
|
77
85
|
this._authPromise = undefined;
|
|
@@ -83,11 +91,12 @@ class RingRestClient {
|
|
|
83
91
|
authPromise
|
|
84
92
|
.then(({ expires_in }) => {
|
|
85
93
|
// clear the existing auth promise 1 minute before it expires
|
|
86
|
-
setTimeout(() => {
|
|
94
|
+
const timeout = setTimeout(() => {
|
|
87
95
|
if (this._authPromise === authPromise) {
|
|
88
96
|
this.clearPreviousAuth();
|
|
89
97
|
}
|
|
90
98
|
}, ((expires_in || 3600) - 60) * 1000);
|
|
99
|
+
this.timeouts.push(timeout);
|
|
91
100
|
})
|
|
92
101
|
.catch(() => {
|
|
93
102
|
// ignore these errors here, they should be handled by the function making a rest request
|
|
@@ -288,5 +297,8 @@ class RingRestClient {
|
|
|
288
297
|
getCurrentAuth() {
|
|
289
298
|
return this.authPromise;
|
|
290
299
|
}
|
|
300
|
+
clearTimeouts() {
|
|
301
|
+
this.timeouts.forEach(clearTimeout);
|
|
302
|
+
}
|
|
291
303
|
}
|
|
292
304
|
exports.RingRestClient = RingRestClient;
|