ring-client-api 10.0.0-beta.0 → 10.0.0-beta.3
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 +31 -0
- package/lib/api/live-call-ring-edge.d.ts +41 -0
- package/lib/api/live-call-ring-edge.js +294 -0
- package/lib/api/live-call.js +15 -4
- package/lib/api/peer-connection.d.ts +8 -1
- package/lib/api/peer-connection.js +28 -1
- package/lib/api/ring-camera.d.ts +4 -2
- package/lib/api/ring-camera.js +9 -1
- package/lib/api/ring-types.d.ts +7 -0
- package/package.json +2 -2
- package/lib/api/rtp-utils.d.ts +0 -26
- package/lib/api/rtp-utils.js +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
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.3](https://github.com/dgreif/ring/compare/v10.0.0-beta.2...v10.0.0-beta.3) (2022-03-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
* separate streaming setup for ring edge vs regular cameras ([6281cf9](https://github.com/dgreif/ring/commit/6281cf966de12dcea8acc6516cd9079634734dec))
|
|
11
|
+
|
|
12
|
+
## [10.0.0-beta.2](https://github.com/dgreif/ring/compare/v10.0.0-beta.1...v10.0.0-beta.2) (2022-03-26)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* support ring edge camera streaming ([b30d99f](https://github.com/dgreif/ring/commit/b30d99f9ffaa1fabc1d09b830cf8ba0e69fe84a0))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **api:** allow snapshot to be force-updated based on options ([fa0212a](https://github.com/dgreif/ring/commit/fa0212ab539cc268f8000c5b9ea39a00799cb8d7))
|
|
23
|
+
|
|
24
|
+
## [10.0.0-beta.1](https://github.com/dgreif/ring/compare/v10.0.0-beta.0...v10.0.0-beta.1) (2022-02-27)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
### ⚠ BREAKING CHANGES
|
|
28
|
+
|
|
29
|
+
* **homebridge:** For anyone who updated to the 10.0 beta and had their cameras reset in HomeKit, this change will cause another reset. This moves the audio codecs back to what they were before the 10.0 beta, which means anyone upgrading directly from v9 will _not_ experience a reset of the cameras. Using native Opus was an experimental feature, which ultimately lead to a degraded user experience.
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* allow video transcoding to be skipped ([1ef8cdd](https://github.com/dgreif/ring/commit/1ef8cdd54d1f118a8fdcfd28f6970d9c8b7c65f9))
|
|
34
|
+
* **homebridge:** switch back to aac-eld from opus ([7dcd2a0](https://github.com/dgreif/ring/commit/7dcd2a087da7c1b24cc9bdd3c3919af5e6fb7997))
|
|
35
|
+
|
|
5
36
|
## [10.0.0-beta.0](https://github.com/dgreif/ring/compare/v9.25.0-beta.1...v10.0.0-beta.0) (2022-02-18)
|
|
6
37
|
|
|
7
38
|
|
|
@@ -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.js
CHANGED
|
@@ -10,7 +10,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
10
10
|
};
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.LiveCall = void 0;
|
|
13
|
-
const rtp_utils_1 = require("./rtp-utils");
|
|
14
13
|
const ws_1 = require("ws");
|
|
15
14
|
const rxjs_1 = require("rxjs");
|
|
16
15
|
const peer_connection_1 = require("./peer-connection");
|
|
@@ -19,6 +18,18 @@ const operators_1 = require("rxjs/operators");
|
|
|
19
18
|
const subscribed_1 = require("./subscribed");
|
|
20
19
|
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
21
20
|
const ffmpeg_1 = require("./ffmpeg");
|
|
21
|
+
function getCleanSdp(sdp, includeVideo) {
|
|
22
|
+
return sdp
|
|
23
|
+
.split('\nm=')
|
|
24
|
+
.slice(1)
|
|
25
|
+
.map((section) => 'm=' + section)
|
|
26
|
+
.filter((section) => includeVideo || !section.startsWith('m=video'))
|
|
27
|
+
.join('\n');
|
|
28
|
+
}
|
|
29
|
+
function parseLiveCallSession(sessionId) {
|
|
30
|
+
const encodedSession = sessionId.split('.')[1], buff = Buffer.from(encodedSession, 'base64'), text = buff.toString('ascii');
|
|
31
|
+
return JSON.parse(text);
|
|
32
|
+
}
|
|
22
33
|
class LiveCall extends subscribed_1.Subscribed {
|
|
23
34
|
constructor(sessionId, camera) {
|
|
24
35
|
super();
|
|
@@ -29,13 +40,13 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
29
40
|
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
30
41
|
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
31
42
|
this.activated = false;
|
|
32
|
-
const liveCallSession =
|
|
43
|
+
const liveCallSession = parseLiveCallSession(sessionId);
|
|
33
44
|
this.pc = new peer_connection_1.PeerConnection();
|
|
34
45
|
this.ws = new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
|
|
35
46
|
headers: {
|
|
36
47
|
API_VERSION: '3.1',
|
|
37
48
|
API_TOKEN: sessionId,
|
|
38
|
-
CLIENT_INFO: 'Ring/3.
|
|
49
|
+
CLIENT_INFO: 'Ring/3.49.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
|
|
39
50
|
},
|
|
40
51
|
});
|
|
41
52
|
this.onAudioRtp = this.pc.onAudioRtp;
|
|
@@ -92,7 +103,7 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
92
103
|
...(ffmpegOptions.input || []),
|
|
93
104
|
'-i',
|
|
94
105
|
'pipe:',
|
|
95
|
-
],
|
|
106
|
+
], ringSdp = yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered), inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
|
|
96
107
|
.replace(/m=audio \d+/, `m=audio ${audioPort}`)
|
|
97
108
|
.replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
|
|
98
109
|
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
@@ -1,17 +1,24 @@
|
|
|
1
1
|
import { MediaStreamTrack, RTCIceCandidate, RTCPeerConnection, RtcpPacket, RtpPacket } from '@koush/werift';
|
|
2
|
-
import { Subject } from 'rxjs';
|
|
2
|
+
import { ReplaySubject, Subject } from 'rxjs';
|
|
3
3
|
export declare class PeerConnection {
|
|
4
4
|
pc: RTCPeerConnection;
|
|
5
5
|
onAudioRtp: Subject<RtpPacket>;
|
|
6
6
|
onAudioRtcp: Subject<RtcpPacket>;
|
|
7
7
|
onVideoRtp: Subject<RtpPacket>;
|
|
8
8
|
onVideoRtcp: Subject<RtcpPacket>;
|
|
9
|
+
onIceCandidate: Subject<RTCIceCandidate>;
|
|
10
|
+
onConnectionState: ReplaySubject<"failed" | "closed" | "connected" | "connecting" | "new" | "disconnected">;
|
|
9
11
|
returnAudioTrack: MediaStreamTrack;
|
|
10
12
|
constructor();
|
|
13
|
+
createOffer(): Promise<import("@koush/werift").RTCSessionDescription>;
|
|
11
14
|
createAnswer(offer: {
|
|
12
15
|
type: 'offer';
|
|
13
16
|
sdp: string;
|
|
14
17
|
}): Promise<import("@koush/werift").RTCSessionDescription>;
|
|
18
|
+
acceptAnswer(answer: {
|
|
19
|
+
type: 'answer';
|
|
20
|
+
sdp: string;
|
|
21
|
+
}): Promise<void>;
|
|
15
22
|
addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
|
|
16
23
|
close(): void;
|
|
17
24
|
}
|
|
@@ -20,6 +20,8 @@ class PeerConnection {
|
|
|
20
20
|
this.onAudioRtcp = new rxjs_1.Subject();
|
|
21
21
|
this.onVideoRtp = new rxjs_1.Subject();
|
|
22
22
|
this.onVideoRtcp = new rxjs_1.Subject();
|
|
23
|
+
this.onIceCandidate = new rxjs_1.Subject();
|
|
24
|
+
this.onConnectionState = new rxjs_1.ReplaySubject(1);
|
|
23
25
|
this.returnAudioTrack = new werift_1.MediaStreamTrack({ kind: 'audio' });
|
|
24
26
|
const pc = (this.pc = new werift_1.RTCPeerConnection({
|
|
25
27
|
codecs: {
|
|
@@ -41,7 +43,7 @@ class PeerConnection {
|
|
|
41
43
|
{ type: 'nack', parameter: 'pli' },
|
|
42
44
|
{ type: 'goog-remb' },
|
|
43
45
|
],
|
|
44
|
-
parameters: '
|
|
46
|
+
parameters: 'packetization-mode=1;profile-level-id=42801F;level-asymmetry-allowed=1',
|
|
45
47
|
}),
|
|
46
48
|
],
|
|
47
49
|
},
|
|
@@ -77,6 +79,26 @@ class PeerConnection {
|
|
|
77
79
|
setInterval(() => videoTransceiver.receiver.sendRtcpPLI(track.ssrc), 2000);
|
|
78
80
|
});
|
|
79
81
|
});
|
|
82
|
+
this.pc.onIceCandidate.subscribe((iceCandidate) => {
|
|
83
|
+
this.onIceCandidate.next(iceCandidate);
|
|
84
|
+
});
|
|
85
|
+
pc.iceConnectionStateChange.subscribe(() => {
|
|
86
|
+
(0, util_1.logInfo)(`iceConnectionStateChange: ${pc.iceConnectionState}`);
|
|
87
|
+
if (pc.iceConnectionState === 'closed') {
|
|
88
|
+
this.onConnectionState.next('closed');
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
pc.connectionStateChange.subscribe(() => {
|
|
92
|
+
(0, util_1.logInfo)(`connectionStateChange: ${pc.connectionState}`);
|
|
93
|
+
this.onConnectionState.next(pc.connectionState);
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
createOffer() {
|
|
97
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
98
|
+
const offer = yield this.pc.createOffer();
|
|
99
|
+
yield this.pc.setLocalDescription(offer);
|
|
100
|
+
return offer;
|
|
101
|
+
});
|
|
80
102
|
}
|
|
81
103
|
createAnswer(offer) {
|
|
82
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
@@ -86,6 +108,11 @@ class PeerConnection {
|
|
|
86
108
|
return answer;
|
|
87
109
|
});
|
|
88
110
|
}
|
|
111
|
+
acceptAnswer(answer) {
|
|
112
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
yield this.pc.setRemoteDescription(answer);
|
|
114
|
+
});
|
|
115
|
+
}
|
|
89
116
|
addIceCandidate(candidate) {
|
|
90
117
|
return this.pc.addIceCandidate(candidate);
|
|
91
118
|
}
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { BehaviorSubject, Subject } from 'rxjs';
|
|
|
5
5
|
import { DeepPartial } from './util';
|
|
6
6
|
import { Subscribed } from './subscribed';
|
|
7
7
|
import { FfmpegOptions, LiveCall } from './live-call';
|
|
8
|
+
import { LiveCallRingEdge } from './live-call-ring-edge';
|
|
8
9
|
export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'>): number | null;
|
|
9
10
|
export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
|
|
10
11
|
accountId: string;
|
|
@@ -42,6 +43,7 @@ export declare class RingCamera extends Subscribed {
|
|
|
42
43
|
get isCharging(): boolean;
|
|
43
44
|
get operatingOnBattery(): boolean;
|
|
44
45
|
get isOffline(): boolean;
|
|
46
|
+
get isRingEdgeEnabled(): boolean;
|
|
45
47
|
get hasInHomeDoorbell(): boolean;
|
|
46
48
|
doorbotUrl(path?: string): string;
|
|
47
49
|
deviceUrl(path?: string): string;
|
|
@@ -52,7 +54,7 @@ export declare class RingCamera extends Subscribed {
|
|
|
52
54
|
getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
|
|
53
55
|
setInHomeDoorbell(enable: boolean): Promise<boolean>;
|
|
54
56
|
getHealth(): Promise<CameraHealth>;
|
|
55
|
-
startLiveCall(): Promise<LiveCall>;
|
|
57
|
+
startLiveCall(): Promise<LiveCall | LiveCallRingEdge>;
|
|
56
58
|
private removeDingById;
|
|
57
59
|
processActiveDing(ding: ActiveDing): void;
|
|
58
60
|
getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
|
|
@@ -86,7 +88,7 @@ export declare class RingCamera extends Subscribed {
|
|
|
86
88
|
force?: boolean;
|
|
87
89
|
}): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
88
90
|
recordToFile(outputPath: string, duration?: number): Promise<void>;
|
|
89
|
-
streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall>;
|
|
91
|
+
streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall | LiveCallRingEdge>;
|
|
90
92
|
subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
91
93
|
unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
92
94
|
subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
package/lib/api/ring-camera.js
CHANGED
|
@@ -17,6 +17,7 @@ const operators_1 = require("rxjs/operators");
|
|
|
17
17
|
const util_1 = require("./util");
|
|
18
18
|
const subscribed_1 = require("./subscribed");
|
|
19
19
|
const live_call_1 = require("./live-call");
|
|
20
|
+
const live_call_ring_edge_1 = require("./live-call-ring-edge");
|
|
20
21
|
const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
|
|
21
22
|
function parseBatteryLife(batteryLife) {
|
|
22
23
|
if (batteryLife === null || batteryLife === undefined) {
|
|
@@ -145,6 +146,9 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
145
146
|
get isOffline() {
|
|
146
147
|
return this.data.alerts.connection === 'offline';
|
|
147
148
|
}
|
|
149
|
+
get isRingEdgeEnabled() {
|
|
150
|
+
return this.data.settings.sheila_settings.local_storage_enabled === true;
|
|
151
|
+
}
|
|
148
152
|
get hasInHomeDoorbell() {
|
|
149
153
|
const { chime_settings } = this.data.settings;
|
|
150
154
|
return (this.isDoorbot &&
|
|
@@ -231,6 +235,10 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
231
235
|
}
|
|
232
236
|
startLiveCall() {
|
|
233
237
|
return __awaiter(this, void 0, void 0, function* () {
|
|
238
|
+
if (this.isRingEdgeEnabled) {
|
|
239
|
+
const auth = yield this.restClient.getCurrentAuth();
|
|
240
|
+
return new live_call_ring_edge_1.LiveCallRingEdge(auth.access_token, this);
|
|
241
|
+
}
|
|
234
242
|
const liveCall = yield this.restClient
|
|
235
243
|
.request({
|
|
236
244
|
method: 'POST',
|
|
@@ -362,7 +370,7 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
362
370
|
getNextSnapshot({ afterMs, maxWaitMs, force, }) {
|
|
363
371
|
return __awaiter(this, void 0, void 0, function* () {
|
|
364
372
|
const response = yield this.restClient.request({
|
|
365
|
-
url: `https://app-snaps.ring.com/snapshots/next/${this.id}
|
|
373
|
+
url: `https://app-snaps.ring.com/snapshots/next/${this.id}`,
|
|
366
374
|
responseType: 'buffer',
|
|
367
375
|
searchParams: {
|
|
368
376
|
'after-ms': afterMs,
|
package/lib/api/ring-types.d.ts
CHANGED
|
@@ -379,6 +379,13 @@ export interface CameraData {
|
|
|
379
379
|
live_view_disabled?: boolean;
|
|
380
380
|
motion_detection_enabled?: boolean;
|
|
381
381
|
power_mode?: 'battery' | 'wired';
|
|
382
|
+
sheila_settings: {
|
|
383
|
+
cv_processing_enabled: boolean | null;
|
|
384
|
+
local_storage_enabled: boolean | null;
|
|
385
|
+
};
|
|
386
|
+
server_settings: {
|
|
387
|
+
ring_media_server_enabled: boolean;
|
|
388
|
+
};
|
|
382
389
|
};
|
|
383
390
|
features: {
|
|
384
391
|
motions_enabled: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ring-client-api",
|
|
3
|
-
"version": "10.0.0-beta.
|
|
3
|
+
"version": "10.0.0-beta.3",
|
|
4
4
|
"description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
|
|
5
5
|
"main": "lib/api/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"watch:tests": "jest --watch",
|
|
15
15
|
"lint": "eslint '**/*.ts' --fix",
|
|
16
16
|
"build": "rm -rf lib && tsc --declaration",
|
|
17
|
-
"build-quick": "rm -rf lib && esbuild ./{api,examples,homebridge}/*.ts
|
|
17
|
+
"build-quick": "rm -rf lib && esbuild ./{api,examples,homebridge}/*.ts --target=es2017 --format=cjs --outdir=lib",
|
|
18
18
|
"example": "npm run build-quick && node ./lib/examples/example.js",
|
|
19
19
|
"api-example": "npm run build-quick && node ./lib/examples/api-example.js",
|
|
20
20
|
"record-example": "npm run build-quick && node ./lib/examples/record-example.js",
|
package/lib/api/rtp-utils.d.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { DingKind } from './ring-types';
|
|
2
|
-
export interface LiveCallSession {
|
|
3
|
-
alexa_port: 18443;
|
|
4
|
-
app_session_token: string;
|
|
5
|
-
availability_zone: 'availability-zone';
|
|
6
|
-
custom_timer: {
|
|
7
|
-
max_sec: number;
|
|
8
|
-
};
|
|
9
|
-
ding_id: string;
|
|
10
|
-
ding_kind: DingKind;
|
|
11
|
-
doorbot_id: number;
|
|
12
|
-
exp: number;
|
|
13
|
-
ip: string;
|
|
14
|
-
port: number;
|
|
15
|
-
private_ip: string;
|
|
16
|
-
rms_fqdn: string;
|
|
17
|
-
rms_version: string;
|
|
18
|
-
rsp_port: number;
|
|
19
|
-
rtsp_port: number;
|
|
20
|
-
session_id: string;
|
|
21
|
-
sip_port: number;
|
|
22
|
-
webrtc_port: number;
|
|
23
|
-
webrtc_url: string;
|
|
24
|
-
wwr_port: number;
|
|
25
|
-
}
|
|
26
|
-
export declare function parseLiveCallSession(sessionId: string): LiveCallSession;
|
package/lib/api/rtp-utils.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseLiveCallSession = void 0;
|
|
4
|
-
function parseLiveCallSession(sessionId) {
|
|
5
|
-
const encodedSession = sessionId.split('.')[1], buff = Buffer.from(encodedSession, 'base64'), text = buff.toString('ascii');
|
|
6
|
-
return JSON.parse(text);
|
|
7
|
-
}
|
|
8
|
-
exports.parseLiveCallSession = parseLiveCallSession;
|