ring-client-api 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 +2 -1
- 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/package.json +37 -31
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
|
@@ -45,6 +45,7 @@ Option | Default | Explanation
|
|
|
45
45
|
`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
46
|
`controlCenterDisplayName` | 'ring-client-api' | This allows you to change the displayed name for the Authorized Device within Control Center in the Ring app
|
|
47
47
|
`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`
|
|
48
49
|
|
|
49
50
|
## Locations
|
|
50
51
|
```typescript
|
|
@@ -108,7 +109,7 @@ Camera also includes the following observables:
|
|
|
108
109
|
* `onDoorbellPressed`: emits a ding every time the doorbell is pressed
|
|
109
110
|
* `onMotionDetected`: `true` or `false` based on `onActiveDings` containing a motion ding
|
|
110
111
|
|
|
111
|
-
Some other useful
|
|
112
|
+
Some other useful properties
|
|
112
113
|
* `id`
|
|
113
114
|
* `name`: same as `description` from `data`
|
|
114
115
|
* `hasLight`: does this camera have a light
|
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;
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { DeepPartial } from './util';
|
|
|
7
7
|
import { FfmpegOptions, SipSession } from './sip-session';
|
|
8
8
|
import { SipOptions } from './sip-call';
|
|
9
9
|
import { Subscribed } from './subscribed';
|
|
10
|
+
import { LiveCall } from './live-call';
|
|
10
11
|
export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'>): number | null;
|
|
11
12
|
export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
|
|
12
13
|
accountId: string;
|
|
@@ -16,13 +17,13 @@ export declare class RingCamera extends Subscribed {
|
|
|
16
17
|
isDoorbot: boolean;
|
|
17
18
|
private restClient;
|
|
18
19
|
private avoidSnapshotBatteryDrain;
|
|
20
|
+
private treatKnockAsDing;
|
|
19
21
|
id: number;
|
|
20
22
|
deviceType: import("./ring-types").RingCameraKind;
|
|
21
23
|
model: string;
|
|
22
24
|
onData: BehaviorSubject<CameraData>;
|
|
23
25
|
hasLight: boolean;
|
|
24
26
|
hasSiren: boolean;
|
|
25
|
-
hasBattery: boolean;
|
|
26
27
|
onRequestUpdate: Subject<unknown>;
|
|
27
28
|
onRequestActiveDings: Subject<unknown>;
|
|
28
29
|
onNewDing: Subject<ActiveDing>;
|
|
@@ -32,13 +33,14 @@ export declare class RingCamera extends Subscribed {
|
|
|
32
33
|
onMotionStarted: import("rxjs").Observable<null>;
|
|
33
34
|
onBatteryLevel: import("rxjs").Observable<number | null>;
|
|
34
35
|
onInHomeDoorbellStatus: import("rxjs").Observable<boolean>;
|
|
35
|
-
constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean);
|
|
36
|
+
constructor(initialData: CameraData, isDoorbot: boolean, restClient: RingRestClient, avoidSnapshotBatteryDrain: boolean, treatKnockAsDing: boolean);
|
|
36
37
|
updateData(update: CameraData): void;
|
|
37
38
|
requestUpdate(): void;
|
|
38
39
|
get data(): CameraData;
|
|
39
40
|
get name(): string;
|
|
40
41
|
get activeDings(): ActiveDing[];
|
|
41
42
|
get batteryLevel(): number | null;
|
|
43
|
+
get hasBattery(): boolean;
|
|
42
44
|
get hasLowBattery(): boolean;
|
|
43
45
|
get isCharging(): boolean;
|
|
44
46
|
get operatingOnBattery(): boolean;
|
|
@@ -53,6 +55,7 @@ export declare class RingCamera extends Subscribed {
|
|
|
53
55
|
getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
|
|
54
56
|
setInHomeDoorbell(enable: boolean): Promise<boolean>;
|
|
55
57
|
getHealth(): Promise<CameraHealth>;
|
|
58
|
+
startLiveCall(): Promise<LiveCall>;
|
|
56
59
|
startVideoOnDemand(): Promise<("" | ActiveDing) & import("./rest-client").ExtendedResponse>;
|
|
57
60
|
private pollForActiveDing;
|
|
58
61
|
private expiredDingIds;
|
|
@@ -74,18 +77,21 @@ export declare class RingCamera extends Subscribed {
|
|
|
74
77
|
}): Promise<string>;
|
|
75
78
|
private isTimestampInLifeTime;
|
|
76
79
|
get snapshotsAreBlocked(): boolean;
|
|
77
|
-
private getSnapshotTimestamp;
|
|
78
|
-
private refreshSnapshotInProgress?;
|
|
79
80
|
get snapshotLifeTime(): number;
|
|
81
|
+
private lastSnapshotTimestamp;
|
|
80
82
|
private lastSnapshotTimestampLocal;
|
|
81
83
|
private lastSnapshotPromise?;
|
|
82
84
|
get currentTimestampAge(): number;
|
|
83
|
-
get currentTimestampExpiresIn(): number;
|
|
84
85
|
get hasSnapshotWithinLifetime(): boolean;
|
|
85
86
|
private checkIfSnapshotsAreBlocked;
|
|
86
|
-
private
|
|
87
|
-
private
|
|
88
|
-
getSnapshot(): Promise<Buffer
|
|
87
|
+
private shouldUseExistingSnapshotPromise;
|
|
88
|
+
private fetchingSnapshot;
|
|
89
|
+
getSnapshot(): Promise<Buffer>;
|
|
90
|
+
getNextSnapshot({ afterMs, maxWaitMs, force, }: {
|
|
91
|
+
afterMs?: number;
|
|
92
|
+
maxWaitMs?: number;
|
|
93
|
+
force?: boolean;
|
|
94
|
+
}): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
89
95
|
getSipOptions(): Promise<SipOptions>;
|
|
90
96
|
getUpdatedSipOptions(expiredDingId: string): Promise<SipOptions>;
|
|
91
97
|
createSipSession(options?: {
|