homebridge-ring 9.25.0-beta.1 → 10.0.0-beta.2
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 +49 -0
- package/lib/api/index.d.ts +0 -1
- package/lib/api/index.js +0 -1
- package/lib/api/live-call.d.ts +7 -7
- package/lib/api/live-call.js +168 -75
- package/lib/api/peer-connection.d.ts +7 -4
- package/lib/api/peer-connection.js +25 -6
- package/lib/api/ring-camera.d.ts +3 -16
- package/lib/api/ring-camera.js +10 -102
- package/lib/api/ring-types.d.ts +8 -15
- package/lib/api/ring-types.js +2 -0
- package/lib/homebridge/camera-source.js +1 -3
- package/package.json +2 -4
- package/lib/api/rtp-utils.d.ts +0 -55
- package/lib/api/rtp-utils.js +0 -61
- package/lib/api/sip-call.d.ts +0 -76
- package/lib/api/sip-call.js +0 -242
- package/lib/api/sip-session.d.ts +0 -41
- package/lib/api/sip-session.js +0 -216
- package/lib/api/sip.d.ts +0 -87
- package/lib/api/sip.js +0 -1314
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,55 @@
|
|
|
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.2](https://github.com/dgreif/ring/compare/v10.0.0-beta.1...v10.0.0-beta.2) (2022-03-26)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* support ring edge camera streaming ([b30d99f](https://github.com/dgreif/ring/commit/b30d99f9ffaa1fabc1d09b830cf8ba0e69fe84a0))
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
### Bug Fixes
|
|
14
|
+
|
|
15
|
+
* **api:** allow snapshot to be force-updated based on options ([fa0212a](https://github.com/dgreif/ring/commit/fa0212ab539cc268f8000c5b9ea39a00799cb8d7))
|
|
16
|
+
|
|
17
|
+
## [10.0.0-beta.1](https://github.com/dgreif/ring/compare/v10.0.0-beta.0...v10.0.0-beta.1) (2022-02-27)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### ⚠ BREAKING CHANGES
|
|
21
|
+
|
|
22
|
+
* **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.
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* allow video transcoding to be skipped ([1ef8cdd](https://github.com/dgreif/ring/commit/1ef8cdd54d1f118a8fdcfd28f6970d9c8b7c65f9))
|
|
27
|
+
* **homebridge:** switch back to aac-eld from opus ([7dcd2a0](https://github.com/dgreif/ring/commit/7dcd2a087da7c1b24cc9bdd3c3919af5e6fb7997))
|
|
28
|
+
|
|
29
|
+
## [10.0.0-beta.0](https://github.com/dgreif/ring/compare/v9.25.0-beta.1...v10.0.0-beta.0) (2022-02-18)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### ⚠ BREAKING CHANGES
|
|
33
|
+
|
|
34
|
+
* `ring-client-api` only (homebridge not impacted) - All streaming will now be done using the new Ring api via WebRTC + WebSockets. For those using `camera.streamVideo`, you should not notice any difference. Anyone directly interacting with sip sessions, you will need to migrate to the new `LiveCall` api. It's fairly similar, and should be more reliable long term. A number of other sip-specific methods have been removed from the `RingCamera` api as well.
|
|
35
|
+
* Node 10 is no longer officially supported. It may still work, but could break without warning in the future.
|
|
36
|
+
* **homebridge:** The minimum supported homebridge version for this plugin is now 1.4.0
|
|
37
|
+
* **homebridge:** This change will break all existing automations related to cameras, and will likely move them back to the default room in HomeKit. Once updated, you will have to set up your automations again manually. This is necessary to get camera audio working again after the switch to Opus
|
|
38
|
+
* **homebridge:** Switching to Opus requires the camera to be removed and re-added to HomeKit. Until this is done, you will not receive audio from your cameras.
|
|
39
|
+
|
|
40
|
+
### Features
|
|
41
|
+
|
|
42
|
+
* add floodlight pro ([f749d38](https://github.com/dgreif/ring/commit/f749d38e0528e631864bf20819692a466744e1f2)), closes [#889](https://github.com/dgreif/ring/issues/889)
|
|
43
|
+
* **homebridge:** stream opus audio without ffmpeg for transcoding ([ee142bc](https://github.com/dgreif/ring/commit/ee142bc0eee9dcfda41d162b2105535b8fdcbf9a))
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
### Bug Fixes
|
|
47
|
+
|
|
48
|
+
* drop support for node 10 ([509d556](https://github.com/dgreif/ring/commit/509d55670b3dd4efee1be968618f6bc6b142ae2b))
|
|
49
|
+
* **homebridge:** force cameras to be recreated in HomeKit ([94a1520](https://github.com/dgreif/ring/commit/94a15204f4a9ccbe94968be8db05add6baa5fcfa))
|
|
50
|
+
* **homebridge:** increase minimum homebridge version to 1.4.0 ([d5be74a](https://github.com/dgreif/ring/commit/d5be74aae45a3e353bcaba4fc42fa25c6b2fa37e))
|
|
51
|
+
* remove sip calling in favor of live calls ([7aeae37](https://github.com/dgreif/ring/commit/7aeae37e6e93b68747e14983cea9c0a9229faec6))
|
|
52
|
+
* use Buffer.from ([2b28d90](https://github.com/dgreif/ring/commit/2b28d9028fc7f2c2e11b11cf3b27cc03f097eab2))
|
|
53
|
+
|
|
5
54
|
## [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
55
|
|
|
7
56
|
|
package/lib/api/index.d.ts
CHANGED
package/lib/api/index.js
CHANGED
package/lib/api/live-call.d.ts
CHANGED
|
@@ -10,29 +10,29 @@ export interface FfmpegOptions {
|
|
|
10
10
|
output: SpawnInput[];
|
|
11
11
|
}
|
|
12
12
|
export declare class LiveCall extends Subscribed {
|
|
13
|
-
private sessionId;
|
|
14
13
|
private camera;
|
|
15
14
|
private readonly ws;
|
|
16
15
|
private readonly onWsOpen;
|
|
17
16
|
private readonly onMessage;
|
|
18
17
|
private readonly pc;
|
|
19
|
-
readonly
|
|
18
|
+
readonly onSessionId: ReplaySubject<string>;
|
|
19
|
+
readonly onCallAnswered: ReplaySubject<string>;
|
|
20
20
|
readonly onCallEnded: ReplaySubject<void>;
|
|
21
21
|
private readonly audioSplitter;
|
|
22
22
|
private readonly videoSplitter;
|
|
23
23
|
readonly onVideoRtp: import("rxjs").Subject<RtpPacket>;
|
|
24
24
|
readonly onAudioRtp: import("rxjs").Subject<RtpPacket>;
|
|
25
|
-
|
|
25
|
+
private readonly onOfferSent;
|
|
26
|
+
constructor(authToken: string, camera: RingCamera);
|
|
27
|
+
private initiateCall;
|
|
28
|
+
private sessionId;
|
|
26
29
|
private handleMessage;
|
|
27
30
|
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
31
|
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
33
32
|
activated: boolean;
|
|
34
33
|
activate(): Promise<void>;
|
|
35
34
|
activateCameraSpeaker(): Promise<void>;
|
|
35
|
+
private sendSessionMessage;
|
|
36
36
|
private sendMessage;
|
|
37
37
|
private callEnded;
|
|
38
38
|
stop(): void;
|
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,23 +18,45 @@ 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
|
+
// 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
|
+
}
|
|
22
40
|
class LiveCall extends subscribed_1.Subscribed {
|
|
23
|
-
constructor(
|
|
41
|
+
constructor(authToken, camera) {
|
|
24
42
|
super();
|
|
25
|
-
this.sessionId = sessionId;
|
|
26
43
|
this.camera = camera;
|
|
44
|
+
this.onSessionId = new rxjs_1.ReplaySubject(1);
|
|
27
45
|
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
28
46
|
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
29
47
|
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
30
48
|
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
49
|
+
this.onOfferSent = new rxjs_1.ReplaySubject(1);
|
|
50
|
+
this.sessionId = null;
|
|
31
51
|
this.activated = false;
|
|
32
|
-
const liveCallSession = (0, rtp_utils_1.parseLiveCallSession)(sessionId);
|
|
33
52
|
this.pc = new peer_connection_1.PeerConnection();
|
|
34
|
-
this.ws = new ws_1.WebSocket(
|
|
53
|
+
this.ws = new ws_1.WebSocket('wss://api.prod.signalling.ring.devices.a2z.com:443/ws', {
|
|
35
54
|
headers: {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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',
|
|
39
60
|
},
|
|
40
61
|
});
|
|
41
62
|
this.onAudioRtp = this.pc.onAudioRtp;
|
|
@@ -49,28 +70,109 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
49
70
|
}))
|
|
50
71
|
.subscribe(), this.onWsOpen.subscribe(() => {
|
|
51
72
|
(0, util_1.logDebug)(`WebSocket connected for ${this.camera.name}`);
|
|
73
|
+
this.initiateCall().catch((e) => {
|
|
74
|
+
(0, util_1.logError)(e);
|
|
75
|
+
this.callEnded();
|
|
76
|
+
});
|
|
52
77
|
}), onError.subscribe((e) => {
|
|
53
78
|
(0, util_1.logError)(e);
|
|
54
79
|
this.callEnded();
|
|
55
80
|
}), onClose.subscribe(() => {
|
|
56
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
|
+
}
|
|
57
118
|
}));
|
|
58
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
|
+
}
|
|
59
135
|
handleMessage(message) {
|
|
60
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
|
+
}
|
|
61
151
|
switch (message.method) {
|
|
152
|
+
case 'session_created':
|
|
153
|
+
case 'session_started':
|
|
154
|
+
// session already stored above
|
|
155
|
+
return;
|
|
62
156
|
case 'sdp':
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
this.onCallAnswered.next();
|
|
157
|
+
yield this.pc.acceptAnswer(message.body);
|
|
158
|
+
this.onCallAnswered.next(message.body.sdp);
|
|
66
159
|
return;
|
|
67
160
|
case 'ice':
|
|
68
161
|
yield this.pc.addIceCandidate({
|
|
69
|
-
candidate: message.ice,
|
|
70
|
-
sdpMLineIndex: message.mlineindex,
|
|
162
|
+
candidate: message.body.ice,
|
|
163
|
+
sdpMLineIndex: message.body.mlineindex,
|
|
71
164
|
});
|
|
72
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;
|
|
73
173
|
}
|
|
174
|
+
(0, util_1.logError)('UNKNOWN MESSAGE');
|
|
175
|
+
(0, util_1.logError)(message);
|
|
74
176
|
});
|
|
75
177
|
}
|
|
76
178
|
reservePort(bufferPorts = 0) {
|
|
@@ -79,57 +181,22 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
79
181
|
return ports[0];
|
|
80
182
|
});
|
|
81
183
|
}
|
|
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
184
|
startTranscoding(ffmpegOptions) {
|
|
131
185
|
return __awaiter(this, void 0, void 0, function* () {
|
|
132
|
-
const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false,
|
|
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({
|
|
133
200
|
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
134
201
|
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
135
202
|
: []), ...(ffmpegOptions.output || [])),
|
|
@@ -141,8 +208,24 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
141
208
|
info: util_1.logDebug,
|
|
142
209
|
},
|
|
143
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
|
+
}
|
|
144
227
|
this.onCallEnded.subscribe(() => ff.stop());
|
|
145
|
-
ff.writeStdin(
|
|
228
|
+
ff.writeStdin(inputSdp);
|
|
146
229
|
// Activate the stream now that ffmpeg is ready to receive
|
|
147
230
|
yield this.activate();
|
|
148
231
|
});
|
|
@@ -153,32 +236,42 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
153
236
|
return;
|
|
154
237
|
}
|
|
155
238
|
this.activated = true;
|
|
156
|
-
yield (0, rxjs_1.firstValueFrom)(this.
|
|
157
|
-
|
|
158
|
-
this.
|
|
159
|
-
video_enabled: true,
|
|
160
|
-
audio_enabled: true,
|
|
161
|
-
method: 'stream_options',
|
|
162
|
-
});
|
|
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');
|
|
163
242
|
});
|
|
164
243
|
}
|
|
165
244
|
activateCameraSpeaker() {
|
|
166
245
|
return __awaiter(this, void 0, void 0, function* () {
|
|
167
246
|
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
168
|
-
this.
|
|
247
|
+
this.sendSessionMessage('camera_options', {
|
|
169
248
|
stealth_mode: false,
|
|
170
|
-
method: 'camera_options',
|
|
171
249
|
});
|
|
172
250
|
});
|
|
173
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
|
+
}
|
|
174
268
|
sendMessage(message) {
|
|
175
269
|
this.ws.send(JSON.stringify(message));
|
|
176
270
|
}
|
|
177
271
|
callEnded() {
|
|
178
272
|
try {
|
|
179
|
-
this.
|
|
180
|
-
reason: { code:
|
|
181
|
-
method: 'close',
|
|
273
|
+
this.sendSessionMessage('close', {
|
|
274
|
+
reason: { code: CloseReasonCode.NormalClose, text: '' },
|
|
182
275
|
});
|
|
183
276
|
this.ws.close();
|
|
184
277
|
}
|
|
@@ -1,17 +1,20 @@
|
|
|
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();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
createOffer(): Promise<import("@koush/werift").RTCSessionDescription>;
|
|
14
|
+
acceptAnswer(answer: {
|
|
15
|
+
type: 'answer';
|
|
13
16
|
sdp: string;
|
|
14
|
-
}): Promise<
|
|
17
|
+
}): Promise<void>;
|
|
15
18
|
addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
|
|
16
19
|
close(): void;
|
|
17
20
|
}
|
|
@@ -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: 'profile-level-id=42801F',
|
|
45
47
|
}),
|
|
46
48
|
],
|
|
47
49
|
},
|
|
@@ -77,13 +79,30 @@ 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
|
+
acceptAnswer(answer) {
|
|
82
104
|
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
-
yield this.pc.setRemoteDescription(
|
|
84
|
-
const answer = yield this.pc.createAnswer();
|
|
85
|
-
yield this.pc.setLocalDescription(answer);
|
|
86
|
-
return answer;
|
|
105
|
+
yield this.pc.setRemoteDescription(answer);
|
|
87
106
|
});
|
|
88
107
|
}
|
|
89
108
|
addIceCandidate(candidate) {
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -2,12 +2,9 @@
|
|
|
2
2
|
import { ActiveDing, CameraData, CameraDeviceSettingsData, CameraEventOptions, CameraEventResponse, CameraHealth, HistoryOptions, PeriodicFootageResponse, VideoSearchResponse } from './ring-types';
|
|
3
3
|
import { RingRestClient } from './rest-client';
|
|
4
4
|
import { BehaviorSubject, Subject } from 'rxjs';
|
|
5
|
-
import { SrtpOptions } from '@homebridge/camera-utils';
|
|
6
5
|
import { DeepPartial } from './util';
|
|
7
|
-
import { FfmpegOptions, SipSession } from './sip-session';
|
|
8
|
-
import { SipOptions } from './sip-call';
|
|
9
6
|
import { Subscribed } from './subscribed';
|
|
10
|
-
import { LiveCall } from './live-call';
|
|
7
|
+
import { FfmpegOptions, LiveCall } from './live-call';
|
|
11
8
|
export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'>): number | null;
|
|
12
9
|
export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
|
|
13
10
|
accountId: string;
|
|
@@ -45,6 +42,7 @@ export declare class RingCamera extends Subscribed {
|
|
|
45
42
|
get isCharging(): boolean;
|
|
46
43
|
get operatingOnBattery(): boolean;
|
|
47
44
|
get isOffline(): boolean;
|
|
45
|
+
get isRingEdgeEnabled(): boolean;
|
|
48
46
|
get hasInHomeDoorbell(): boolean;
|
|
49
47
|
doorbotUrl(path?: string): string;
|
|
50
48
|
deviceUrl(path?: string): string;
|
|
@@ -56,10 +54,6 @@ export declare class RingCamera extends Subscribed {
|
|
|
56
54
|
setInHomeDoorbell(enable: boolean): Promise<boolean>;
|
|
57
55
|
getHealth(): Promise<CameraHealth>;
|
|
58
56
|
startLiveCall(): Promise<LiveCall>;
|
|
59
|
-
startVideoOnDemand(): Promise<("" | ActiveDing) & import("./rest-client").ExtendedResponse>;
|
|
60
|
-
private pollForActiveDing;
|
|
61
|
-
private expiredDingIds;
|
|
62
|
-
getSipConnectionDetails(): Promise<ActiveDing>;
|
|
63
57
|
private removeDingById;
|
|
64
58
|
processActiveDing(ding: ActiveDing): void;
|
|
65
59
|
getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
|
|
@@ -92,15 +86,8 @@ export declare class RingCamera extends Subscribed {
|
|
|
92
86
|
maxWaitMs?: number;
|
|
93
87
|
force?: boolean;
|
|
94
88
|
}): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
95
|
-
getSipOptions(): Promise<SipOptions>;
|
|
96
|
-
getUpdatedSipOptions(expiredDingId: string): Promise<SipOptions>;
|
|
97
|
-
createSipSession(options?: {
|
|
98
|
-
audio?: SrtpOptions;
|
|
99
|
-
video?: SrtpOptions;
|
|
100
|
-
skipFfmpegCheck?: boolean;
|
|
101
|
-
}): Promise<SipSession>;
|
|
102
89
|
recordToFile(outputPath: string, duration?: number): Promise<void>;
|
|
103
|
-
streamVideo(ffmpegOptions: FfmpegOptions): Promise<
|
|
90
|
+
streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall>;
|
|
104
91
|
subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
105
92
|
unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
106
93
|
subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|