ring-client-api 11.8.1 → 12.0.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/lib/ring-camera.d.ts +1 -1
- package/lib/ring-camera.js +3 -13
- package/lib/ring-types.d.ts +3 -14
- package/lib/streaming/peer-connection.d.ts +0 -8
- package/lib/streaming/peer-connection.js +0 -6
- package/lib/streaming/{ring-edge-connection.d.ts → streaming-messages.d.ts} +1 -13
- package/lib/streaming/streaming-messages.js +13 -0
- package/lib/streaming/streaming-session.d.ts +1 -2
- package/lib/streaming/webrtc-connection.d.ts +34 -19
- package/lib/streaming/webrtc-connection.js +220 -24
- package/package.json +6 -3
- package/lib/streaming/ring-edge-connection.js +0 -147
- package/lib/streaming/streaming-connection-base.d.ts +0 -35
- package/lib/streaming/streaming-connection-base.js +0 -131
package/lib/ring-camera.d.ts
CHANGED
|
@@ -4,8 +4,8 @@ import { RingRestClient } from './rest-client';
|
|
|
4
4
|
import { BehaviorSubject, Subject } from 'rxjs';
|
|
5
5
|
import { DeepPartial } from './util';
|
|
6
6
|
import { Subscribed } from './subscribed';
|
|
7
|
+
import { StreamingConnectionOptions } from './streaming/webrtc-connection';
|
|
7
8
|
import { FfmpegOptions, StreamingSession } from './streaming/streaming-session';
|
|
8
|
-
import { StreamingConnectionOptions } from './streaming/streaming-connection-base';
|
|
9
9
|
import { SimpleWebRtcSession } from './streaming/simple-webrtc-session';
|
|
10
10
|
export type AnyCameraData = CameraData | OnvifCameraData;
|
|
11
11
|
export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'> & {
|
package/lib/ring-camera.js
CHANGED
|
@@ -8,7 +8,6 @@ const operators_1 = require("rxjs/operators");
|
|
|
8
8
|
const util_1 = require("./util");
|
|
9
9
|
const subscribed_1 = require("./subscribed");
|
|
10
10
|
const webrtc_connection_1 = require("./streaming/webrtc-connection");
|
|
11
|
-
const ring_edge_connection_1 = require("./streaming/ring-edge-connection");
|
|
12
11
|
const streaming_session_1 = require("./streaming/streaming-session");
|
|
13
12
|
const simple_webrtc_session_1 = require("./streaming/simple-webrtc-session");
|
|
14
13
|
const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
|
|
@@ -239,24 +238,15 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
239
238
|
return response.device_health;
|
|
240
239
|
}
|
|
241
240
|
async createStreamingConnection(options) {
|
|
242
|
-
|
|
243
|
-
const auth = await this.restClient.getCurrentAuth();
|
|
244
|
-
return new ring_edge_connection_1.RingEdgeConnection(auth.access_token, this, options);
|
|
245
|
-
}
|
|
246
|
-
const liveCall = await this.restClient
|
|
241
|
+
const response = await this.restClient
|
|
247
242
|
.request({
|
|
248
243
|
method: 'POST',
|
|
249
|
-
url:
|
|
244
|
+
url: (0, rest_client_1.appApi)('clap/ticket/request/signalsocket'),
|
|
250
245
|
})
|
|
251
246
|
.catch((e) => {
|
|
252
|
-
if (e.response?.statusCode === 403) {
|
|
253
|
-
const errorMessage = `Camera ${this.name} returned 403 when starting a live stream. This usually indicates that live streaming is blocked by Modes settings. Check your Ring app and verify that you are able to stream from this camera with the current Modes settings.`;
|
|
254
|
-
(0, util_1.logError)(errorMessage);
|
|
255
|
-
throw new Error(errorMessage);
|
|
256
|
-
}
|
|
257
247
|
throw e;
|
|
258
248
|
});
|
|
259
|
-
return new webrtc_connection_1.WebrtcConnection(
|
|
249
|
+
return new webrtc_connection_1.WebrtcConnection(response.ticket, this, options);
|
|
260
250
|
}
|
|
261
251
|
async startLiveCall(options = {}) {
|
|
262
252
|
const connection = await this.createStreamingConnection(options);
|
package/lib/ring-types.d.ts
CHANGED
|
@@ -919,20 +919,9 @@ export interface PushNotificationLowBattery {
|
|
|
919
919
|
action: PushNotificationAction.LowBattery;
|
|
920
920
|
}
|
|
921
921
|
export type PushNotification = PushNotificationDing | PushNotificationAlarm | PushNotificationLowBattery;
|
|
922
|
-
export interface
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
device_id: number;
|
|
926
|
-
device_kind: RingCameraKind;
|
|
927
|
-
device_description: string;
|
|
928
|
-
state: DingState;
|
|
929
|
-
protocol: 'ring_media_server' | unknown;
|
|
930
|
-
now: number;
|
|
931
|
-
video_jitter_buffer_ms: number;
|
|
932
|
-
audio_jitter_buffer_ms: number;
|
|
933
|
-
data: {
|
|
934
|
-
session_id: string;
|
|
935
|
-
};
|
|
922
|
+
export interface SocketTicketResponse {
|
|
923
|
+
ticket: string;
|
|
924
|
+
responseTimestampe: number;
|
|
936
925
|
}
|
|
937
926
|
export interface AuthTokenResponse {
|
|
938
927
|
access_token: string;
|
|
@@ -5,10 +5,6 @@ export interface BasicPeerConnection {
|
|
|
5
5
|
createOffer(): Promise<{
|
|
6
6
|
sdp: string;
|
|
7
7
|
}>;
|
|
8
|
-
createAnswer(offer: {
|
|
9
|
-
type: 'offer';
|
|
10
|
-
sdp: string;
|
|
11
|
-
}): Promise<RTCSessionDescriptionInit>;
|
|
12
8
|
acceptAnswer(answer: {
|
|
13
9
|
type: 'answer';
|
|
14
10
|
sdp: string;
|
|
@@ -31,10 +27,6 @@ export declare class WeriftPeerConnection extends Subscribed implements BasicPee
|
|
|
31
27
|
private onRequestKeyFrame;
|
|
32
28
|
constructor();
|
|
33
29
|
createOffer(): Promise<import("werift").RTCSessionDescription>;
|
|
34
|
-
createAnswer(offer: {
|
|
35
|
-
type: 'offer';
|
|
36
|
-
sdp: string;
|
|
37
|
-
}): Promise<import("werift").RTCSessionDescription>;
|
|
38
30
|
acceptAnswer(answer: {
|
|
39
31
|
type: 'answer';
|
|
40
32
|
sdp: string;
|
|
@@ -116,12 +116,6 @@ class WeriftPeerConnection extends subscribed_1.Subscribed {
|
|
|
116
116
|
await this.pc.setLocalDescription(offer);
|
|
117
117
|
return offer;
|
|
118
118
|
}
|
|
119
|
-
async createAnswer(offer) {
|
|
120
|
-
await this.pc.setRemoteDescription(offer);
|
|
121
|
-
const answer = await this.pc.createAnswer();
|
|
122
|
-
await this.pc.setLocalDescription(answer);
|
|
123
|
-
return answer;
|
|
124
|
-
}
|
|
125
119
|
async acceptAnswer(answer) {
|
|
126
120
|
await this.pc.setRemoteDescription(answer);
|
|
127
121
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { RingCamera } from '../ring-camera';
|
|
2
|
-
import { StreamingConnectionBase, StreamingConnectionOptions } from './streaming-connection-base';
|
|
3
1
|
interface SessionBody {
|
|
4
2
|
doorbot_id: number;
|
|
5
3
|
session_id: string;
|
|
@@ -51,15 +49,5 @@ interface CloseMessage {
|
|
|
51
49
|
};
|
|
52
50
|
} & SessionBody;
|
|
53
51
|
}
|
|
54
|
-
type IncomingMessage = AnswerMessage | IceCandidateMessage | SessionCreatedMessage | SessionStartedMessage | PongMessage | CloseMessage | NotificationMessage;
|
|
55
|
-
export declare class RingEdgeConnection extends StreamingConnectionBase {
|
|
56
|
-
private camera;
|
|
57
|
-
private readonly onSessionId;
|
|
58
|
-
private readonly onOfferSent;
|
|
59
|
-
constructor(authToken: string, camera: RingCamera, options: StreamingConnectionOptions);
|
|
60
|
-
private initiateCall;
|
|
61
|
-
private sessionId;
|
|
62
|
-
protected handleMessage(message: IncomingMessage): Promise<void>;
|
|
63
|
-
protected sendSessionMessage(method: string, body?: Record<any, any>): void;
|
|
64
|
-
}
|
|
52
|
+
export type IncomingMessage = AnswerMessage | IceCandidateMessage | SessionCreatedMessage | SessionStartedMessage | PongMessage | CloseMessage | NotificationMessage;
|
|
65
53
|
export {};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
// eslint-disable-next-line no-shadow
|
|
4
|
+
var CloseReasonCode;
|
|
5
|
+
(function (CloseReasonCode) {
|
|
6
|
+
CloseReasonCode[CloseReasonCode["NormalClose"] = 0] = "NormalClose";
|
|
7
|
+
// 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' }
|
|
8
|
+
// reason: { code: 5, text: 'Authentication failed: -1' }
|
|
9
|
+
// reason: { code: 5, text: 'Sessions with the provided ID not found' }
|
|
10
|
+
CloseReasonCode[CloseReasonCode["AuthenticationFailed"] = 5] = "AuthenticationFailed";
|
|
11
|
+
// reason: { code: 6, text: 'Timeout waiting for ping' }
|
|
12
|
+
CloseReasonCode[CloseReasonCode["Timeout"] = 6] = "Timeout";
|
|
13
|
+
})(CloseReasonCode || (CloseReasonCode = {}));
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { RtpPacket } from 'werift';
|
|
2
2
|
import { ReplaySubject, Subject } from 'rxjs';
|
|
3
|
-
import { RingEdgeConnection } from './ring-edge-connection';
|
|
4
3
|
import { WebrtcConnection } from './webrtc-connection';
|
|
5
4
|
import { RingCamera } from '../ring-camera';
|
|
6
5
|
import { Subscribed } from '../subscribed';
|
|
@@ -21,7 +20,7 @@ export declare class StreamingSession extends Subscribed {
|
|
|
21
20
|
private readonly audioSplitter;
|
|
22
21
|
private readonly videoSplitter;
|
|
23
22
|
private readonly returnAudioSplitter;
|
|
24
|
-
constructor(camera: RingCamera, connection:
|
|
23
|
+
constructor(camera: RingCamera, connection: WebrtcConnection);
|
|
25
24
|
private bindToConnection;
|
|
26
25
|
/**
|
|
27
26
|
* @deprecated
|
|
@@ -1,23 +1,38 @@
|
|
|
1
|
+
import { ReplaySubject, Subject } from 'rxjs';
|
|
1
2
|
import { RingCamera } from '../ring-camera';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
import { BasicPeerConnection } from './peer-connection';
|
|
4
|
+
import { Subscribed } from '../subscribed';
|
|
5
|
+
import { RtpPacket } from 'werift';
|
|
6
|
+
export interface StreamingConnectionOptions {
|
|
7
|
+
createPeerConnection?: () => BasicPeerConnection;
|
|
6
8
|
}
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
9
|
+
export declare class WebrtcConnection extends Subscribed {
|
|
10
|
+
private camera;
|
|
11
|
+
private readonly onSessionId;
|
|
12
|
+
private readonly onOfferSent;
|
|
13
|
+
private readonly dialogId;
|
|
14
|
+
readonly onCallAnswered: ReplaySubject<string>;
|
|
15
|
+
readonly onCallEnded: ReplaySubject<void>;
|
|
16
|
+
readonly onError: ReplaySubject<void>;
|
|
17
|
+
readonly onMessage: ReplaySubject<{
|
|
18
|
+
method: string;
|
|
19
|
+
}>;
|
|
20
|
+
readonly onWsOpen: import("rxjs").Observable<unknown>;
|
|
21
|
+
readonly onAudioRtp: Subject<RtpPacket>;
|
|
22
|
+
readonly onVideoRtp: Subject<RtpPacket>;
|
|
23
|
+
private readonly pc;
|
|
24
|
+
private readonly ws;
|
|
25
|
+
constructor(ticket: string, camera: RingCamera, options: StreamingConnectionOptions);
|
|
26
|
+
private initiateCall;
|
|
18
27
|
private sessionId;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
28
|
+
private handleMessage;
|
|
29
|
+
private sendSessionMessage;
|
|
30
|
+
private sendMessage;
|
|
31
|
+
sendAudioPacket(rtp: RtpPacket): void;
|
|
32
|
+
private activate;
|
|
33
|
+
activateCameraSpeaker(): void;
|
|
34
|
+
private hasEnded;
|
|
35
|
+
private callEnded;
|
|
36
|
+
stop(): void;
|
|
37
|
+
requestKeyFrame(): void;
|
|
22
38
|
}
|
|
23
|
-
export {};
|
|
@@ -2,48 +2,244 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.WebrtcConnection = void 0;
|
|
4
4
|
const ws_1 = require("ws");
|
|
5
|
-
const
|
|
5
|
+
const rxjs_1 = require("rxjs");
|
|
6
|
+
const operators_1 = require("rxjs/operators");
|
|
6
7
|
const util_1 = require("../util");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const peer_connection_1 = require("./peer-connection");
|
|
9
|
+
const subscribed_1 = require("../subscribed");
|
|
10
|
+
class WebrtcConnection extends subscribed_1.Subscribed {
|
|
11
|
+
constructor(ticket, camera, options) {
|
|
12
|
+
super();
|
|
13
|
+
this.camera = camera;
|
|
14
|
+
this.onSessionId = new rxjs_1.ReplaySubject(1);
|
|
15
|
+
this.onOfferSent = new rxjs_1.ReplaySubject(1);
|
|
16
|
+
this.dialogId = (0, util_1.generateUuid)();
|
|
17
|
+
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
18
|
+
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
19
|
+
this.onError = new rxjs_1.ReplaySubject(1);
|
|
20
|
+
this.onMessage = new rxjs_1.ReplaySubject();
|
|
21
|
+
this.sessionId = null;
|
|
22
|
+
this.hasEnded = false;
|
|
23
|
+
this.ws = new ws_1.WebSocket(`wss://api.prod.signalling.ring.devices.a2z.com:443/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-${(0, util_1.generateUuid)()}&token=${ticket}`, {
|
|
15
24
|
headers: {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
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',
|
|
25
|
+
// This must exist or the socket will close immediately but content does not seem to matter
|
|
26
|
+
'User-Agent': 'android:com.ringapp',
|
|
19
27
|
},
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
28
|
+
});
|
|
29
|
+
if (options.createPeerConnection) {
|
|
30
|
+
// we were passed a custom peer connection factory
|
|
31
|
+
this.pc = options.createPeerConnection();
|
|
32
|
+
// passing rtp packets is not supported for custom peer connections
|
|
33
|
+
this.onAudioRtp = new rxjs_1.Subject();
|
|
34
|
+
this.onVideoRtp = new rxjs_1.Subject();
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// no custom peer connection factory, use the werift and pass along rtp packets
|
|
38
|
+
const pc = new peer_connection_1.WeriftPeerConnection();
|
|
39
|
+
this.pc = pc;
|
|
40
|
+
this.onAudioRtp = pc.onAudioRtp;
|
|
41
|
+
this.onVideoRtp = pc.onVideoRtp;
|
|
42
|
+
}
|
|
43
|
+
this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
|
|
44
|
+
const onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message'), onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
|
|
45
|
+
this.addSubscriptions(onMessage
|
|
46
|
+
.pipe((0, operators_1.concatMap)((event) => {
|
|
47
|
+
const message = JSON.parse(event.data);
|
|
48
|
+
this.onMessage.next(message);
|
|
49
|
+
return this.handleMessage(message).catch((e) => {
|
|
50
|
+
if (e instanceof Error &&
|
|
51
|
+
e.message.includes('negotiate codecs failed')) {
|
|
52
|
+
e = new Error('Failed to negotiate codecs. This is a known issue with Ring cameras. Please see https://github.com/dgreif/ring/wiki/Streaming-Legacy-Mode');
|
|
53
|
+
}
|
|
54
|
+
this.onError.next(e);
|
|
55
|
+
throw e;
|
|
56
|
+
});
|
|
57
|
+
}))
|
|
58
|
+
.subscribe(), onError.subscribe((e) => {
|
|
59
|
+
(0, util_1.logError)(e);
|
|
60
|
+
this.callEnded();
|
|
61
|
+
}), onClose.subscribe(() => {
|
|
62
|
+
this.callEnded();
|
|
63
|
+
}), this.pc.onConnectionState.subscribe((state) => {
|
|
64
|
+
if (state === 'failed') {
|
|
65
|
+
(0, util_1.logError)('Stream connection failed');
|
|
66
|
+
this.callEnded();
|
|
67
|
+
}
|
|
68
|
+
if (state === 'closed') {
|
|
69
|
+
(0, util_1.logDebug)('Stream connection closed');
|
|
70
|
+
this.callEnded();
|
|
71
|
+
}
|
|
72
|
+
}), this.onError.subscribe((e) => {
|
|
73
|
+
(0, util_1.logError)(e);
|
|
74
|
+
this.callEnded();
|
|
75
|
+
}), this.onWsOpen.subscribe(() => {
|
|
76
|
+
const connectionType = camera.isRingEdgeEnabled ? 'Ring Edge' : 'Cloud';
|
|
77
|
+
(0, util_1.logDebug)(`WebSocket connected for ${camera.name} (${connectionType})`);
|
|
78
|
+
this.initiateCall().catch((e) => {
|
|
79
|
+
(0, util_1.logError)(e);
|
|
80
|
+
this.callEnded();
|
|
81
|
+
});
|
|
82
|
+
}),
|
|
83
|
+
// The ring-edge session needs a ping every 5 seconds to keep the connection alive
|
|
84
|
+
(0, rxjs_1.interval)(5000).subscribe(() => {
|
|
85
|
+
this.sendSessionMessage('ping');
|
|
86
|
+
}), this.pc.onIceCandidate.subscribe(async (iceCandidate) => {
|
|
87
|
+
await (0, rxjs_1.firstValueFrom)(this.onOfferSent);
|
|
88
|
+
this.sendMessage({
|
|
89
|
+
method: 'ice',
|
|
90
|
+
dialog_id: this.dialogId,
|
|
91
|
+
body: {
|
|
92
|
+
doorbot_id: camera.id,
|
|
93
|
+
ice: iceCandidate.candidate,
|
|
94
|
+
mlineindex: iceCandidate.sdpMLineIndex,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
24
97
|
}));
|
|
25
98
|
}
|
|
99
|
+
async initiateCall() {
|
|
100
|
+
const { sdp } = await this.pc.createOffer();
|
|
101
|
+
this.sendMessage({
|
|
102
|
+
method: 'live_view',
|
|
103
|
+
dialog_id: this.dialogId,
|
|
104
|
+
body: {
|
|
105
|
+
doorbot_id: this.camera.id,
|
|
106
|
+
stream_options: { audio_enabled: true, video_enabled: true },
|
|
107
|
+
sdp,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
this.onOfferSent.next();
|
|
111
|
+
}
|
|
26
112
|
async handleMessage(message) {
|
|
113
|
+
if (message.body.doorbot_id !== this.camera.id) {
|
|
114
|
+
// ignore messages for other cameras
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
if (['session_created', 'session_started'].includes(message.method) &&
|
|
118
|
+
'session_id' in message.body &&
|
|
119
|
+
!this.sessionId) {
|
|
120
|
+
this.sessionId = message.body.session_id;
|
|
121
|
+
this.onSessionId.next(this.sessionId);
|
|
122
|
+
}
|
|
123
|
+
if (message.body.session_id && message.body.session_id !== this.sessionId) {
|
|
124
|
+
// ignore messages for other sessions
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
27
127
|
switch (message.method) {
|
|
128
|
+
case 'session_created':
|
|
129
|
+
case 'session_started':
|
|
130
|
+
// session already stored above
|
|
131
|
+
return;
|
|
28
132
|
case 'sdp':
|
|
29
|
-
|
|
30
|
-
this.
|
|
31
|
-
this.onCallAnswered.next(message.sdp);
|
|
133
|
+
await this.pc.acceptAnswer(message.body);
|
|
134
|
+
this.onCallAnswered.next(message.body.sdp);
|
|
32
135
|
this.activate();
|
|
33
136
|
return;
|
|
34
137
|
case 'ice':
|
|
35
138
|
await this.pc.addIceCandidate({
|
|
36
|
-
candidate: message.ice,
|
|
37
|
-
sdpMLineIndex: message.mlineindex,
|
|
139
|
+
candidate: message.body.ice,
|
|
140
|
+
sdpMLineIndex: message.body.mlineindex,
|
|
38
141
|
});
|
|
39
142
|
return;
|
|
143
|
+
case 'pong':
|
|
144
|
+
return;
|
|
145
|
+
case 'notification':
|
|
146
|
+
const { text } = message.body;
|
|
147
|
+
if (text === 'PeerConnectionState::kConnecting' ||
|
|
148
|
+
text === 'PeerConnectionState::kConnected' ||
|
|
149
|
+
text === 'camera_connected') {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
case 'close':
|
|
154
|
+
(0, util_1.logError)('Video stream closed');
|
|
155
|
+
(0, util_1.logError)(message.body);
|
|
156
|
+
this.callEnded();
|
|
157
|
+
return;
|
|
40
158
|
}
|
|
159
|
+
(0, util_1.logError)('UNKNOWN MESSAGE');
|
|
160
|
+
(0, util_1.logError)(message);
|
|
41
161
|
}
|
|
42
162
|
sendSessionMessage(method, body = {}) {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
163
|
+
const sendSessionMessage = (sessionId) => {
|
|
164
|
+
const message = {
|
|
165
|
+
method,
|
|
166
|
+
dialog_id: this.dialogId,
|
|
167
|
+
body: {
|
|
168
|
+
...body,
|
|
169
|
+
doorbot_id: this.camera.id,
|
|
170
|
+
session_id: sessionId,
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
this.sendMessage(message);
|
|
174
|
+
};
|
|
175
|
+
if (this.sessionId) {
|
|
176
|
+
// Send immediately if we already have a session id
|
|
177
|
+
// This is needed to send `close` before closing the websocket
|
|
178
|
+
sendSessionMessage(this.sessionId);
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// Otherwise wait for the session id to be set and then send the message
|
|
182
|
+
this.addSubscriptions(this.onSessionId.pipe((0, operators_1.take)(1)).subscribe(sendSessionMessage));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
sendMessage(message) {
|
|
186
|
+
if (this.hasEnded) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
this.ws.send(JSON.stringify(message));
|
|
190
|
+
}
|
|
191
|
+
sendAudioPacket(rtp) {
|
|
192
|
+
if (this.hasEnded) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (this.pc instanceof peer_connection_1.WeriftPeerConnection) {
|
|
196
|
+
this.pc.returnAudioTrack.writeRtp(rtp);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
throw new Error('Cannot send audio packets to a custom peer connection implementation');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
activate() {
|
|
203
|
+
(0, util_1.logInfo)('Activating Session');
|
|
204
|
+
// the activate_session message is required to keep the stream alive longer than 70 seconds
|
|
205
|
+
this.sendSessionMessage('activate_session');
|
|
206
|
+
this.sendSessionMessage('stream_options', {
|
|
207
|
+
audio_enabled: true,
|
|
208
|
+
video_enabled: true,
|
|
46
209
|
});
|
|
47
210
|
}
|
|
211
|
+
activateCameraSpeaker() {
|
|
212
|
+
// Fire and forget this call so that callers don't get hung up waiting for answer (which might not happen)
|
|
213
|
+
this.addSubscriptions(this.onCallAnswered.pipe((0, operators_1.take)(1)).subscribe(() => {
|
|
214
|
+
this.sendSessionMessage('camera_options', {
|
|
215
|
+
stealth_mode: false,
|
|
216
|
+
});
|
|
217
|
+
}));
|
|
218
|
+
}
|
|
219
|
+
callEnded() {
|
|
220
|
+
if (this.hasEnded) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
this.sendMessage({
|
|
225
|
+
reason: { code: 0, text: '' },
|
|
226
|
+
method: 'close',
|
|
227
|
+
});
|
|
228
|
+
this.ws.close();
|
|
229
|
+
}
|
|
230
|
+
catch (_) {
|
|
231
|
+
// ignore any errors since we are stopping the call
|
|
232
|
+
}
|
|
233
|
+
this.hasEnded = true;
|
|
234
|
+
this.unsubscribe();
|
|
235
|
+
this.onCallEnded.next();
|
|
236
|
+
this.pc.close();
|
|
237
|
+
}
|
|
238
|
+
stop() {
|
|
239
|
+
this.callEnded();
|
|
240
|
+
}
|
|
241
|
+
requestKeyFrame() {
|
|
242
|
+
this.pc.requestKeyFrame?.();
|
|
243
|
+
}
|
|
48
244
|
}
|
|
49
245
|
exports.WebrtcConnection = WebrtcConnection;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ring-client-api",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "12.0.0-beta.1",
|
|
4
4
|
"description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -31,10 +31,13 @@
|
|
|
31
31
|
"socket.io-client": "^2.5.0",
|
|
32
32
|
"systeminformation": "^5.18.10",
|
|
33
33
|
"uuid": "^9.0.0",
|
|
34
|
-
"webcrypto-core": "^1.7.7",
|
|
35
34
|
"werift": "0.18.5",
|
|
36
35
|
"ws": "^8.13.0"
|
|
37
36
|
},
|
|
37
|
+
"overrides": {
|
|
38
|
+
"webcrypto-core": "^1.7.7",
|
|
39
|
+
"pvtsutils": "1.3.2"
|
|
40
|
+
},
|
|
38
41
|
"devDependencies": {
|
|
39
42
|
"@types/debug": "4.1.8",
|
|
40
43
|
"@types/jest": "29.5.3",
|
|
@@ -49,7 +52,7 @@
|
|
|
49
52
|
"typescript": "5.1.6"
|
|
50
53
|
},
|
|
51
54
|
"engines": {
|
|
52
|
-
"node": "^
|
|
55
|
+
"node": "^18 || ^20"
|
|
53
56
|
},
|
|
54
57
|
"repository": {
|
|
55
58
|
"type": "git",
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.RingEdgeConnection = void 0;
|
|
7
|
-
const ws_1 = require("ws");
|
|
8
|
-
const rxjs_1 = require("rxjs");
|
|
9
|
-
const util_1 = require("../util");
|
|
10
|
-
const streaming_connection_base_1 = require("./streaming-connection-base");
|
|
11
|
-
const crypto_1 = __importDefault(require("crypto"));
|
|
12
|
-
// eslint-disable-next-line no-shadow
|
|
13
|
-
var CloseReasonCode;
|
|
14
|
-
(function (CloseReasonCode) {
|
|
15
|
-
CloseReasonCode[CloseReasonCode["NormalClose"] = 0] = "NormalClose";
|
|
16
|
-
// 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' }
|
|
17
|
-
// reason: { code: 5, text: 'Authentication failed: -1' }
|
|
18
|
-
// reason: { code: 5, text: 'Sessions with the provided ID not found' }
|
|
19
|
-
CloseReasonCode[CloseReasonCode["AuthenticationFailed"] = 5] = "AuthenticationFailed";
|
|
20
|
-
// reason: { code: 6, text: 'Timeout waiting for ping' }
|
|
21
|
-
CloseReasonCode[CloseReasonCode["Timeout"] = 6] = "Timeout";
|
|
22
|
-
})(CloseReasonCode || (CloseReasonCode = {}));
|
|
23
|
-
class RingEdgeConnection extends streaming_connection_base_1.StreamingConnectionBase {
|
|
24
|
-
constructor(authToken, camera, options) {
|
|
25
|
-
super(new ws_1.WebSocket('wss://api.prod.signalling.ring.devices.a2z.com:443/ws', {
|
|
26
|
-
headers: {
|
|
27
|
-
Authorization: `Bearer ${authToken}`,
|
|
28
|
-
'X-Sig-API-Version': '4.0',
|
|
29
|
-
'X-Sig-Client-ID': `ring_android-${crypto_1.default
|
|
30
|
-
.randomBytes(4)
|
|
31
|
-
.toString('hex')}`,
|
|
32
|
-
'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',
|
|
33
|
-
'X-Sig-Auth-Type': 'ring_oauth',
|
|
34
|
-
},
|
|
35
|
-
}), options);
|
|
36
|
-
this.camera = camera;
|
|
37
|
-
this.onSessionId = new rxjs_1.ReplaySubject(1);
|
|
38
|
-
this.onOfferSent = new rxjs_1.ReplaySubject(1);
|
|
39
|
-
this.sessionId = null;
|
|
40
|
-
this.addSubscriptions(this.onWsOpen.subscribe(() => {
|
|
41
|
-
(0, util_1.logDebug)(`WebSocket connected for ${camera.name} (Ring Edge)`);
|
|
42
|
-
this.initiateCall().catch((e) => {
|
|
43
|
-
(0, util_1.logError)(e);
|
|
44
|
-
this.callEnded();
|
|
45
|
-
});
|
|
46
|
-
}),
|
|
47
|
-
// The ring-edge session needs a ping every 5 seconds to keep the connection alive
|
|
48
|
-
(0, rxjs_1.interval)(5000).subscribe(() => {
|
|
49
|
-
this.sendSessionMessage('ping');
|
|
50
|
-
}), this.pc.onIceCandidate.subscribe(async (iceCandidate) => {
|
|
51
|
-
await (0, rxjs_1.firstValueFrom)(this.onOfferSent);
|
|
52
|
-
this.sendMessage({
|
|
53
|
-
method: 'ice',
|
|
54
|
-
body: {
|
|
55
|
-
doorbot_id: camera.id,
|
|
56
|
-
ice: iceCandidate.candidate,
|
|
57
|
-
mlineindex: iceCandidate.sdpMLineIndex,
|
|
58
|
-
},
|
|
59
|
-
});
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
async initiateCall() {
|
|
63
|
-
const { sdp } = await this.pc.createOffer();
|
|
64
|
-
this.sendMessage({
|
|
65
|
-
method: 'live_view',
|
|
66
|
-
body: {
|
|
67
|
-
doorbot_id: this.camera.id,
|
|
68
|
-
stream_options: { audio_enabled: true, video_enabled: true },
|
|
69
|
-
sdp,
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
this.onOfferSent.next();
|
|
73
|
-
}
|
|
74
|
-
async handleMessage(message) {
|
|
75
|
-
if (message.body.doorbot_id !== this.camera.id) {
|
|
76
|
-
// ignore messages for other cameras
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
if (['session_created', 'session_started'].includes(message.method) &&
|
|
80
|
-
'session_id' in message.body &&
|
|
81
|
-
!this.sessionId) {
|
|
82
|
-
this.sessionId = message.body.session_id;
|
|
83
|
-
this.onSessionId.next(this.sessionId);
|
|
84
|
-
}
|
|
85
|
-
if (message.body.session_id && message.body.session_id !== this.sessionId) {
|
|
86
|
-
// ignore messages for other sessions
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
switch (message.method) {
|
|
90
|
-
case 'session_created':
|
|
91
|
-
case 'session_started':
|
|
92
|
-
// session already stored above
|
|
93
|
-
return;
|
|
94
|
-
case 'sdp':
|
|
95
|
-
await this.pc.acceptAnswer(message.body);
|
|
96
|
-
this.onCallAnswered.next(message.body.sdp);
|
|
97
|
-
this.activate();
|
|
98
|
-
return;
|
|
99
|
-
case 'ice':
|
|
100
|
-
await this.pc.addIceCandidate({
|
|
101
|
-
candidate: message.body.ice,
|
|
102
|
-
sdpMLineIndex: message.body.mlineindex,
|
|
103
|
-
});
|
|
104
|
-
return;
|
|
105
|
-
case 'pong':
|
|
106
|
-
return;
|
|
107
|
-
case 'notification':
|
|
108
|
-
const { text } = message.body;
|
|
109
|
-
if (text === 'PeerConnectionState::kConnecting' ||
|
|
110
|
-
text === 'PeerConnectionState::kConnected') {
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
break;
|
|
114
|
-
case 'close':
|
|
115
|
-
(0, util_1.logError)('Video stream closed');
|
|
116
|
-
(0, util_1.logError)(message.body);
|
|
117
|
-
this.callEnded();
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
(0, util_1.logError)('UNKNOWN MESSAGE');
|
|
121
|
-
(0, util_1.logError)(message);
|
|
122
|
-
}
|
|
123
|
-
sendSessionMessage(method, body = {}) {
|
|
124
|
-
const sendSessionMessage = () => {
|
|
125
|
-
const message = {
|
|
126
|
-
method,
|
|
127
|
-
body: {
|
|
128
|
-
...body,
|
|
129
|
-
doorbot_id: this.camera.id,
|
|
130
|
-
session_id: this.sessionId,
|
|
131
|
-
},
|
|
132
|
-
};
|
|
133
|
-
this.sendMessage(message);
|
|
134
|
-
};
|
|
135
|
-
if (this.sessionId) {
|
|
136
|
-
// Send immediately if we already have a session id
|
|
137
|
-
// This is needed to send `close` before closing the websocket
|
|
138
|
-
sendSessionMessage();
|
|
139
|
-
}
|
|
140
|
-
else {
|
|
141
|
-
(0, rxjs_1.firstValueFrom)(this.onSessionId)
|
|
142
|
-
.then(sendSessionMessage)
|
|
143
|
-
.catch((e) => (0, util_1.logError)(e));
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
exports.RingEdgeConnection = RingEdgeConnection;
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { BasicPeerConnection } from './peer-connection';
|
|
2
|
-
import { Subscribed } from '../subscribed';
|
|
3
|
-
import { WebSocket } from 'ws';
|
|
4
|
-
import { RtpPacket } from 'werift';
|
|
5
|
-
import { ReplaySubject, Subject } from 'rxjs';
|
|
6
|
-
export interface StreamingConnectionOptions {
|
|
7
|
-
createPeerConnection?: () => BasicPeerConnection;
|
|
8
|
-
}
|
|
9
|
-
export declare abstract class StreamingConnectionBase extends Subscribed {
|
|
10
|
-
protected ws: WebSocket;
|
|
11
|
-
protected options: StreamingConnectionOptions;
|
|
12
|
-
readonly onCallAnswered: ReplaySubject<string>;
|
|
13
|
-
readonly onCallEnded: ReplaySubject<void>;
|
|
14
|
-
readonly onError: ReplaySubject<void>;
|
|
15
|
-
readonly onMessage: ReplaySubject<{
|
|
16
|
-
method: string;
|
|
17
|
-
}>;
|
|
18
|
-
readonly onWsOpen: import("rxjs").Observable<unknown>;
|
|
19
|
-
protected readonly pc: BasicPeerConnection;
|
|
20
|
-
readonly onAudioRtp: Subject<RtpPacket>;
|
|
21
|
-
readonly onVideoRtp: Subject<RtpPacket>;
|
|
22
|
-
constructor(ws: WebSocket, options?: StreamingConnectionOptions);
|
|
23
|
-
protected abstract handleMessage(message: {
|
|
24
|
-
method: string;
|
|
25
|
-
}): Promise<void>;
|
|
26
|
-
protected abstract sendSessionMessage(method: string, body?: Record<any, any>): void;
|
|
27
|
-
protected activate(): void;
|
|
28
|
-
activateCameraSpeaker(): void;
|
|
29
|
-
protected sendMessage(message: Record<any, any>): void;
|
|
30
|
-
sendAudioPacket(rtp: RtpPacket): void;
|
|
31
|
-
private hasEnded;
|
|
32
|
-
protected callEnded(): void;
|
|
33
|
-
stop(): void;
|
|
34
|
-
requestKeyFrame(): void;
|
|
35
|
-
}
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.StreamingConnectionBase = void 0;
|
|
4
|
-
const peer_connection_1 = require("./peer-connection");
|
|
5
|
-
const subscribed_1 = require("../subscribed");
|
|
6
|
-
const rxjs_1 = require("rxjs");
|
|
7
|
-
const operators_1 = require("rxjs/operators");
|
|
8
|
-
const util_1 = require("../util");
|
|
9
|
-
class StreamingConnectionBase extends subscribed_1.Subscribed {
|
|
10
|
-
constructor(ws, options = {}) {
|
|
11
|
-
super();
|
|
12
|
-
this.ws = ws;
|
|
13
|
-
this.options = options;
|
|
14
|
-
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
15
|
-
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
16
|
-
this.onError = new rxjs_1.ReplaySubject(1);
|
|
17
|
-
this.onMessage = new rxjs_1.ReplaySubject();
|
|
18
|
-
this.hasEnded = false;
|
|
19
|
-
if (options.createPeerConnection) {
|
|
20
|
-
// we were passed a custom peer connection factory
|
|
21
|
-
this.pc = options.createPeerConnection();
|
|
22
|
-
// passing rtp packets is not supported for custom peer connections
|
|
23
|
-
this.onAudioRtp = new rxjs_1.Subject();
|
|
24
|
-
this.onVideoRtp = new rxjs_1.Subject();
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
// no custom peer connection factory, use the werift and pass along rtp packets
|
|
28
|
-
const pc = new peer_connection_1.WeriftPeerConnection();
|
|
29
|
-
this.pc = pc;
|
|
30
|
-
this.onAudioRtp = pc.onAudioRtp;
|
|
31
|
-
this.onVideoRtp = pc.onVideoRtp;
|
|
32
|
-
}
|
|
33
|
-
this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
|
|
34
|
-
const onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message'), onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
|
|
35
|
-
this.addSubscriptions(onMessage
|
|
36
|
-
.pipe((0, operators_1.concatMap)((event) => {
|
|
37
|
-
const message = JSON.parse(event.data);
|
|
38
|
-
this.onMessage.next(message);
|
|
39
|
-
return this.handleMessage(message).catch((e) => {
|
|
40
|
-
if (e instanceof Error &&
|
|
41
|
-
e.message.includes('negotiate codecs failed')) {
|
|
42
|
-
e = new Error('Failed to negotiate codecs. This is a known issue with Ring cameras. Please see https://github.com/dgreif/ring/wiki/Streaming-Legacy-Mode');
|
|
43
|
-
}
|
|
44
|
-
this.onError.next(e);
|
|
45
|
-
throw e;
|
|
46
|
-
});
|
|
47
|
-
}))
|
|
48
|
-
.subscribe(), onError.subscribe((e) => {
|
|
49
|
-
(0, util_1.logError)(e);
|
|
50
|
-
this.callEnded();
|
|
51
|
-
}), onClose.subscribe(() => {
|
|
52
|
-
this.callEnded();
|
|
53
|
-
}), this.pc.onConnectionState.subscribe((state) => {
|
|
54
|
-
if (state === 'failed') {
|
|
55
|
-
(0, util_1.logError)('Stream connection failed');
|
|
56
|
-
this.callEnded();
|
|
57
|
-
}
|
|
58
|
-
if (state === 'closed') {
|
|
59
|
-
(0, util_1.logDebug)('Stream connection closed');
|
|
60
|
-
this.callEnded();
|
|
61
|
-
}
|
|
62
|
-
}), this.onError.subscribe((e) => {
|
|
63
|
-
(0, util_1.logError)(e);
|
|
64
|
-
this.callEnded();
|
|
65
|
-
}));
|
|
66
|
-
}
|
|
67
|
-
activate() {
|
|
68
|
-
(0, util_1.logInfo)('Activating Session');
|
|
69
|
-
// the activate_session message is required to keep the stream alive longer than 70 seconds
|
|
70
|
-
this.sendSessionMessage('activate_session');
|
|
71
|
-
this.sendSessionMessage('stream_options', {
|
|
72
|
-
audio_enabled: true,
|
|
73
|
-
video_enabled: true,
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
activateCameraSpeaker() {
|
|
77
|
-
// Fire and forget this call so that callers don't get hung up waiting for answer (which might not happen)
|
|
78
|
-
(0, rxjs_1.firstValueFrom)(this.onCallAnswered)
|
|
79
|
-
.then(() => {
|
|
80
|
-
this.sendSessionMessage('camera_options', {
|
|
81
|
-
stealth_mode: false,
|
|
82
|
-
});
|
|
83
|
-
})
|
|
84
|
-
.catch((e) => {
|
|
85
|
-
(0, util_1.logError)(e);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
sendMessage(message) {
|
|
89
|
-
if (this.hasEnded) {
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
this.ws.send(JSON.stringify(message));
|
|
93
|
-
}
|
|
94
|
-
sendAudioPacket(rtp) {
|
|
95
|
-
if (this.hasEnded) {
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
if (this.pc instanceof peer_connection_1.WeriftPeerConnection) {
|
|
99
|
-
this.pc.returnAudioTrack.writeRtp(rtp);
|
|
100
|
-
}
|
|
101
|
-
else {
|
|
102
|
-
throw new Error('Cannot send audio packets to a custom peer connection implementation');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
callEnded() {
|
|
106
|
-
if (this.hasEnded) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
try {
|
|
110
|
-
this.sendMessage({
|
|
111
|
-
reason: { code: 0, text: '' },
|
|
112
|
-
method: 'close',
|
|
113
|
-
});
|
|
114
|
-
this.ws.close();
|
|
115
|
-
}
|
|
116
|
-
catch (_) {
|
|
117
|
-
// ignore any errors since we are stopping the call
|
|
118
|
-
}
|
|
119
|
-
this.hasEnded = true;
|
|
120
|
-
this.unsubscribe();
|
|
121
|
-
this.onCallEnded.next();
|
|
122
|
-
this.pc.close();
|
|
123
|
-
}
|
|
124
|
-
stop() {
|
|
125
|
-
this.callEnded();
|
|
126
|
-
}
|
|
127
|
-
requestKeyFrame() {
|
|
128
|
-
this.pc.requestKeyFrame?.();
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
exports.StreamingConnectionBase = StreamingConnectionBase;
|