homebridge-ring 11.5.0 → 11.5.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/lib/base-accessory.js +36 -34
- package/lib/base-data-accessory.js +28 -28
- package/lib/base-device-accessory.js +40 -34
- package/lib/beam.js +38 -48
- package/lib/brightness-only.js +23 -30
- package/lib/camera-source.js +229 -263
- package/lib/camera.js +74 -67
- package/lib/chime.js +39 -37
- package/lib/co-alarm.js +13 -15
- package/lib/config.js +18 -29
- package/lib/contact-sensor.js +13 -15
- package/lib/fan.js +23 -37
- package/lib/flood-freeze-sensor.js +28 -24
- package/lib/freeze-sensor.js +19 -19
- package/lib/hap.js +4 -15
- package/lib/homebridge-ui/server.js +21 -32
- package/lib/index.js +7 -12
- package/lib/location-mode-switch.js +39 -42
- package/lib/lock.js +28 -29
- package/lib/motion-sensor.js +10 -14
- package/lib/multi-level-switch.js +24 -38
- package/lib/opus-repacketizer.js +66 -23
- package/lib/outlet.js +16 -26
- package/lib/panic-buttons.js +41 -45
- package/lib/ring-platform.js +186 -175
- package/lib/security-panel.js +84 -74
- package/lib/smoke-alarm.js +13 -15
- package/lib/smoke-co-listener.js +18 -18
- package/lib/switch.js +15 -25
- package/lib/target-value-timer.js +4 -8
- package/lib/temperature-sensor.js +11 -15
- package/lib/thermostat.js +60 -77
- package/lib/unknown-zwave-switch.js +15 -25
- package/lib/util.js +6 -10
- package/lib/water-sensor.js +19 -19
- package/package.json +9 -6
- package/lib/server.js +0 -58
package/lib/camera-source.js
CHANGED
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const _fs = require("fs");
|
|
16
|
-
const _util1 = require("util");
|
|
17
|
-
const _ffmpeg = require("ring-client-api/ffmpeg");
|
|
18
|
-
const _werift = require("werift");
|
|
19
|
-
const _opusRepacketizer = require("./opus-repacketizer");
|
|
20
|
-
const readFileAsync = (0, _util1.promisify)(_fs.readFile), cameraOfflinePath = require.resolve('../media/camera-offline.jpg'), snapshotsBlockedPath = require.resolve('../media/snapshots-blocked.jpg');
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CameraSource = void 0;
|
|
4
|
+
const hap_1 = require("./hap");
|
|
5
|
+
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
6
|
+
const util_1 = require("ring-client-api/util");
|
|
7
|
+
const operators_1 = require("rxjs/operators");
|
|
8
|
+
const rxjs_1 = require("rxjs");
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const util_2 = require("util");
|
|
11
|
+
const ffmpeg_1 = require("ring-client-api/ffmpeg");
|
|
12
|
+
const werift_1 = require("werift");
|
|
13
|
+
const opus_repacketizer_1 = require("./opus-repacketizer");
|
|
14
|
+
const readFileAsync = (0, util_2.promisify)(fs_1.readFile), cameraOfflinePath = require.resolve('../media/camera-offline.jpg'), snapshotsBlockedPath = require.resolve('../media/snapshots-blocked.jpg');
|
|
21
15
|
function getDurationSeconds(start) {
|
|
22
16
|
return (Date.now() - start) / 1000;
|
|
23
17
|
}
|
|
@@ -27,22 +21,87 @@ function getSessionConfig(srtpOptions) {
|
|
|
27
21
|
localMasterKey: srtpOptions.srtpKey,
|
|
28
22
|
localMasterSalt: srtpOptions.srtpSalt,
|
|
29
23
|
remoteMasterKey: srtpOptions.srtpKey,
|
|
30
|
-
remoteMasterSalt: srtpOptions.srtpSalt
|
|
24
|
+
remoteMasterSalt: srtpOptions.srtpSalt,
|
|
31
25
|
},
|
|
32
|
-
profile: 1
|
|
26
|
+
profile: 1,
|
|
33
27
|
};
|
|
34
28
|
}
|
|
35
29
|
class StreamingSessionWrapper {
|
|
30
|
+
constructor(streamingSession, prepareStreamRequest, ringCamera, start) {
|
|
31
|
+
this.streamingSession = streamingSession;
|
|
32
|
+
this.prepareStreamRequest = prepareStreamRequest;
|
|
33
|
+
this.ringCamera = ringCamera;
|
|
34
|
+
this.start = start;
|
|
35
|
+
this.audioSsrc = hap_1.hap.CameraController.generateSynchronisationSource();
|
|
36
|
+
this.videoSsrc = hap_1.hap.CameraController.generateSynchronisationSource();
|
|
37
|
+
this.audioSrtp = (0, camera_utils_1.generateSrtpOptions)();
|
|
38
|
+
this.videoSrtp = (0, camera_utils_1.generateSrtpOptions)();
|
|
39
|
+
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
40
|
+
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
41
|
+
this.repacketizeAudioSplitter = new camera_utils_1.RtpSplitter();
|
|
42
|
+
this.libfdkAacInstalledPromise = (0, camera_utils_1.doesFfmpegSupportCodec)('libfdk_aac', (0, ffmpeg_1.getFfmpegPath)())
|
|
43
|
+
.then((supported) => {
|
|
44
|
+
if (!supported) {
|
|
45
|
+
(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.');
|
|
46
|
+
}
|
|
47
|
+
return supported;
|
|
48
|
+
})
|
|
49
|
+
.catch(() => {
|
|
50
|
+
(0, util_1.logError)('Streaming video only - ffmpeg was not found. See https://github.com/dgreif/ring/wiki/FFmpeg for details.');
|
|
51
|
+
return false;
|
|
52
|
+
});
|
|
53
|
+
const { targetAddress, video: { port: videoPort }, } = prepareStreamRequest,
|
|
54
|
+
// used to encrypt rtcp to HomeKit for keepalive
|
|
55
|
+
videoSrtcpSession = new werift_1.SrtcpSession(getSessionConfig(this.videoSrtp)), onReturnPacketReceived = new rxjs_1.Subject();
|
|
56
|
+
// Watch return packets to detect a dead stream from the HomeKit side
|
|
57
|
+
// This can happen if the user force-quits the Home app
|
|
58
|
+
this.videoSplitter.addMessageHandler(() => {
|
|
59
|
+
// return packet from HomeKit
|
|
60
|
+
onReturnPacketReceived.next(null);
|
|
61
|
+
return null;
|
|
62
|
+
});
|
|
63
|
+
this.audioSplitter.addMessageHandler(() => {
|
|
64
|
+
// return packet from HomeKit
|
|
65
|
+
onReturnPacketReceived.next(null);
|
|
66
|
+
return null;
|
|
67
|
+
});
|
|
68
|
+
streamingSession.addSubscriptions((0, rxjs_1.merge)((0, rxjs_1.of)(true).pipe((0, operators_1.delay)(15000)), onReturnPacketReceived)
|
|
69
|
+
.pipe((0, operators_1.debounceTime)(5000))
|
|
70
|
+
.subscribe(() => {
|
|
71
|
+
(0, util_1.logInfo)(`Live stream for ${this.ringCamera.name} appears to be inactive. (${getDurationSeconds(start)}s)`);
|
|
72
|
+
streamingSession.stop();
|
|
73
|
+
}));
|
|
74
|
+
// Periodically send a blank RTCP packet to the HomeKit video port
|
|
75
|
+
// Without this, HomeKit assumes the stream is dead after 30 second and sends a stop request
|
|
76
|
+
streamingSession.addSubscriptions((0, rxjs_1.interval)(500).subscribe(() => {
|
|
77
|
+
const senderInfo = new werift_1.RtcpSenderInfo({
|
|
78
|
+
ntpTimestamp: BigInt(0),
|
|
79
|
+
packetCount: 0,
|
|
80
|
+
octetCount: 0,
|
|
81
|
+
rtpTimestamp: 0,
|
|
82
|
+
}), senderReport = new werift_1.RtcpSrPacket({
|
|
83
|
+
ssrc: this.videoSsrc,
|
|
84
|
+
senderInfo: senderInfo,
|
|
85
|
+
}), message = videoSrtcpSession.encrypt(senderReport.serialize());
|
|
86
|
+
this.videoSplitter
|
|
87
|
+
.send(message, {
|
|
88
|
+
port: videoPort,
|
|
89
|
+
address: targetAddress,
|
|
90
|
+
})
|
|
91
|
+
.catch(util_1.logError);
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
36
94
|
listenForAudioPackets(startStreamRequest) {
|
|
37
|
-
const { targetAddress
|
|
38
|
-
|
|
95
|
+
const { targetAddress, audio: { port: audioPort }, } = this.prepareStreamRequest, { audio: { codec: audioCodec, sample_rate: audioSampleRate }, } = startStreamRequest,
|
|
96
|
+
// Repacketize the audio stream after it's been transcoded
|
|
97
|
+
opusRepacketizer = new opus_repacketizer_1.OpusRepacketizer(1), audioIntervalScale = audioSampleRate / 8, audioSrtpSession = new werift_1.SrtpSession(getSessionConfig(this.audioSrtp));
|
|
39
98
|
let firstTimestamp, audioPacketCount = 0;
|
|
40
|
-
this.repacketizeAudioSplitter.addMessageHandler(({ message
|
|
41
|
-
let rtp =
|
|
42
|
-
if (audioCodec ===
|
|
99
|
+
this.repacketizeAudioSplitter.addMessageHandler(({ message }) => {
|
|
100
|
+
let rtp = werift_1.RtpPacket.deSerialize(message);
|
|
101
|
+
if (audioCodec === "OPUS" /* AudioStreamingCodecType.OPUS */) {
|
|
43
102
|
// borrowed from scrypted
|
|
44
103
|
// Original source: https://github.com/koush/scrypted/blob/c13ba09889c3e0d9d3724cb7d49253c9d787fb97/plugins/homekit/src/types/camera/camera-streaming-srtp-sender.ts#L124-L143
|
|
45
|
-
rtp = opusRepacketizer.repacketize(
|
|
104
|
+
rtp = opusRepacketizer.repacketize(werift_1.RtpPacket.deSerialize(message));
|
|
46
105
|
if (!rtp) {
|
|
47
106
|
return null;
|
|
48
107
|
}
|
|
@@ -63,63 +122,70 @@ class StreamingSessionWrapper {
|
|
|
63
122
|
// audio will work so long as the rtp timestamps are created properly: which is a construct of the sample rate
|
|
64
123
|
// HAP requests, and the packet time is respected,
|
|
65
124
|
// opus 48khz will work just fine.
|
|
66
|
-
rtp.header.timestamp =
|
|
125
|
+
rtp.header.timestamp =
|
|
126
|
+
(firstTimestamp + audioPacketCount * 180 * audioIntervalScale) %
|
|
127
|
+
0xffffffff;
|
|
67
128
|
audioPacketCount++;
|
|
68
129
|
}
|
|
69
130
|
// encrypt the packet
|
|
70
131
|
const encryptedPacket = audioSrtpSession.encrypt(rtp.payload, rtp.header);
|
|
71
132
|
// send the encrypted packet to HomeKit
|
|
72
|
-
this.audioSplitter
|
|
133
|
+
this.audioSplitter
|
|
134
|
+
.send(encryptedPacket, {
|
|
73
135
|
port: audioPort,
|
|
74
|
-
address: targetAddress
|
|
75
|
-
})
|
|
136
|
+
address: targetAddress,
|
|
137
|
+
})
|
|
138
|
+
.catch(util_1.logError);
|
|
76
139
|
return null;
|
|
77
140
|
});
|
|
78
141
|
}
|
|
79
142
|
async activate(request) {
|
|
80
143
|
let sentVideo = false;
|
|
81
|
-
const { targetAddress
|
|
82
|
-
|
|
144
|
+
const { targetAddress, audio: { srtp_key: remoteAudioSrtpKey, srtp_salt: remoteAudioSrtpSalt }, video: { port: videoPort }, } = this.prepareStreamRequest,
|
|
145
|
+
// use to encrypt Ring video to HomeKit
|
|
146
|
+
videoSrtpSession = new werift_1.SrtpSession(getSessionConfig(this.videoSrtp));
|
|
83
147
|
// Set up packet forwarding for video stream
|
|
84
|
-
this.streamingSession.addSubscriptions(this.streamingSession.onVideoRtp.subscribe(({ header
|
|
148
|
+
this.streamingSession.addSubscriptions(this.streamingSession.onVideoRtp.subscribe(({ header, payload }) => {
|
|
85
149
|
header.ssrc = this.videoSsrc;
|
|
86
150
|
header.payloadType = request.video.pt;
|
|
87
151
|
const encryptedPacket = videoSrtpSession.encrypt(payload, header);
|
|
88
152
|
if (!sentVideo) {
|
|
89
153
|
sentVideo = true;
|
|
90
|
-
(0,
|
|
154
|
+
(0, util_1.logInfo)(`Received stream data from ${this.ringCamera.name} (${getDurationSeconds(this.start)}s)`);
|
|
91
155
|
}
|
|
92
|
-
this.videoSplitter
|
|
156
|
+
this.videoSplitter
|
|
157
|
+
.send(encryptedPacket, {
|
|
93
158
|
port: videoPort,
|
|
94
|
-
address: targetAddress
|
|
95
|
-
})
|
|
159
|
+
address: targetAddress,
|
|
160
|
+
})
|
|
161
|
+
.catch(util_1.logError);
|
|
96
162
|
}));
|
|
97
163
|
const shouldTranscodeAudio = await this.libfdkAacInstalledPromise;
|
|
98
164
|
if (!shouldTranscodeAudio) {
|
|
99
165
|
return this.streamingSession.requestKeyFrame();
|
|
100
166
|
}
|
|
101
167
|
const transcodingPromise = this.streamingSession.startTranscoding({
|
|
102
|
-
input: [
|
|
103
|
-
'-vn'
|
|
104
|
-
],
|
|
168
|
+
input: ['-vn'],
|
|
105
169
|
audio: [
|
|
106
170
|
'-map',
|
|
107
171
|
'0:a',
|
|
108
|
-
...request.audio.codec ===
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
172
|
+
...(request.audio.codec === "OPUS" /* AudioStreamingCodecType.OPUS */
|
|
173
|
+
? [
|
|
174
|
+
// OPUS specific - it works, but audio is very choppy
|
|
175
|
+
'-acodec',
|
|
176
|
+
'libopus',
|
|
177
|
+
'-frame_duration',
|
|
178
|
+
request.audio.packet_time,
|
|
179
|
+
'-application',
|
|
180
|
+
'lowdelay',
|
|
181
|
+
]
|
|
182
|
+
: [
|
|
183
|
+
// AAC-eld specific
|
|
184
|
+
'-acodec',
|
|
185
|
+
'libfdk_aac',
|
|
186
|
+
'-profile:a',
|
|
187
|
+
'aac_eld',
|
|
188
|
+
]),
|
|
123
189
|
// Shared options
|
|
124
190
|
'-flags',
|
|
125
191
|
'+global_header',
|
|
@@ -137,68 +203,60 @@ class StreamingSessionWrapper {
|
|
|
137
203
|
this.audioSsrc,
|
|
138
204
|
'-f',
|
|
139
205
|
'rtp',
|
|
140
|
-
`rtp://127.0.0.1:${await this.repacketizeAudioSplitter
|
|
206
|
+
`rtp://127.0.0.1:${await this.repacketizeAudioSplitter
|
|
207
|
+
.portPromise}?pkt_size=188`,
|
|
141
208
|
],
|
|
142
209
|
video: false,
|
|
143
|
-
output: []
|
|
210
|
+
output: [],
|
|
144
211
|
});
|
|
145
212
|
let cameraSpeakerActive = false;
|
|
146
213
|
// used to decrypt return audio from HomeKit to Ring
|
|
147
214
|
const remoteAudioSrtpOptions = {
|
|
148
215
|
srtpKey: remoteAudioSrtpKey,
|
|
149
|
-
srtpSalt: remoteAudioSrtpSalt
|
|
150
|
-
}, audioSrtpSession = new
|
|
216
|
+
srtpSalt: remoteAudioSrtpSalt,
|
|
217
|
+
}, audioSrtpSession = new werift_1.SrtpSession(getSessionConfig(remoteAudioSrtpOptions)), returnAudioTranscodedSplitter = new camera_utils_1.RtpSplitter(({ message }) => {
|
|
151
218
|
if (!cameraSpeakerActive) {
|
|
152
219
|
cameraSpeakerActive = true;
|
|
153
220
|
this.streamingSession.activateCameraSpeaker();
|
|
154
221
|
}
|
|
155
222
|
// decrypt the message
|
|
156
223
|
try {
|
|
157
|
-
const rtp =
|
|
224
|
+
const rtp = werift_1.RtpPacket.deSerialize(message);
|
|
158
225
|
rtp.payload = audioSrtpSession.decrypt(rtp.payload);
|
|
159
226
|
// send to Ring - werift will handle encryption and other header params
|
|
160
227
|
this.streamingSession.sendAudioPacket(rtp);
|
|
161
|
-
}
|
|
162
|
-
|
|
228
|
+
}
|
|
229
|
+
catch (_) {
|
|
230
|
+
// deSerialize will sometimes fail, but the errors can be ignored
|
|
163
231
|
}
|
|
164
232
|
return null;
|
|
165
|
-
}), isRingUsingOpus = await this.streamingSession.isUsingOpus, returnAudioTranscoder = new
|
|
233
|
+
}), isRingUsingOpus = await this.streamingSession.isUsingOpus, returnAudioTranscoder = new camera_utils_1.ReturnAudioTranscoder({
|
|
166
234
|
prepareStreamRequest: this.prepareStreamRequest,
|
|
167
235
|
startStreamRequest: request,
|
|
168
236
|
incomingAudioOptions: {
|
|
169
237
|
ssrc: this.audioSsrc,
|
|
170
|
-
rtcpPort: 0
|
|
238
|
+
rtcpPort: 0, // we don't care about rtcp for incoming audio
|
|
171
239
|
},
|
|
172
240
|
outputArgs: [
|
|
173
241
|
'-acodec',
|
|
174
|
-
...isRingUsingOpus
|
|
175
|
-
'libopus',
|
|
176
|
-
'-ac',
|
|
177
|
-
2,
|
|
178
|
-
'-ar',
|
|
179
|
-
'48k'
|
|
180
|
-
] : [
|
|
181
|
-
'pcm_mulaw',
|
|
182
|
-
'-ac',
|
|
183
|
-
1,
|
|
184
|
-
'-ar',
|
|
185
|
-
'8k'
|
|
186
|
-
],
|
|
242
|
+
...(isRingUsingOpus
|
|
243
|
+
? ['libopus', '-ac', 2, '-ar', '48k']
|
|
244
|
+
: ['pcm_mulaw', '-ac', 1, '-ar', '8k']),
|
|
187
245
|
'-flags',
|
|
188
246
|
'+global_header',
|
|
189
247
|
'-f',
|
|
190
248
|
'rtp',
|
|
191
|
-
`rtp://127.0.0.1:${await returnAudioTranscodedSplitter.portPromise}
|
|
249
|
+
`rtp://127.0.0.1:${await returnAudioTranscodedSplitter.portPromise}`,
|
|
192
250
|
],
|
|
193
|
-
ffmpegPath: (0,
|
|
251
|
+
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
194
252
|
logger: {
|
|
195
|
-
info:
|
|
196
|
-
error:
|
|
253
|
+
info: util_1.logDebug,
|
|
254
|
+
error: util_1.logError,
|
|
197
255
|
},
|
|
198
256
|
logLabel: `Return Audio (${this.ringCamera.name})`,
|
|
199
|
-
returnAudioSplitter: this.audioSplitter
|
|
257
|
+
returnAudioSplitter: this.audioSplitter,
|
|
200
258
|
});
|
|
201
|
-
this.streamingSession.onCallEnded.pipe((0,
|
|
259
|
+
this.streamingSession.onCallEnded.pipe((0, operators_1.take)(1)).subscribe(() => {
|
|
202
260
|
returnAudioTranscoder.stop();
|
|
203
261
|
returnAudioTranscodedSplitter.close();
|
|
204
262
|
});
|
|
@@ -212,65 +270,62 @@ class StreamingSessionWrapper {
|
|
|
212
270
|
this.videoSplitter.close();
|
|
213
271
|
this.streamingSession.stop();
|
|
214
272
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
273
|
+
}
|
|
274
|
+
class CameraSource {
|
|
275
|
+
constructor(ringCamera, useOpus = false) {
|
|
218
276
|
this.ringCamera = ringCamera;
|
|
219
|
-
this.
|
|
220
|
-
this.
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
277
|
+
this.useOpus = useOpus;
|
|
278
|
+
this.controller = new hap_1.hap.CameraController({
|
|
279
|
+
cameraStreamCount: 10,
|
|
280
|
+
delegate: this,
|
|
281
|
+
streamingOptions: {
|
|
282
|
+
supportedCryptoSuites: [0 /* SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80 */],
|
|
283
|
+
video: {
|
|
284
|
+
resolutions: [
|
|
285
|
+
[1280, 720, 30],
|
|
286
|
+
[1024, 768, 30],
|
|
287
|
+
[640, 480, 30],
|
|
288
|
+
[640, 360, 30],
|
|
289
|
+
[480, 360, 30],
|
|
290
|
+
[480, 270, 30],
|
|
291
|
+
[320, 240, 30],
|
|
292
|
+
[320, 240, 15],
|
|
293
|
+
[320, 180, 30],
|
|
294
|
+
],
|
|
295
|
+
codec: {
|
|
296
|
+
profiles: [0 /* H264Profile.BASELINE */],
|
|
297
|
+
levels: [0 /* H264Level.LEVEL3_1 */],
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
audio: {
|
|
301
|
+
codecs: this.useOpus
|
|
302
|
+
? [
|
|
303
|
+
{
|
|
304
|
+
type: "OPUS" /* AudioStreamingCodecType.OPUS */,
|
|
305
|
+
// required by watch
|
|
306
|
+
samplerate: 8 /* AudioStreamingSamplerate.KHZ_8 */,
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
type: "OPUS" /* AudioStreamingCodecType.OPUS */,
|
|
310
|
+
samplerate: 16 /* AudioStreamingSamplerate.KHZ_16 */,
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
type: "OPUS" /* AudioStreamingCodecType.OPUS */,
|
|
314
|
+
samplerate: 24 /* AudioStreamingSamplerate.KHZ_24 */,
|
|
315
|
+
},
|
|
316
|
+
]
|
|
317
|
+
: [
|
|
318
|
+
{
|
|
319
|
+
type: "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */,
|
|
320
|
+
samplerate: 16 /* AudioStreamingSamplerate.KHZ_16 */,
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
249
325
|
});
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
streamingSession.stop();
|
|
253
|
-
}));
|
|
254
|
-
// Periodically send a blank RTCP packet to the HomeKit video port
|
|
255
|
-
// Without this, HomeKit assumes the stream is dead after 30 second and sends a stop request
|
|
256
|
-
streamingSession.addSubscriptions((0, _rxjs.interval)(500).subscribe(()=>{
|
|
257
|
-
const senderInfo = new _werift.RtcpSenderInfo({
|
|
258
|
-
ntpTimestamp: BigInt(0),
|
|
259
|
-
packetCount: 0,
|
|
260
|
-
octetCount: 0,
|
|
261
|
-
rtpTimestamp: 0
|
|
262
|
-
}), senderReport = new _werift.RtcpSrPacket({
|
|
263
|
-
ssrc: this.videoSsrc,
|
|
264
|
-
senderInfo: senderInfo
|
|
265
|
-
}), message = videoSrtcpSession.encrypt(senderReport.serialize());
|
|
266
|
-
this.videoSplitter.send(message, {
|
|
267
|
-
port: videoPort,
|
|
268
|
-
address: targetAddress
|
|
269
|
-
}).catch(_util.logError);
|
|
270
|
-
}));
|
|
326
|
+
this.sessions = {};
|
|
327
|
+
this.fn = 1;
|
|
271
328
|
}
|
|
272
|
-
}
|
|
273
|
-
class CameraSource {
|
|
274
329
|
async loadSnapshot(imageUuid) {
|
|
275
330
|
// cache a promise of the snapshot load
|
|
276
331
|
// This prevents multiple concurrent requests for snapshot from pilling up and creating lots of logs
|
|
@@ -280,37 +335,38 @@ class CameraSource {
|
|
|
280
335
|
this.previousLoadSnapshotPromise = this.loadAndCacheSnapshot(imageUuid);
|
|
281
336
|
try {
|
|
282
337
|
await this.previousLoadSnapshotPromise;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
|
|
338
|
+
}
|
|
339
|
+
catch (_) {
|
|
340
|
+
// ignore errors
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
286
343
|
// clear so another request can be made
|
|
287
344
|
this.previousLoadSnapshotPromise = undefined;
|
|
288
345
|
}
|
|
289
346
|
}
|
|
290
347
|
async loadAndCacheSnapshot(imageUuid) {
|
|
291
348
|
const start = Date.now();
|
|
292
|
-
(0,
|
|
349
|
+
(0, util_1.logDebug)(`Loading new snapshot into cache for ${this.ringCamera.name}${imageUuid ? ' by uuid' : ''}`);
|
|
293
350
|
try {
|
|
294
|
-
const previousSnapshot = this.cachedSnapshot, newSnapshot = await this.ringCamera.getSnapshot({
|
|
295
|
-
uuid: imageUuid
|
|
296
|
-
});
|
|
351
|
+
const previousSnapshot = this.cachedSnapshot, newSnapshot = await this.ringCamera.getSnapshot({ uuid: imageUuid });
|
|
297
352
|
this.cachedSnapshot = newSnapshot;
|
|
298
353
|
if (previousSnapshot !== newSnapshot) {
|
|
299
354
|
// Keep the snapshots in cache 2 minutes longer than their lifetime
|
|
300
355
|
// This allows users on LTE with wired camera to get snapshots each 60 second pull even though the cached snapshot is out of date
|
|
301
|
-
setTimeout(()=>{
|
|
356
|
+
setTimeout(() => {
|
|
302
357
|
if (this.cachedSnapshot === newSnapshot) {
|
|
303
358
|
this.cachedSnapshot = undefined;
|
|
304
359
|
}
|
|
305
360
|
}, this.ringCamera.snapshotLifeTime + 2 * 60 * 1000);
|
|
306
361
|
}
|
|
307
|
-
(0,
|
|
308
|
-
}
|
|
362
|
+
(0, util_1.logDebug)(`Snapshot cached for ${this.ringCamera.name}${imageUuid ? ' by uuid' : ''} (${getDurationSeconds(start)}s)`);
|
|
363
|
+
}
|
|
364
|
+
catch (e) {
|
|
309
365
|
this.cachedSnapshot = undefined;
|
|
310
|
-
(0,
|
|
366
|
+
(0, util_1.logDebug)(`Failed to cache snapshot for ${this.ringCamera.name} (${getDurationSeconds(start)}s), The camera currently reports that it is ${this.ringCamera.isOffline ? 'offline' : 'online'}`);
|
|
311
367
|
// log additioanl snapshot error message if one is present
|
|
312
368
|
if (e.message.includes('Snapshot')) {
|
|
313
|
-
(0,
|
|
369
|
+
(0, util_1.logDebug)(e.message);
|
|
314
370
|
}
|
|
315
371
|
}
|
|
316
372
|
}
|
|
@@ -321,9 +377,9 @@ class CameraSource {
|
|
|
321
377
|
if (this.ringCamera.snapshotsAreBlocked) {
|
|
322
378
|
return readFileAsync(snapshotsBlockedPath);
|
|
323
379
|
}
|
|
324
|
-
(0,
|
|
380
|
+
(0, util_1.logDebug)(`${this.cachedSnapshot ? 'Used cached snapshot' : 'No snapshot cached'} for ${this.ringCamera.name}`);
|
|
325
381
|
if (!this.ringCamera.hasSnapshotWithinLifetime) {
|
|
326
|
-
this.loadSnapshot().catch(
|
|
382
|
+
this.loadSnapshot().catch(util_1.logError);
|
|
327
383
|
}
|
|
328
384
|
// may or may not have a snapshot cached
|
|
329
385
|
return this.cachedSnapshot;
|
|
@@ -338,36 +394,38 @@ class CameraSource {
|
|
|
338
394
|
// Not currently resizing the image.
|
|
339
395
|
// HomeKit does a good job of resizing and doesn't seem to care if it's not right
|
|
340
396
|
callback(undefined, snapshot);
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
(0,
|
|
397
|
+
}
|
|
398
|
+
catch (e) {
|
|
399
|
+
(0, util_1.logError)(`Error fetching snapshot for ${this.ringCamera.name}`);
|
|
400
|
+
(0, util_1.logError)(e);
|
|
344
401
|
callback(e);
|
|
345
402
|
}
|
|
346
403
|
}
|
|
347
404
|
async prepareStream(request, callback) {
|
|
348
405
|
const start = Date.now();
|
|
349
|
-
(0,
|
|
406
|
+
(0, util_1.logInfo)(`Preparing Live Stream for ${this.ringCamera.name}`);
|
|
350
407
|
try {
|
|
351
408
|
const liveCall = await this.ringCamera.startLiveCall(), session = new StreamingSessionWrapper(liveCall, request, this.ringCamera, start);
|
|
352
409
|
this.sessions[request.sessionID] = session;
|
|
353
|
-
(0,
|
|
410
|
+
(0, util_1.logInfo)(`Stream Prepared for ${this.ringCamera.name} (${getDurationSeconds(start)}s)`);
|
|
354
411
|
callback(undefined, {
|
|
355
412
|
audio: {
|
|
356
413
|
port: await session.audioSplitter.portPromise,
|
|
357
414
|
ssrc: session.audioSsrc,
|
|
358
415
|
srtp_key: session.audioSrtp.srtpKey,
|
|
359
|
-
srtp_salt: session.audioSrtp.srtpSalt
|
|
416
|
+
srtp_salt: session.audioSrtp.srtpSalt,
|
|
360
417
|
},
|
|
361
418
|
video: {
|
|
362
419
|
port: await session.videoSplitter.portPromise,
|
|
363
420
|
ssrc: session.videoSsrc,
|
|
364
421
|
srtp_key: session.videoSrtp.srtpKey,
|
|
365
|
-
srtp_salt: session.videoSrtp.srtpSalt
|
|
366
|
-
}
|
|
422
|
+
srtp_salt: session.videoSrtp.srtpSalt,
|
|
423
|
+
},
|
|
367
424
|
});
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
(0,
|
|
425
|
+
}
|
|
426
|
+
catch (e) {
|
|
427
|
+
(0, util_1.logError)(`Failed to prepare stream for ${this.ringCamera.name} (${getDurationSeconds(start)}s)`);
|
|
428
|
+
(0, util_1.logError)(e);
|
|
371
429
|
callback(e);
|
|
372
430
|
}
|
|
373
431
|
}
|
|
@@ -378,108 +436,16 @@ class CameraSource {
|
|
|
378
436
|
return;
|
|
379
437
|
}
|
|
380
438
|
if (requestType === 'start') {
|
|
381
|
-
(0,
|
|
439
|
+
(0, util_1.logInfo)(`Activating stream for ${this.ringCamera.name} (${getDurationSeconds(session.start)}s)`);
|
|
382
440
|
await session.activate(request);
|
|
383
|
-
(0,
|
|
384
|
-
}
|
|
385
|
-
|
|
441
|
+
(0, util_1.logInfo)(`Streaming active for ${this.ringCamera.name} (${getDurationSeconds(session.start)}s)`);
|
|
442
|
+
}
|
|
443
|
+
else if (requestType === 'stop') {
|
|
444
|
+
(0, util_1.logInfo)(`Stopped Live Stream for ${this.ringCamera.name}`);
|
|
386
445
|
session.stop();
|
|
387
446
|
delete this.sessions[sessionID];
|
|
388
447
|
}
|
|
389
448
|
callback();
|
|
390
449
|
}
|
|
391
|
-
constructor(ringCamera, useOpus = false){
|
|
392
|
-
this.ringCamera = ringCamera;
|
|
393
|
-
this.useOpus = useOpus;
|
|
394
|
-
this.controller = new _hap.hap.CameraController({
|
|
395
|
-
cameraStreamCount: 10,
|
|
396
|
-
delegate: this,
|
|
397
|
-
streamingOptions: {
|
|
398
|
-
supportedCryptoSuites: [
|
|
399
|
-
_homebridge.SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80
|
|
400
|
-
],
|
|
401
|
-
video: {
|
|
402
|
-
resolutions: [
|
|
403
|
-
[
|
|
404
|
-
1280,
|
|
405
|
-
720,
|
|
406
|
-
30
|
|
407
|
-
],
|
|
408
|
-
[
|
|
409
|
-
1024,
|
|
410
|
-
768,
|
|
411
|
-
30
|
|
412
|
-
],
|
|
413
|
-
[
|
|
414
|
-
640,
|
|
415
|
-
480,
|
|
416
|
-
30
|
|
417
|
-
],
|
|
418
|
-
[
|
|
419
|
-
640,
|
|
420
|
-
360,
|
|
421
|
-
30
|
|
422
|
-
],
|
|
423
|
-
[
|
|
424
|
-
480,
|
|
425
|
-
360,
|
|
426
|
-
30
|
|
427
|
-
],
|
|
428
|
-
[
|
|
429
|
-
480,
|
|
430
|
-
270,
|
|
431
|
-
30
|
|
432
|
-
],
|
|
433
|
-
[
|
|
434
|
-
320,
|
|
435
|
-
240,
|
|
436
|
-
30
|
|
437
|
-
],
|
|
438
|
-
[
|
|
439
|
-
320,
|
|
440
|
-
240,
|
|
441
|
-
15
|
|
442
|
-
],
|
|
443
|
-
[
|
|
444
|
-
320,
|
|
445
|
-
180,
|
|
446
|
-
30
|
|
447
|
-
]
|
|
448
|
-
],
|
|
449
|
-
codec: {
|
|
450
|
-
profiles: [
|
|
451
|
-
_homebridge.H264Profile.BASELINE
|
|
452
|
-
],
|
|
453
|
-
levels: [
|
|
454
|
-
_homebridge.H264Level.LEVEL3_1
|
|
455
|
-
]
|
|
456
|
-
}
|
|
457
|
-
},
|
|
458
|
-
audio: {
|
|
459
|
-
codecs: this.useOpus ? [
|
|
460
|
-
{
|
|
461
|
-
type: _homebridge.AudioStreamingCodecType.OPUS,
|
|
462
|
-
// required by watch
|
|
463
|
-
samplerate: _homebridge.AudioStreamingSamplerate.KHZ_8
|
|
464
|
-
},
|
|
465
|
-
{
|
|
466
|
-
type: _homebridge.AudioStreamingCodecType.OPUS,
|
|
467
|
-
samplerate: _homebridge.AudioStreamingSamplerate.KHZ_16
|
|
468
|
-
},
|
|
469
|
-
{
|
|
470
|
-
type: _homebridge.AudioStreamingCodecType.OPUS,
|
|
471
|
-
samplerate: _homebridge.AudioStreamingSamplerate.KHZ_24
|
|
472
|
-
}
|
|
473
|
-
] : [
|
|
474
|
-
{
|
|
475
|
-
type: _homebridge.AudioStreamingCodecType.AAC_ELD,
|
|
476
|
-
samplerate: _homebridge.AudioStreamingSamplerate.KHZ_16
|
|
477
|
-
}
|
|
478
|
-
]
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
this.sessions = {};
|
|
483
|
-
this.fn = 1;
|
|
484
|
-
}
|
|
485
450
|
}
|
|
451
|
+
exports.CameraSource = CameraSource;
|