ring-client-api 10.0.0-beta.3 → 10.0.0-beta.6

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.
@@ -1,294 +0,0 @@
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;