homebridge-ring 9.25.0-beta.1 → 10.0.0-beta.0
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 +25 -0
- package/lib/api/index.d.ts +0 -1
- package/lib/api/index.js +0 -1
- package/lib/api/live-call.d.ts +1 -5
- package/lib/api/live-call.js +32 -51
- package/lib/api/ring-camera.d.ts +2 -16
- package/lib/api/ring-camera.js +4 -86
- package/lib/api/ring-types.d.ts +1 -0
- package/lib/api/ring-types.js +2 -0
- package/lib/api/rtp-utils.d.ts +0 -29
- package/lib/api/rtp-utils.js +2 -55
- package/lib/homebridge/camera-source.js +43 -123
- package/lib/homebridge/ring-platform.js +2 -1
- package/package.json +2 -4
- 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,31 @@
|
|
|
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.0](https://github.com/dgreif/ring/compare/v9.25.0-beta.1...v10.0.0-beta.0) (2022-02-18)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### ⚠ BREAKING CHANGES
|
|
9
|
+
|
|
10
|
+
* `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.
|
|
11
|
+
* Node 10 is no longer officially supported. It may still work, but could break without warning in the future.
|
|
12
|
+
* **homebridge:** The minimum supported homebridge version for this plugin is now 1.4.0
|
|
13
|
+
* **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
|
|
14
|
+
* **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.
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* add floodlight pro ([f749d38](https://github.com/dgreif/ring/commit/f749d38e0528e631864bf20819692a466744e1f2)), closes [#889](https://github.com/dgreif/ring/issues/889)
|
|
19
|
+
* **homebridge:** stream opus audio without ffmpeg for transcoding ([ee142bc](https://github.com/dgreif/ring/commit/ee142bc0eee9dcfda41d162b2105535b8fdcbf9a))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* drop support for node 10 ([509d556](https://github.com/dgreif/ring/commit/509d55670b3dd4efee1be968618f6bc6b142ae2b))
|
|
25
|
+
* **homebridge:** force cameras to be recreated in HomeKit ([94a1520](https://github.com/dgreif/ring/commit/94a15204f4a9ccbe94968be8db05add6baa5fcfa))
|
|
26
|
+
* **homebridge:** increase minimum homebridge version to 1.4.0 ([d5be74a](https://github.com/dgreif/ring/commit/d5be74aae45a3e353bcaba4fc42fa25c6b2fa37e))
|
|
27
|
+
* remove sip calling in favor of live calls ([7aeae37](https://github.com/dgreif/ring/commit/7aeae37e6e93b68747e14983cea9c0a9229faec6))
|
|
28
|
+
* use Buffer.from ([2b28d90](https://github.com/dgreif/ring/commit/2b28d9028fc7f2c2e11b11cf3b27cc03f097eab2))
|
|
29
|
+
|
|
5
30
|
## [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
31
|
|
|
7
32
|
|
package/lib/api/index.d.ts
CHANGED
package/lib/api/index.js
CHANGED
package/lib/api/live-call.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare class LiveCall extends Subscribed {
|
|
|
16
16
|
private readonly onWsOpen;
|
|
17
17
|
private readonly onMessage;
|
|
18
18
|
private readonly pc;
|
|
19
|
-
readonly onCallAnswered: ReplaySubject<
|
|
19
|
+
readonly onCallAnswered: ReplaySubject<string>;
|
|
20
20
|
readonly onCallEnded: ReplaySubject<void>;
|
|
21
21
|
private readonly audioSplitter;
|
|
22
22
|
private readonly videoSplitter;
|
|
@@ -25,10 +25,6 @@ export declare class LiveCall extends Subscribed {
|
|
|
25
25
|
constructor(sessionId: string, camera: RingCamera);
|
|
26
26
|
private handleMessage;
|
|
27
27
|
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
28
|
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
33
29
|
activated: boolean;
|
|
34
30
|
activate(): Promise<void>;
|
package/lib/api/live-call.js
CHANGED
|
@@ -62,7 +62,7 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
62
62
|
case 'sdp':
|
|
63
63
|
const answer = yield this.pc.createAnswer(message);
|
|
64
64
|
this.sendMessage(Object.assign({ method: 'sdp' }, answer));
|
|
65
|
-
this.onCallAnswered.next();
|
|
65
|
+
this.onCallAnswered.next(message.sdp);
|
|
66
66
|
return;
|
|
67
67
|
case 'ice':
|
|
68
68
|
yield this.pc.addIceCandidate({
|
|
@@ -79,57 +79,22 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
79
79
|
return ports[0];
|
|
80
80
|
});
|
|
81
81
|
}
|
|
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
82
|
startTranscoding(ffmpegOptions) {
|
|
131
83
|
return __awaiter(this, void 0, void 0, function* () {
|
|
132
|
-
const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false,
|
|
84
|
+
const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, ffmpegInputArguments = [
|
|
85
|
+
'-hide_banner',
|
|
86
|
+
'-protocol_whitelist',
|
|
87
|
+
'pipe,udp,rtp,file,crypto',
|
|
88
|
+
'-acodec',
|
|
89
|
+
'libopus',
|
|
90
|
+
'-f',
|
|
91
|
+
'sdp',
|
|
92
|
+
...(ffmpegOptions.input || []),
|
|
93
|
+
'-i',
|
|
94
|
+
'pipe:',
|
|
95
|
+
], inputSdp = (yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered))
|
|
96
|
+
.replace(/m=audio \d+/, `m=audio ${audioPort}`)
|
|
97
|
+
.replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
|
|
133
98
|
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
134
99
|
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
135
100
|
: []), ...(ffmpegOptions.output || [])),
|
|
@@ -141,8 +106,24 @@ class LiveCall extends subscribed_1.Subscribed {
|
|
|
141
106
|
info: util_1.logDebug,
|
|
142
107
|
},
|
|
143
108
|
});
|
|
109
|
+
this.addSubscriptions(this.onAudioRtp
|
|
110
|
+
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
111
|
+
return this.audioSplitter.send(rtp.serialize(), {
|
|
112
|
+
port: audioPort,
|
|
113
|
+
});
|
|
114
|
+
}))
|
|
115
|
+
.subscribe());
|
|
116
|
+
if (transcodeVideoStream) {
|
|
117
|
+
this.addSubscriptions(this.onVideoRtp
|
|
118
|
+
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
119
|
+
return this.videoSplitter.send(rtp.serialize(), {
|
|
120
|
+
port: videoPort,
|
|
121
|
+
});
|
|
122
|
+
}))
|
|
123
|
+
.subscribe());
|
|
124
|
+
}
|
|
144
125
|
this.onCallEnded.subscribe(() => ff.stop());
|
|
145
|
-
ff.writeStdin(
|
|
126
|
+
ff.writeStdin(inputSdp);
|
|
146
127
|
// Activate the stream now that ffmpeg is ready to receive
|
|
147
128
|
yield this.activate();
|
|
148
129
|
});
|
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;
|
|
@@ -56,10 +53,6 @@ export declare class RingCamera extends Subscribed {
|
|
|
56
53
|
setInHomeDoorbell(enable: boolean): Promise<boolean>;
|
|
57
54
|
getHealth(): Promise<CameraHealth>;
|
|
58
55
|
startLiveCall(): Promise<LiveCall>;
|
|
59
|
-
startVideoOnDemand(): Promise<("" | ActiveDing) & import("./rest-client").ExtendedResponse>;
|
|
60
|
-
private pollForActiveDing;
|
|
61
|
-
private expiredDingIds;
|
|
62
|
-
getSipConnectionDetails(): Promise<ActiveDing>;
|
|
63
56
|
private removeDingById;
|
|
64
57
|
processActiveDing(ding: ActiveDing): void;
|
|
65
58
|
getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
|
|
@@ -92,15 +85,8 @@ export declare class RingCamera extends Subscribed {
|
|
|
92
85
|
maxWaitMs?: number;
|
|
93
86
|
force?: boolean;
|
|
94
87
|
}): 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
88
|
recordToFile(outputPath: string, duration?: number): Promise<void>;
|
|
103
|
-
streamVideo(ffmpegOptions: FfmpegOptions): Promise<
|
|
89
|
+
streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall>;
|
|
104
90
|
subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
105
91
|
unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
106
92
|
subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
package/lib/api/ring-camera.js
CHANGED
|
@@ -14,9 +14,7 @@ const ring_types_1 = require("./ring-types");
|
|
|
14
14
|
const rest_client_1 = require("./rest-client");
|
|
15
15
|
const rxjs_1 = require("rxjs");
|
|
16
16
|
const operators_1 = require("rxjs/operators");
|
|
17
|
-
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
18
17
|
const util_1 = require("./util");
|
|
19
|
-
const sip_session_1 = require("./sip-session");
|
|
20
18
|
const subscribed_1 = require("./subscribed");
|
|
21
19
|
const live_call_1 = require("./live-call");
|
|
22
20
|
const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
|
|
@@ -82,7 +80,6 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
82
80
|
this.onMotionDetected = this.onActiveDings.pipe((0, operators_1.map)((dings) => dings.some((ding) => ding.motion || ding.kind === 'motion')), (0, operators_1.distinctUntilChanged)(), (0, operators_1.publishReplay)(1), (0, operators_1.refCount)());
|
|
83
81
|
this.onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.mapTo)(null), // no value needed, event is what matters
|
|
84
82
|
(0, operators_1.share)());
|
|
85
|
-
this.expiredDingIds = [];
|
|
86
83
|
this.lastSnapshotTimestamp = 0;
|
|
87
84
|
this.lastSnapshotTimestampLocal = 0;
|
|
88
85
|
this.fetchingSnapshot = false;
|
|
@@ -251,42 +248,6 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
251
248
|
return new live_call_1.LiveCall(liveCall.data.session_id, this);
|
|
252
249
|
});
|
|
253
250
|
}
|
|
254
|
-
startVideoOnDemand() {
|
|
255
|
-
return this.restClient
|
|
256
|
-
.request({
|
|
257
|
-
method: 'POST',
|
|
258
|
-
url: this.doorbotUrl('live_view'), // Ring app uses vod for battery cams, but doesn't appear to be necessary
|
|
259
|
-
})
|
|
260
|
-
.catch((e) => {
|
|
261
|
-
var _a;
|
|
262
|
-
if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.statusCode) === 403) {
|
|
263
|
-
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.`;
|
|
264
|
-
(0, util_1.logError)(errorMessage);
|
|
265
|
-
throw new Error(errorMessage);
|
|
266
|
-
}
|
|
267
|
-
throw e;
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
pollForActiveDing() {
|
|
271
|
-
// try every second until a new ding is received
|
|
272
|
-
this.addSubscriptions((0, rxjs_1.interval)(1000)
|
|
273
|
-
.pipe((0, operators_1.takeUntil)(this.onNewDing))
|
|
274
|
-
.subscribe(() => {
|
|
275
|
-
this.onRequestActiveDings.next(null);
|
|
276
|
-
}));
|
|
277
|
-
}
|
|
278
|
-
getSipConnectionDetails() {
|
|
279
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
280
|
-
const vodPromise = (0, rxjs_1.firstValueFrom)(this.onNewDing), videoOnDemandDing = yield this.startVideoOnDemand();
|
|
281
|
-
if (videoOnDemandDing && 'sip_from' in videoOnDemandDing) {
|
|
282
|
-
// wired cams return a ding from live_view so we don't need to wait
|
|
283
|
-
return videoOnDemandDing;
|
|
284
|
-
}
|
|
285
|
-
// battery cams return '' from live_view so we need to request active dings and wait
|
|
286
|
-
this.pollForActiveDing();
|
|
287
|
-
return vodPromise;
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
251
|
removeDingById(idToRemove) {
|
|
291
252
|
const allActiveDings = this.activeDings, otherDings = allActiveDings.filter((ding) => ding.id_str !== idToRemove);
|
|
292
253
|
this.onActiveDings.next(otherDings);
|
|
@@ -297,7 +258,6 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
297
258
|
this.onNewDing.next(ding);
|
|
298
259
|
setTimeout(() => {
|
|
299
260
|
this.removeDingById(ding.id_str);
|
|
300
|
-
this.expiredDingIds = this.expiredDingIds.filter((id) => id !== dingId);
|
|
301
261
|
}, 65 * 1000); // dings last ~1 minute
|
|
302
262
|
}
|
|
303
263
|
getEvents(options = {}) {
|
|
@@ -418,49 +378,9 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
418
378
|
return response;
|
|
419
379
|
});
|
|
420
380
|
}
|
|
421
|
-
getSipOptions() {
|
|
422
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
423
|
-
const activeDings = this.onActiveDings.getValue(), existingDing = activeDings
|
|
424
|
-
.filter((ding) => !this.expiredDingIds.includes(ding.id_str))
|
|
425
|
-
.slice()
|
|
426
|
-
.reverse()[0], ding = existingDing || (yield this.getSipConnectionDetails());
|
|
427
|
-
return {
|
|
428
|
-
to: ding.sip_to,
|
|
429
|
-
from: ding.sip_from,
|
|
430
|
-
dingId: ding.id_str,
|
|
431
|
-
localIp: yield (0, camera_utils_1.getDefaultIpAddress)(),
|
|
432
|
-
};
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
getUpdatedSipOptions(expiredDingId) {
|
|
436
|
-
// Got a 480 from sip session, which means it's no longer active
|
|
437
|
-
this.expiredDingIds.push(expiredDingId);
|
|
438
|
-
return this.getSipOptions();
|
|
439
|
-
}
|
|
440
|
-
createSipSession(options = {}) {
|
|
441
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
442
|
-
const audioSplitter = new camera_utils_1.RtpSplitter(), audioRtcpSplitter = new camera_utils_1.RtpSplitter(), videoSplitter = new camera_utils_1.RtpSplitter(), videoRtcpSplitter = new camera_utils_1.RtpSplitter(), [sipOptions, ffmpegIsInstalled, audioPort, audioRtcpPort, videoPort, videoRtcpPort, [tlsPort],] = yield Promise.all([
|
|
443
|
-
this.getSipOptions(),
|
|
444
|
-
options.skipFfmpegCheck ? Promise.resolve(true) : (0, camera_utils_1.isFfmpegInstalled)(),
|
|
445
|
-
audioSplitter.portPromise,
|
|
446
|
-
audioRtcpSplitter.portPromise,
|
|
447
|
-
videoSplitter.portPromise,
|
|
448
|
-
videoRtcpSplitter.portPromise,
|
|
449
|
-
(0, camera_utils_1.reservePorts)({ type: 'tcp' }),
|
|
450
|
-
]), rtpOptions = {
|
|
451
|
-
audio: Object.assign({ port: audioPort, rtcpPort: audioRtcpPort }, (options.audio || (0, camera_utils_1.generateSrtpOptions)())),
|
|
452
|
-
video: Object.assign({ port: videoPort, rtcpPort: videoRtcpPort }, (options.video || (0, camera_utils_1.generateSrtpOptions)())),
|
|
453
|
-
};
|
|
454
|
-
if (!ffmpegIsInstalled) {
|
|
455
|
-
throw new Error('Ffmpeg is not installed. See https://github.com/dgreif/ring/wiki/FFmpeg for directions.');
|
|
456
|
-
}
|
|
457
|
-
return new sip_session_1.SipSession(sipOptions, rtpOptions, audioSplitter, audioRtcpSplitter, videoSplitter, videoRtcpSplitter, tlsPort, this);
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
381
|
recordToFile(outputPath, duration = 30) {
|
|
461
382
|
return __awaiter(this, void 0, void 0, function* () {
|
|
462
|
-
const liveCall = yield this.
|
|
463
|
-
yield liveCall.startTranscoding({
|
|
383
|
+
const liveCall = yield this.streamVideo({
|
|
464
384
|
output: ['-t', duration.toString(), outputPath],
|
|
465
385
|
});
|
|
466
386
|
yield (0, rxjs_1.firstValueFrom)(liveCall.onCallEnded);
|
|
@@ -468,9 +388,9 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
468
388
|
}
|
|
469
389
|
streamVideo(ffmpegOptions) {
|
|
470
390
|
return __awaiter(this, void 0, void 0, function* () {
|
|
471
|
-
const
|
|
472
|
-
yield
|
|
473
|
-
return
|
|
391
|
+
const liveCall = yield this.startLiveCall();
|
|
392
|
+
yield liveCall.startTranscoding(ffmpegOptions);
|
|
393
|
+
return liveCall;
|
|
474
394
|
});
|
|
475
395
|
}
|
|
476
396
|
subscribeToDingEvents() {
|
|
@@ -502,5 +422,3 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
502
422
|
}
|
|
503
423
|
}
|
|
504
424
|
exports.RingCamera = RingCamera;
|
|
505
|
-
// SOMEDAY: extract image from video file?
|
|
506
|
-
// ffmpeg -i input.mp4 -r 1 -f image2 image-%2d.png
|
package/lib/api/ring-types.d.ts
CHANGED
|
@@ -82,6 +82,7 @@ export declare enum RingCameraKind {
|
|
|
82
82
|
stickup_cam_v4 = "stickup_cam_v4",
|
|
83
83
|
floodlight_v1 = "floodlight_v1",
|
|
84
84
|
floodlight_v2 = "floodlight_v2",
|
|
85
|
+
floodlight_pro = "floodlight_pro",
|
|
85
86
|
cocoa_camera = "cocoa_camera",
|
|
86
87
|
cocoa_doorbell = "cocoa_doorbell",
|
|
87
88
|
stickup_cam_mini = "stickup_cam_mini"
|
package/lib/api/ring-types.js
CHANGED
|
@@ -90,6 +90,7 @@ var RingCameraKind;
|
|
|
90
90
|
RingCameraKind["stickup_cam_v4"] = "stickup_cam_v4";
|
|
91
91
|
RingCameraKind["floodlight_v1"] = "floodlight_v1";
|
|
92
92
|
RingCameraKind["floodlight_v2"] = "floodlight_v2";
|
|
93
|
+
RingCameraKind["floodlight_pro"] = "floodlight_pro";
|
|
93
94
|
RingCameraKind["cocoa_camera"] = "cocoa_camera";
|
|
94
95
|
RingCameraKind["cocoa_doorbell"] = "cocoa_doorbell";
|
|
95
96
|
RingCameraKind["stickup_cam_mini"] = "stickup_cam_mini";
|
|
@@ -123,6 +124,7 @@ exports.RingCameraModel = {
|
|
|
123
124
|
stickup_cam_v4: 'Spotlight Cam',
|
|
124
125
|
floodlight_v1: 'Floodlight Cam',
|
|
125
126
|
floodlight_v2: 'Floodlight Cam',
|
|
127
|
+
floodlight_pro: 'Floodlight Pro',
|
|
126
128
|
cocoa_camera: 'Stick Up Cam',
|
|
127
129
|
cocoa_doorbell: 'Doorbell Gen 2',
|
|
128
130
|
stickup_cam_mini: 'Indoor Cam',
|
package/lib/api/rtp-utils.d.ts
CHANGED
|
@@ -1,33 +1,4 @@
|
|
|
1
|
-
/// <reference types="node" />
|
|
2
|
-
import { RtpSplitter, SrtpOptions } from '@homebridge/camera-utils';
|
|
3
1
|
import { DingKind } from './ring-types';
|
|
4
|
-
export interface RtpStreamOptions extends SrtpOptions {
|
|
5
|
-
port: number;
|
|
6
|
-
rtcpPort: number;
|
|
7
|
-
}
|
|
8
|
-
export interface RtpOptions {
|
|
9
|
-
audio: RtpStreamOptions;
|
|
10
|
-
video: RtpStreamOptions;
|
|
11
|
-
}
|
|
12
|
-
export interface RtpStreamDescription extends RtpStreamOptions {
|
|
13
|
-
ssrc?: number;
|
|
14
|
-
iceUFrag?: string;
|
|
15
|
-
icePwd?: string;
|
|
16
|
-
}
|
|
17
|
-
export interface RtpDescription {
|
|
18
|
-
address: string;
|
|
19
|
-
audio: RtpStreamDescription;
|
|
20
|
-
video: RtpStreamDescription;
|
|
21
|
-
}
|
|
22
|
-
export declare function isStunMessage(message: Buffer): boolean;
|
|
23
|
-
export declare function sendStunBindingRequest({ rtpDescription, rtpSplitter, rtcpSplitter, localUfrag, type, }: {
|
|
24
|
-
rtpSplitter: RtpSplitter;
|
|
25
|
-
rtcpSplitter: RtpSplitter;
|
|
26
|
-
rtpDescription: RtpDescription;
|
|
27
|
-
localUfrag?: string;
|
|
28
|
-
type: 'video' | 'audio';
|
|
29
|
-
}): void;
|
|
30
|
-
export declare function createStunResponder(rtpSplitter: RtpSplitter): void;
|
|
31
2
|
export interface LiveCallSession {
|
|
32
3
|
alexa_port: 18443;
|
|
33
4
|
app_session_token: string;
|
package/lib/api/rtp-utils.js
CHANGED
|
@@ -1,61 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.parseLiveCallSession =
|
|
4
|
-
const util_1 = require("./util");
|
|
5
|
-
const stun = require('stun');
|
|
6
|
-
const stunMagicCookie = 0x2112a442; // https://tools.ietf.org/html/rfc5389#section-6
|
|
7
|
-
function isStunMessage(message) {
|
|
8
|
-
return message.length > 8 && message.readInt32BE(4) === stunMagicCookie;
|
|
9
|
-
}
|
|
10
|
-
exports.isStunMessage = isStunMessage;
|
|
11
|
-
function sendStunBindingRequest({ rtpDescription, rtpSplitter, rtcpSplitter, localUfrag, type, }) {
|
|
12
|
-
const message = stun.createMessage(1), remoteDescription = rtpDescription[type], { address } = rtpDescription, { iceUFrag, icePwd, port, rtcpPort } = remoteDescription;
|
|
13
|
-
if (iceUFrag && icePwd && localUfrag) {
|
|
14
|
-
// Full ICE supported. Send as formal stun request
|
|
15
|
-
message.addUsername(iceUFrag + ':' + localUfrag);
|
|
16
|
-
message.addMessageIntegrity(icePwd);
|
|
17
|
-
stun
|
|
18
|
-
.request(`${address}:${port}`, {
|
|
19
|
-
socket: rtpSplitter.socket,
|
|
20
|
-
message,
|
|
21
|
-
})
|
|
22
|
-
.then(() => (0, util_1.logDebug)(`${type} stun complete`))
|
|
23
|
-
.catch((e) => {
|
|
24
|
-
(0, util_1.logError)(`${type} stun error`);
|
|
25
|
-
(0, util_1.logError)(e);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
// ICE not supported. Fire and forget the stun request for RTP and RTCP
|
|
30
|
-
const encodedMessage = stun.encode(message);
|
|
31
|
-
rtpSplitter.send(encodedMessage, { address, port }).catch(util_1.logError);
|
|
32
|
-
rtcpSplitter
|
|
33
|
-
.send(encodedMessage, { address, port: rtcpPort })
|
|
34
|
-
.catch(util_1.logError);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
exports.sendStunBindingRequest = sendStunBindingRequest;
|
|
38
|
-
function createStunResponder(rtpSplitter) {
|
|
39
|
-
return rtpSplitter.addMessageHandler(({ message, info }) => {
|
|
40
|
-
if (!isStunMessage(message)) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
try {
|
|
44
|
-
const decodedMessage = stun.decode(message), response = stun.createMessage(stun.constants.STUN_BINDING_RESPONSE, decodedMessage.transactionId);
|
|
45
|
-
response.addXorAddress(info.address, info.port);
|
|
46
|
-
rtpSplitter.send(stun.encode(response), info).catch(util_1.logError);
|
|
47
|
-
}
|
|
48
|
-
catch (e) {
|
|
49
|
-
(0, util_1.logDebug)('Failed to Decode STUN Message');
|
|
50
|
-
(0, util_1.logDebug)(message.toString('hex'));
|
|
51
|
-
(0, util_1.logDebug)(e);
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
exports.createStunResponder = createStunResponder;
|
|
3
|
+
exports.parseLiveCallSession = void 0;
|
|
57
4
|
function parseLiveCallSession(sessionId) {
|
|
58
|
-
const encodedSession = sessionId.split('.')[1], buff =
|
|
5
|
+
const encodedSession = sessionId.split('.')[1], buff = Buffer.from(encodedSession, 'base64'), text = buff.toString('ascii');
|
|
59
6
|
return JSON.parse(text);
|
|
60
7
|
}
|
|
61
8
|
exports.parseLiveCallSession = parseLiveCallSession;
|