homebridge-ring 10.0.0-beta.0 → 10.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/CHANGELOG.md +12 -0
- package/lib/api/live-call.js +9 -1
- package/lib/homebridge/camera-source.js +121 -43
- package/lib/homebridge/ring-platform.js +1 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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.1](https://github.com/dgreif/ring/compare/v10.0.0-beta.0...v10.0.0-beta.1) (2022-02-27)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* **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.
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
* allow video transcoding to be skipped ([1ef8cdd](https://github.com/dgreif/ring/commit/1ef8cdd54d1f118a8fdcfd28f6970d9c8b7c65f9))
|
|
15
|
+
* **homebridge:** switch back to aac-eld from opus ([7dcd2a0](https://github.com/dgreif/ring/commit/7dcd2a087da7c1b24cc9bdd3c3919af5e6fb7997))
|
|
16
|
+
|
|
5
17
|
## [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
18
|
|
|
7
19
|
|
package/lib/api/live-call.js
CHANGED
|
@@ -19,6 +19,14 @@ const operators_1 = require("rxjs/operators");
|
|
|
19
19
|
const subscribed_1 = require("./subscribed");
|
|
20
20
|
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
21
21
|
const ffmpeg_1 = require("./ffmpeg");
|
|
22
|
+
function getCleanSdp(sdp, includeVideo) {
|
|
23
|
+
return sdp
|
|
24
|
+
.split('\nm=')
|
|
25
|
+
.slice(1)
|
|
26
|
+
.map((section) => 'm=' + section)
|
|
27
|
+
.filter((section) => includeVideo || !section.startsWith('m=video'))
|
|
28
|
+
.join('\n');
|
|
29
|
+
}
|
|
22
30
|
class LiveCall extends subscribed_1.Subscribed {
|
|
23
31
|
constructor(sessionId, camera) {
|
|
24
32
|
super();
|
|
@@ -92,7 +100,7 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
92
100
|
...(ffmpegOptions.input || []),
|
|
93
101
|
'-i',
|
|
94
102
|
'pipe:',
|
|
95
|
-
],
|
|
103
|
+
], ringSdp = yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered), inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
|
|
96
104
|
.replace(/m=audio \d+/, `m=audio ${audioPort}`)
|
|
97
105
|
.replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
|
|
98
106
|
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
@@ -17,6 +17,7 @@ const operators_1 = require("rxjs/operators");
|
|
|
17
17
|
const rxjs_1 = require("rxjs");
|
|
18
18
|
const fs_1 = require("fs");
|
|
19
19
|
const util_2 = require("util");
|
|
20
|
+
const ffmpeg_1 = require("../api/ffmpeg");
|
|
20
21
|
const werift_1 = require("@koush/werift");
|
|
21
22
|
const readFileAsync = (0, util_2.promisify)(fs_1.readFile), cameraOfflinePath = require.resolve('../../media/camera-offline.jpg'), snapshotsBlockedPath = require.resolve('../../media/snapshots-blocked.jpg');
|
|
22
23
|
function getDurationSeconds(start) {
|
|
@@ -45,13 +46,18 @@ class StreamingSession {
|
|
|
45
46
|
this.videoSrtp = (0, camera_utils_1.generateSrtpOptions)();
|
|
46
47
|
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
47
48
|
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
48
|
-
this.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
})
|
|
49
|
+
this.libfdkAacInstalledPromise = (0, camera_utils_1.doesFfmpegSupportCodec)('libfdk_aac', (0, ffmpeg_1.getFfmpegPath)())
|
|
50
|
+
.then((supported) => {
|
|
51
|
+
if (!supported) {
|
|
52
|
+
(0, util_1.logError)('Streaming video only - found ffmpeg, but libfdk_aac is not installed. See https://github.com/dgreif/ring/wiki/FFmpeg for details.');
|
|
53
|
+
}
|
|
54
|
+
return supported;
|
|
55
|
+
})
|
|
56
|
+
.catch(() => {
|
|
57
|
+
(0, util_1.logError)('Streaming video only - ffmpeg was not found. See https://github.com/dgreif/ring/wiki/FFmpeg for details.');
|
|
58
|
+
return false;
|
|
59
|
+
});
|
|
60
|
+
const { targetAddress, video: { port: videoPort }, } = prepareStreamRequest,
|
|
55
61
|
// used to encrypt rtcp to HomeKit for keepalive
|
|
56
62
|
videoSrtcpSession = new werift_1.SrtcpSession(getSessionConfig(this.videoSrtp)), onReturnPacketReceived = new rxjs_1.Subject();
|
|
57
63
|
// Watch return packets to detect a dead stream from the HomeKit side
|
|
@@ -61,21 +67,9 @@ class StreamingSession {
|
|
|
61
67
|
onReturnPacketReceived.next(null);
|
|
62
68
|
return null;
|
|
63
69
|
});
|
|
64
|
-
this.audioSplitter.addMessageHandler((
|
|
65
|
-
if (!isRtpMessage) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
70
|
+
this.audioSplitter.addMessageHandler(() => {
|
|
68
71
|
// return packet from HomeKit
|
|
69
72
|
onReturnPacketReceived.next(null);
|
|
70
|
-
// Make sure the external speaker on the camera is active since we will be sending packets to it
|
|
71
|
-
if (!this.cameraSpeakerActive) {
|
|
72
|
-
this.cameraSpeakerActive = true;
|
|
73
|
-
this.liveCall.activateCameraSpeaker().catch(util_1.logError);
|
|
74
|
-
}
|
|
75
|
-
// decrypt the packet
|
|
76
|
-
const decryptedMessage = returnAudioSrtpSession.decrypt(message), rtp = werift_1.RtpPacket.deSerialize(decryptedMessage);
|
|
77
|
-
// send to Ring - werift will handle encryption and other header params
|
|
78
|
-
this.liveCall.sendAudioPacket(rtp);
|
|
79
73
|
return null;
|
|
80
74
|
});
|
|
81
75
|
liveCall.addSubscriptions((0, rxjs_1.merge)((0, rxjs_1.of)(true).pipe((0, operators_1.delay)(15000)), onReturnPacketReceived)
|
|
@@ -107,27 +101,9 @@ class StreamingSession {
|
|
|
107
101
|
activate(request) {
|
|
108
102
|
return __awaiter(this, void 0, void 0, function* () {
|
|
109
103
|
let sentVideo = false;
|
|
110
|
-
const { targetAddress, audio: { port: audioPort }, video: { port: videoPort }, } = this.prepareStreamRequest,
|
|
104
|
+
const { targetAddress, audio: { port: audioPort, srtp_key: remoteAudioSrtpKey, srtp_salt: remoteAudioSrtpSalt, }, video: { port: videoPort }, } = this.prepareStreamRequest,
|
|
111
105
|
// use to encrypt Ring video to HomeKit
|
|
112
|
-
|
|
113
|
-
let firstTimestamp = 0, packetCount = 0;
|
|
114
|
-
// Set up packet forwarding for audio stream
|
|
115
|
-
this.liveCall.addSubscriptions(this.liveCall.onAudioRtp.subscribe(({ header, payload }) => {
|
|
116
|
-
if (!firstTimestamp) {
|
|
117
|
-
firstTimestamp = header.timestamp;
|
|
118
|
-
}
|
|
119
|
-
header.ssrc = this.audioSsrc;
|
|
120
|
-
header.payloadType = request.audio.pt;
|
|
121
|
-
header.timestamp = firstTimestamp + packetCount * 3 * 160;
|
|
122
|
-
packetCount++;
|
|
123
|
-
const encryptedPacket = audioSrtpSession.encrypt(payload, header);
|
|
124
|
-
this.audioSplitter
|
|
125
|
-
.send(encryptedPacket, {
|
|
126
|
-
port: audioPort,
|
|
127
|
-
address: targetAddress,
|
|
128
|
-
})
|
|
129
|
-
.catch(util_1.logError);
|
|
130
|
-
}));
|
|
106
|
+
videoSrtpSession = new werift_1.SrtpSession(getSessionConfig(this.videoSrtp));
|
|
131
107
|
// Set up packet forwarding for video stream
|
|
132
108
|
this.liveCall.addSubscriptions(this.liveCall.onVideoRtp.subscribe(({ header, payload }) => {
|
|
133
109
|
header.ssrc = this.videoSsrc;
|
|
@@ -144,7 +120,109 @@ class StreamingSession {
|
|
|
144
120
|
})
|
|
145
121
|
.catch(util_1.logError);
|
|
146
122
|
}));
|
|
147
|
-
yield this.
|
|
123
|
+
const shouldTranscodeAudio = yield this.libfdkAacInstalledPromise;
|
|
124
|
+
if (!shouldTranscodeAudio) {
|
|
125
|
+
return this.liveCall.activate();
|
|
126
|
+
}
|
|
127
|
+
const transcodingPromise = this.liveCall.startTranscoding({
|
|
128
|
+
input: ['-vn'],
|
|
129
|
+
audio: [
|
|
130
|
+
'-map',
|
|
131
|
+
'0:a',
|
|
132
|
+
// OPUS specific - it works, but audio is very choppy
|
|
133
|
+
// '-acodec',
|
|
134
|
+
// 'libopus',
|
|
135
|
+
// '-vbr',
|
|
136
|
+
// 'on',
|
|
137
|
+
// '-frame_duration',
|
|
138
|
+
// 20,
|
|
139
|
+
// '-application',
|
|
140
|
+
// 'lowdelay',
|
|
141
|
+
// AAC-eld specific
|
|
142
|
+
'-acodec',
|
|
143
|
+
'libfdk_aac',
|
|
144
|
+
'-profile:a',
|
|
145
|
+
'aac_eld',
|
|
146
|
+
// Shared options
|
|
147
|
+
'-flags',
|
|
148
|
+
'+global_header',
|
|
149
|
+
'-ac',
|
|
150
|
+
`${request.audio.channel}`,
|
|
151
|
+
'-ar',
|
|
152
|
+
`${request.audio.sample_rate}k`,
|
|
153
|
+
'-b:a',
|
|
154
|
+
`${request.audio.max_bit_rate}k`,
|
|
155
|
+
'-bufsize',
|
|
156
|
+
`${request.audio.max_bit_rate * 4}k`,
|
|
157
|
+
'-payload_type',
|
|
158
|
+
request.audio.pt,
|
|
159
|
+
'-ssrc',
|
|
160
|
+
this.audioSsrc,
|
|
161
|
+
'-f',
|
|
162
|
+
'rtp',
|
|
163
|
+
'-srtp_out_suite',
|
|
164
|
+
'AES_CM_128_HMAC_SHA1_80',
|
|
165
|
+
'-srtp_out_params',
|
|
166
|
+
(0, camera_utils_1.encodeSrtpOptions)(this.audioSrtp),
|
|
167
|
+
`srtp://${targetAddress}:${audioPort}?pkt_size=188`,
|
|
168
|
+
],
|
|
169
|
+
video: false,
|
|
170
|
+
output: [],
|
|
171
|
+
});
|
|
172
|
+
let cameraSpeakerActive = false;
|
|
173
|
+
// used to decrypt return audio from HomeKit to Ring
|
|
174
|
+
const remoteAudioSrtpOptions = {
|
|
175
|
+
srtpKey: remoteAudioSrtpKey,
|
|
176
|
+
srtpSalt: remoteAudioSrtpSalt,
|
|
177
|
+
}, audioSrtpSession = new werift_1.SrtpSession(getSessionConfig(remoteAudioSrtpOptions)), returnAudioTranscodedSplitter = new camera_utils_1.RtpSplitter(({ message }) => {
|
|
178
|
+
if (!cameraSpeakerActive) {
|
|
179
|
+
cameraSpeakerActive = true;
|
|
180
|
+
this.liveCall.activateCameraSpeaker().catch(util_1.logError);
|
|
181
|
+
}
|
|
182
|
+
// decrypt the message
|
|
183
|
+
try {
|
|
184
|
+
const rtp = werift_1.RtpPacket.deSerialize(message);
|
|
185
|
+
rtp.payload = audioSrtpSession.decrypt(rtp.payload);
|
|
186
|
+
// send to Ring - werift will handle encryption and other header params
|
|
187
|
+
this.liveCall.sendAudioPacket(rtp);
|
|
188
|
+
}
|
|
189
|
+
catch (_) {
|
|
190
|
+
// deSerialize will sometimes fail, but the errors can be ignored
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}), returnAudioTranscoder = new camera_utils_1.ReturnAudioTranscoder({
|
|
194
|
+
prepareStreamRequest: this.prepareStreamRequest,
|
|
195
|
+
incomingAudioOptions: {
|
|
196
|
+
ssrc: this.audioSsrc,
|
|
197
|
+
rtcpPort: 0, // we don't care about rtcp for incoming audio
|
|
198
|
+
},
|
|
199
|
+
outputArgs: [
|
|
200
|
+
'-acodec',
|
|
201
|
+
'libopus',
|
|
202
|
+
'-flags',
|
|
203
|
+
'+global_header',
|
|
204
|
+
'-ac',
|
|
205
|
+
2,
|
|
206
|
+
'-ar',
|
|
207
|
+
'48k',
|
|
208
|
+
'-f',
|
|
209
|
+
'rtp',
|
|
210
|
+
`rtp://127.0.0.1:${yield returnAudioTranscodedSplitter.portPromise}`,
|
|
211
|
+
],
|
|
212
|
+
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
213
|
+
logger: {
|
|
214
|
+
info: util_1.logDebug,
|
|
215
|
+
error: util_1.logError,
|
|
216
|
+
},
|
|
217
|
+
logLabel: `Return Audio (${this.ringCamera.name})`,
|
|
218
|
+
returnAudioSplitter: this.audioSplitter,
|
|
219
|
+
});
|
|
220
|
+
this.liveCall.onCallEnded.pipe((0, operators_1.take)(1)).subscribe(() => {
|
|
221
|
+
returnAudioTranscoder.stop();
|
|
222
|
+
returnAudioTranscodedSplitter.close();
|
|
223
|
+
});
|
|
224
|
+
yield returnAudioTranscoder.start();
|
|
225
|
+
yield transcodingPromise;
|
|
148
226
|
});
|
|
149
227
|
}
|
|
150
228
|
stop() {
|
|
@@ -181,8 +259,8 @@ class CameraSource {
|
|
|
181
259
|
audio: {
|
|
182
260
|
codecs: [
|
|
183
261
|
{
|
|
184
|
-
type: "
|
|
185
|
-
samplerate:
|
|
262
|
+
type: "AAC-eld" /* AAC_ELD */,
|
|
263
|
+
samplerate: 16 /* KHZ_16 */,
|
|
186
264
|
},
|
|
187
265
|
],
|
|
188
266
|
},
|
|
@@ -211,8 +211,7 @@ class RingPlatform {
|
|
|
211
211
|
}
|
|
212
212
|
this.log.info(`Configuring ${cameras.length} cameras and ${hapDevices.length} devices for location "${location.name}" - locationId: ${location.id}`);
|
|
213
213
|
hapDevices.forEach(({ deviceType, device, isCamera, id, name, AccessoryClass }) => {
|
|
214
|
-
|
|
215
|
-
const cameraOpusQualifier = isCamera ? 'opus' : '', uuid = hap_1.hap.uuid.generate(debugPrefix + id + cameraOpusQualifier), displayName = debugPrefix + name;
|
|
214
|
+
const uuid = hap_1.hap.uuid.generate(debugPrefix + id), displayName = debugPrefix + name;
|
|
216
215
|
if (!AccessoryClass ||
|
|
217
216
|
(config.hideLightGroups &&
|
|
218
217
|
deviceType === api_1.RingDeviceType.BeamsLightGroupSwitch) ||
|
package/package.json
CHANGED