ring-client-api 9.25.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 +44 -0
- package/lib/api/index.d.ts +0 -1
- package/lib/api/index.js +0 -1
- package/lib/api/live-call.d.ts +37 -0
- package/lib/api/live-call.js +190 -0
- package/lib/api/peer-connection.d.ts +17 -0
- package/lib/api/peer-connection.js +96 -0
- package/lib/api/ring-camera.d.ts +3 -15
- package/lib/api/ring-camera.js +22 -83
- package/lib/api/ring-types.d.ts +16 -0
- package/lib/api/ring-types.js +2 -0
- package/lib/api/rtp-utils.d.ts +24 -27
- package/lib/api/rtp-utils.js +5 -53
- package/package.json +19 -15
- 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,50 @@
|
|
|
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
|
+
|
|
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)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### ⚠ BREAKING CHANGES
|
|
21
|
+
|
|
22
|
+
* `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.
|
|
23
|
+
* Node 10 is no longer officially supported. It may still work, but could break without warning in the future.
|
|
24
|
+
* **homebridge:** The minimum supported homebridge version for this plugin is now 1.4.0
|
|
25
|
+
* **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
|
|
26
|
+
* **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.
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
* add floodlight pro ([f749d38](https://github.com/dgreif/ring/commit/f749d38e0528e631864bf20819692a466744e1f2)), closes [#889](https://github.com/dgreif/ring/issues/889)
|
|
31
|
+
* **homebridge:** stream opus audio without ffmpeg for transcoding ([ee142bc](https://github.com/dgreif/ring/commit/ee142bc0eee9dcfda41d162b2105535b8fdcbf9a))
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
### Bug Fixes
|
|
35
|
+
|
|
36
|
+
* drop support for node 10 ([509d556](https://github.com/dgreif/ring/commit/509d55670b3dd4efee1be968618f6bc6b142ae2b))
|
|
37
|
+
* **homebridge:** force cameras to be recreated in HomeKit ([94a1520](https://github.com/dgreif/ring/commit/94a15204f4a9ccbe94968be8db05add6baa5fcfa))
|
|
38
|
+
* **homebridge:** increase minimum homebridge version to 1.4.0 ([d5be74a](https://github.com/dgreif/ring/commit/d5be74aae45a3e353bcaba4fc42fa25c6b2fa37e))
|
|
39
|
+
* remove sip calling in favor of live calls ([7aeae37](https://github.com/dgreif/ring/commit/7aeae37e6e93b68747e14983cea9c0a9229faec6))
|
|
40
|
+
* use Buffer.from ([2b28d90](https://github.com/dgreif/ring/commit/2b28d9028fc7f2c2e11b11cf3b27cc03f097eab2))
|
|
41
|
+
|
|
42
|
+
## [9.25.0-beta.1](https://github.com/dgreif/ring/compare/v9.25.0-beta.0...v9.25.0-beta.1) (2022-02-14)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
### Features
|
|
46
|
+
|
|
47
|
+
* use new WebRTC endpoint for live calls ([6e7779f](https://github.com/dgreif/ring/commit/6e7779feef9789583921fc44bab32c099f3026d2))
|
|
48
|
+
|
|
5
49
|
## [9.25.0-beta.0](https://github.com/dgreif/ring/compare/v9.24.0...v9.25.0-beta.0) (2022-01-29)
|
|
6
50
|
|
|
7
51
|
|
package/lib/api/index.d.ts
CHANGED
package/lib/api/index.js
CHANGED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ReplaySubject } from 'rxjs';
|
|
2
|
+
import { RingCamera } from './ring-camera';
|
|
3
|
+
import { Subscribed } from './subscribed';
|
|
4
|
+
import { RtpPacket } from '@koush/werift';
|
|
5
|
+
declare type SpawnInput = string | number;
|
|
6
|
+
export interface FfmpegOptions {
|
|
7
|
+
input?: SpawnInput[];
|
|
8
|
+
video?: SpawnInput[] | false;
|
|
9
|
+
audio?: SpawnInput[];
|
|
10
|
+
output: SpawnInput[];
|
|
11
|
+
}
|
|
12
|
+
export declare class LiveCall extends Subscribed {
|
|
13
|
+
private sessionId;
|
|
14
|
+
private camera;
|
|
15
|
+
private readonly ws;
|
|
16
|
+
private readonly onWsOpen;
|
|
17
|
+
private readonly onMessage;
|
|
18
|
+
private readonly pc;
|
|
19
|
+
readonly onCallAnswered: ReplaySubject<string>;
|
|
20
|
+
readonly onCallEnded: ReplaySubject<void>;
|
|
21
|
+
private readonly audioSplitter;
|
|
22
|
+
private readonly videoSplitter;
|
|
23
|
+
readonly onVideoRtp: import("rxjs").Subject<RtpPacket>;
|
|
24
|
+
readonly onAudioRtp: import("rxjs").Subject<RtpPacket>;
|
|
25
|
+
constructor(sessionId: string, camera: RingCamera);
|
|
26
|
+
private handleMessage;
|
|
27
|
+
reservePort(bufferPorts?: number): Promise<number>;
|
|
28
|
+
startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
|
|
29
|
+
activated: boolean;
|
|
30
|
+
activate(): Promise<void>;
|
|
31
|
+
activateCameraSpeaker(): Promise<void>;
|
|
32
|
+
private sendMessage;
|
|
33
|
+
private callEnded;
|
|
34
|
+
stop(): void;
|
|
35
|
+
sendAudioPacket(rtp: RtpPacket): void;
|
|
36
|
+
}
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.LiveCall = void 0;
|
|
13
|
+
const rtp_utils_1 = require("./rtp-utils");
|
|
14
|
+
const ws_1 = require("ws");
|
|
15
|
+
const rxjs_1 = require("rxjs");
|
|
16
|
+
const peer_connection_1 = require("./peer-connection");
|
|
17
|
+
const util_1 = require("./util");
|
|
18
|
+
const operators_1 = require("rxjs/operators");
|
|
19
|
+
const subscribed_1 = require("./subscribed");
|
|
20
|
+
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
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
|
+
}
|
|
30
|
+
class LiveCall extends subscribed_1.Subscribed {
|
|
31
|
+
constructor(sessionId, camera) {
|
|
32
|
+
super();
|
|
33
|
+
this.sessionId = sessionId;
|
|
34
|
+
this.camera = camera;
|
|
35
|
+
this.onCallAnswered = new rxjs_1.ReplaySubject(1);
|
|
36
|
+
this.onCallEnded = new rxjs_1.ReplaySubject(1);
|
|
37
|
+
this.audioSplitter = new camera_utils_1.RtpSplitter();
|
|
38
|
+
this.videoSplitter = new camera_utils_1.RtpSplitter();
|
|
39
|
+
this.activated = false;
|
|
40
|
+
const liveCallSession = (0, rtp_utils_1.parseLiveCallSession)(sessionId);
|
|
41
|
+
this.pc = new peer_connection_1.PeerConnection();
|
|
42
|
+
this.ws = new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
|
|
43
|
+
headers: {
|
|
44
|
+
API_VERSION: '3.1',
|
|
45
|
+
API_TOKEN: sessionId,
|
|
46
|
+
CLIENT_INFO: 'Ring/3.48.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
this.onAudioRtp = this.pc.onAudioRtp;
|
|
50
|
+
this.onVideoRtp = this.pc.onVideoRtp;
|
|
51
|
+
this.onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message');
|
|
52
|
+
this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
|
|
53
|
+
const onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
|
|
54
|
+
this.addSubscriptions(this.onMessage
|
|
55
|
+
.pipe((0, operators_1.concatMap)((message) => {
|
|
56
|
+
return this.handleMessage(JSON.parse(message.data));
|
|
57
|
+
}))
|
|
58
|
+
.subscribe(), this.onWsOpen.subscribe(() => {
|
|
59
|
+
(0, util_1.logDebug)(`WebSocket connected for ${this.camera.name}`);
|
|
60
|
+
}), onError.subscribe((e) => {
|
|
61
|
+
(0, util_1.logError)(e);
|
|
62
|
+
this.callEnded();
|
|
63
|
+
}), onClose.subscribe(() => {
|
|
64
|
+
this.callEnded();
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
handleMessage(message) {
|
|
68
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
69
|
+
switch (message.method) {
|
|
70
|
+
case 'sdp':
|
|
71
|
+
const answer = yield this.pc.createAnswer(message);
|
|
72
|
+
this.sendMessage(Object.assign({ method: 'sdp' }, answer));
|
|
73
|
+
this.onCallAnswered.next(message.sdp);
|
|
74
|
+
return;
|
|
75
|
+
case 'ice':
|
|
76
|
+
yield this.pc.addIceCandidate({
|
|
77
|
+
candidate: message.ice,
|
|
78
|
+
sdpMLineIndex: message.mlineindex,
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
reservePort(bufferPorts = 0) {
|
|
85
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
86
|
+
const ports = yield (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
|
|
87
|
+
return ports[0];
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
startTranscoding(ffmpegOptions) {
|
|
91
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
92
|
+
const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, ffmpegInputArguments = [
|
|
93
|
+
'-hide_banner',
|
|
94
|
+
'-protocol_whitelist',
|
|
95
|
+
'pipe,udp,rtp,file,crypto',
|
|
96
|
+
'-acodec',
|
|
97
|
+
'libopus',
|
|
98
|
+
'-f',
|
|
99
|
+
'sdp',
|
|
100
|
+
...(ffmpegOptions.input || []),
|
|
101
|
+
'-i',
|
|
102
|
+
'pipe:',
|
|
103
|
+
], ringSdp = yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered), inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
|
|
104
|
+
.replace(/m=audio \d+/, `m=audio ${audioPort}`)
|
|
105
|
+
.replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
|
|
106
|
+
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
107
|
+
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
108
|
+
: []), ...(ffmpegOptions.output || [])),
|
|
109
|
+
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
110
|
+
exitCallback: () => this.callEnded(),
|
|
111
|
+
logLabel: `From Ring (${this.camera.name})`,
|
|
112
|
+
logger: {
|
|
113
|
+
error: util_1.logError,
|
|
114
|
+
info: util_1.logDebug,
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
this.addSubscriptions(this.onAudioRtp
|
|
118
|
+
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
119
|
+
return this.audioSplitter.send(rtp.serialize(), {
|
|
120
|
+
port: audioPort,
|
|
121
|
+
});
|
|
122
|
+
}))
|
|
123
|
+
.subscribe());
|
|
124
|
+
if (transcodeVideoStream) {
|
|
125
|
+
this.addSubscriptions(this.onVideoRtp
|
|
126
|
+
.pipe((0, operators_1.concatMap)((rtp) => {
|
|
127
|
+
return this.videoSplitter.send(rtp.serialize(), {
|
|
128
|
+
port: videoPort,
|
|
129
|
+
});
|
|
130
|
+
}))
|
|
131
|
+
.subscribe());
|
|
132
|
+
}
|
|
133
|
+
this.onCallEnded.subscribe(() => ff.stop());
|
|
134
|
+
ff.writeStdin(inputSdp);
|
|
135
|
+
// Activate the stream now that ffmpeg is ready to receive
|
|
136
|
+
yield this.activate();
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
activate() {
|
|
140
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
141
|
+
if (this.activated) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.activated = true;
|
|
145
|
+
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
146
|
+
this.sendMessage({ method: 'activate_session' });
|
|
147
|
+
this.sendMessage({
|
|
148
|
+
video_enabled: true,
|
|
149
|
+
audio_enabled: true,
|
|
150
|
+
method: 'stream_options',
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
activateCameraSpeaker() {
|
|
155
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
156
|
+
yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
|
|
157
|
+
this.sendMessage({
|
|
158
|
+
stealth_mode: false,
|
|
159
|
+
method: 'camera_options',
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
sendMessage(message) {
|
|
164
|
+
this.ws.send(JSON.stringify(message));
|
|
165
|
+
}
|
|
166
|
+
callEnded() {
|
|
167
|
+
try {
|
|
168
|
+
this.sendMessage({
|
|
169
|
+
reason: { code: 0, text: '' },
|
|
170
|
+
method: 'close',
|
|
171
|
+
});
|
|
172
|
+
this.ws.close();
|
|
173
|
+
}
|
|
174
|
+
catch (_) {
|
|
175
|
+
// ignore any errors since we are stopping the call
|
|
176
|
+
}
|
|
177
|
+
this.unsubscribe();
|
|
178
|
+
this.onCallEnded.next();
|
|
179
|
+
this.pc.close();
|
|
180
|
+
this.audioSplitter.close();
|
|
181
|
+
this.videoSplitter.close();
|
|
182
|
+
}
|
|
183
|
+
stop() {
|
|
184
|
+
this.callEnded();
|
|
185
|
+
}
|
|
186
|
+
sendAudioPacket(rtp) {
|
|
187
|
+
this.pc.returnAudioTrack.writeRtp(rtp);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
exports.LiveCall = LiveCall;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MediaStreamTrack, RTCIceCandidate, RTCPeerConnection, RtcpPacket, RtpPacket } from '@koush/werift';
|
|
2
|
+
import { Subject } from 'rxjs';
|
|
3
|
+
export declare class PeerConnection {
|
|
4
|
+
pc: RTCPeerConnection;
|
|
5
|
+
onAudioRtp: Subject<RtpPacket>;
|
|
6
|
+
onAudioRtcp: Subject<RtcpPacket>;
|
|
7
|
+
onVideoRtp: Subject<RtpPacket>;
|
|
8
|
+
onVideoRtcp: Subject<RtcpPacket>;
|
|
9
|
+
returnAudioTrack: MediaStreamTrack;
|
|
10
|
+
constructor();
|
|
11
|
+
createAnswer(offer: {
|
|
12
|
+
type: 'offer';
|
|
13
|
+
sdp: string;
|
|
14
|
+
}): Promise<import("@koush/werift").RTCSessionDescription>;
|
|
15
|
+
addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
|
|
16
|
+
close(): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.PeerConnection = void 0;
|
|
13
|
+
const werift_1 = require("@koush/werift");
|
|
14
|
+
const rxjs_1 = require("rxjs");
|
|
15
|
+
const util_1 = require("./util");
|
|
16
|
+
const debug = false;
|
|
17
|
+
class PeerConnection {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.onAudioRtp = new rxjs_1.Subject();
|
|
20
|
+
this.onAudioRtcp = new rxjs_1.Subject();
|
|
21
|
+
this.onVideoRtp = new rxjs_1.Subject();
|
|
22
|
+
this.onVideoRtcp = new rxjs_1.Subject();
|
|
23
|
+
this.returnAudioTrack = new werift_1.MediaStreamTrack({ kind: 'audio' });
|
|
24
|
+
const pc = (this.pc = new werift_1.RTCPeerConnection({
|
|
25
|
+
codecs: {
|
|
26
|
+
audio: [
|
|
27
|
+
new werift_1.RTCRtpCodecParameters({
|
|
28
|
+
mimeType: 'audio/opus',
|
|
29
|
+
clockRate: 48000,
|
|
30
|
+
channels: 2,
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
video: [
|
|
34
|
+
new werift_1.RTCRtpCodecParameters({
|
|
35
|
+
mimeType: 'video/H264',
|
|
36
|
+
clockRate: 90000,
|
|
37
|
+
rtcpFeedback: [
|
|
38
|
+
{ type: 'transport-cc' },
|
|
39
|
+
{ type: 'ccm', parameter: 'fir' },
|
|
40
|
+
{ type: 'nack' },
|
|
41
|
+
{ type: 'nack', parameter: 'pli' },
|
|
42
|
+
{ type: 'goog-remb' },
|
|
43
|
+
],
|
|
44
|
+
parameters: 'level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f',
|
|
45
|
+
}),
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
})), audioTransceiver = pc.addTransceiver(this.returnAudioTrack, {
|
|
49
|
+
direction: 'sendrecv',
|
|
50
|
+
}), videoTransceiver = pc.addTransceiver('video', {
|
|
51
|
+
direction: 'recvonly',
|
|
52
|
+
});
|
|
53
|
+
audioTransceiver.onTrack.subscribe((track) => {
|
|
54
|
+
track.onReceiveRtp.subscribe((rtp) => {
|
|
55
|
+
this.onAudioRtp.next(rtp);
|
|
56
|
+
});
|
|
57
|
+
track.onReceiveRtcp.subscribe((rtcp) => {
|
|
58
|
+
this.onAudioRtcp.next(rtcp);
|
|
59
|
+
});
|
|
60
|
+
if (debug) {
|
|
61
|
+
track.onReceiveRtp.once(() => {
|
|
62
|
+
(0, util_1.logInfo)('received first audio packet');
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
videoTransceiver.onTrack.subscribe((track) => {
|
|
67
|
+
track.onReceiveRtp.subscribe((rtp) => {
|
|
68
|
+
this.onVideoRtp.next(rtp);
|
|
69
|
+
});
|
|
70
|
+
track.onReceiveRtcp.subscribe((rtcp) => {
|
|
71
|
+
this.onVideoRtcp.next(rtcp);
|
|
72
|
+
});
|
|
73
|
+
track.onReceiveRtp.once(() => {
|
|
74
|
+
if (debug) {
|
|
75
|
+
(0, util_1.logInfo)('received first video packet');
|
|
76
|
+
}
|
|
77
|
+
setInterval(() => videoTransceiver.receiver.sendRtcpPLI(track.ssrc), 2000);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
createAnswer(offer) {
|
|
82
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
83
|
+
yield this.pc.setRemoteDescription(offer);
|
|
84
|
+
const answer = yield this.pc.createAnswer();
|
|
85
|
+
yield this.pc.setLocalDescription(answer);
|
|
86
|
+
return answer;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
addIceCandidate(candidate) {
|
|
90
|
+
return this.pc.addIceCandidate(candidate);
|
|
91
|
+
}
|
|
92
|
+
close() {
|
|
93
|
+
this.pc.close().catch(util_1.logError);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
exports.PeerConnection = PeerConnection;
|
package/lib/api/ring-camera.d.ts
CHANGED
|
@@ -2,11 +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';
|
|
7
|
+
import { FfmpegOptions, LiveCall } from './live-call';
|
|
10
8
|
export declare function getBatteryLevel(data: Pick<CameraData, 'battery_life' | 'battery_life_2'>): number | null;
|
|
11
9
|
export declare function getSearchQueryString(options: CameraEventOptions | (HistoryOptions & {
|
|
12
10
|
accountId: string;
|
|
@@ -54,10 +52,7 @@ export declare class RingCamera extends Subscribed {
|
|
|
54
52
|
getDeviceSettings(): Promise<CameraDeviceSettingsData & import("./rest-client").ExtendedResponse>;
|
|
55
53
|
setInHomeDoorbell(enable: boolean): Promise<boolean>;
|
|
56
54
|
getHealth(): Promise<CameraHealth>;
|
|
57
|
-
|
|
58
|
-
private pollForActiveDing;
|
|
59
|
-
private expiredDingIds;
|
|
60
|
-
getSipConnectionDetails(): Promise<ActiveDing>;
|
|
55
|
+
startLiveCall(): Promise<LiveCall>;
|
|
61
56
|
private removeDingById;
|
|
62
57
|
processActiveDing(ding: ActiveDing): void;
|
|
63
58
|
getEvents(options?: CameraEventOptions): Promise<CameraEventResponse & import("./rest-client").ExtendedResponse>;
|
|
@@ -90,15 +85,8 @@ export declare class RingCamera extends Subscribed {
|
|
|
90
85
|
maxWaitMs?: number;
|
|
91
86
|
force?: boolean;
|
|
92
87
|
}): Promise<Buffer & import("./rest-client").ExtendedResponse>;
|
|
93
|
-
getSipOptions(): Promise<SipOptions>;
|
|
94
|
-
getUpdatedSipOptions(expiredDingId: string): Promise<SipOptions>;
|
|
95
|
-
createSipSession(options?: {
|
|
96
|
-
audio?: SrtpOptions;
|
|
97
|
-
video?: SrtpOptions;
|
|
98
|
-
skipFfmpegCheck?: boolean;
|
|
99
|
-
}): Promise<SipSession>;
|
|
100
88
|
recordToFile(outputPath: string, duration?: number): Promise<void>;
|
|
101
|
-
streamVideo(ffmpegOptions: FfmpegOptions): Promise<
|
|
89
|
+
streamVideo(ffmpegOptions: FfmpegOptions): Promise<LiveCall>;
|
|
102
90
|
subscribeToDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
103
91
|
unsubscribeFromDingEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
|
104
92
|
subscribeToMotionEvents(): Promise<void & import("./rest-client").ExtendedResponse>;
|
package/lib/api/ring-camera.js
CHANGED
|
@@ -14,10 +14,9 @@ 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");
|
|
19
|
+
const live_call_1 = require("./live-call");
|
|
21
20
|
const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
|
|
22
21
|
function parseBatteryLife(batteryLife) {
|
|
23
22
|
if (batteryLife === null || batteryLife === undefined) {
|
|
@@ -81,7 +80,6 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
81
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)());
|
|
82
81
|
this.onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.mapTo)(null), // no value needed, event is what matters
|
|
83
82
|
(0, operators_1.share)());
|
|
84
|
-
this.expiredDingIds = [];
|
|
85
83
|
this.lastSnapshotTimestamp = 0;
|
|
86
84
|
this.lastSnapshotTimestampLocal = 0;
|
|
87
85
|
this.fetchingSnapshot = false;
|
|
@@ -231,40 +229,23 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
231
229
|
return response.device_health;
|
|
232
230
|
});
|
|
233
231
|
}
|
|
234
|
-
|
|
235
|
-
return this.restClient
|
|
236
|
-
.request({
|
|
237
|
-
method: 'POST',
|
|
238
|
-
url: this.doorbotUrl('live_view'), // Ring app uses vod for battery cams, but doesn't appear to be necessary
|
|
239
|
-
})
|
|
240
|
-
.catch((e) => {
|
|
241
|
-
var _a;
|
|
242
|
-
if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.statusCode) === 403) {
|
|
243
|
-
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.`;
|
|
244
|
-
(0, util_1.logError)(errorMessage);
|
|
245
|
-
throw new Error(errorMessage);
|
|
246
|
-
}
|
|
247
|
-
throw e;
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
pollForActiveDing() {
|
|
251
|
-
// try every second until a new ding is received
|
|
252
|
-
this.addSubscriptions((0, rxjs_1.interval)(1000)
|
|
253
|
-
.pipe((0, operators_1.takeUntil)(this.onNewDing))
|
|
254
|
-
.subscribe(() => {
|
|
255
|
-
this.onRequestActiveDings.next(null);
|
|
256
|
-
}));
|
|
257
|
-
}
|
|
258
|
-
getSipConnectionDetails() {
|
|
232
|
+
startLiveCall() {
|
|
259
233
|
return __awaiter(this, void 0, void 0, function* () {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
234
|
+
const liveCall = yield this.restClient
|
|
235
|
+
.request({
|
|
236
|
+
method: 'POST',
|
|
237
|
+
url: this.doorbotUrl('live_call'),
|
|
238
|
+
})
|
|
239
|
+
.catch((e) => {
|
|
240
|
+
var _a;
|
|
241
|
+
if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.statusCode) === 403) {
|
|
242
|
+
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.`;
|
|
243
|
+
(0, util_1.logError)(errorMessage);
|
|
244
|
+
throw new Error(errorMessage);
|
|
245
|
+
}
|
|
246
|
+
throw e;
|
|
247
|
+
});
|
|
248
|
+
return new live_call_1.LiveCall(liveCall.data.session_id, this);
|
|
268
249
|
});
|
|
269
250
|
}
|
|
270
251
|
removeDingById(idToRemove) {
|
|
@@ -277,7 +258,6 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
277
258
|
this.onNewDing.next(ding);
|
|
278
259
|
setTimeout(() => {
|
|
279
260
|
this.removeDingById(ding.id_str);
|
|
280
|
-
this.expiredDingIds = this.expiredDingIds.filter((id) => id !== dingId);
|
|
281
261
|
}, 65 * 1000); // dings last ~1 minute
|
|
282
262
|
}
|
|
283
263
|
getEvents(options = {}) {
|
|
@@ -398,58 +378,19 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
398
378
|
return response;
|
|
399
379
|
});
|
|
400
380
|
}
|
|
401
|
-
getSipOptions() {
|
|
402
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
403
|
-
const activeDings = this.onActiveDings.getValue(), existingDing = activeDings
|
|
404
|
-
.filter((ding) => !this.expiredDingIds.includes(ding.id_str))
|
|
405
|
-
.slice()
|
|
406
|
-
.reverse()[0], ding = existingDing || (yield this.getSipConnectionDetails());
|
|
407
|
-
return {
|
|
408
|
-
to: ding.sip_to,
|
|
409
|
-
from: ding.sip_from,
|
|
410
|
-
dingId: ding.id_str,
|
|
411
|
-
localIp: yield (0, camera_utils_1.getDefaultIpAddress)(),
|
|
412
|
-
};
|
|
413
|
-
});
|
|
414
|
-
}
|
|
415
|
-
getUpdatedSipOptions(expiredDingId) {
|
|
416
|
-
// Got a 480 from sip session, which means it's no longer active
|
|
417
|
-
this.expiredDingIds.push(expiredDingId);
|
|
418
|
-
return this.getSipOptions();
|
|
419
|
-
}
|
|
420
|
-
createSipSession(options = {}) {
|
|
421
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
422
|
-
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([
|
|
423
|
-
this.getSipOptions(),
|
|
424
|
-
options.skipFfmpegCheck ? Promise.resolve(true) : (0, camera_utils_1.isFfmpegInstalled)(),
|
|
425
|
-
audioSplitter.portPromise,
|
|
426
|
-
audioRtcpSplitter.portPromise,
|
|
427
|
-
videoSplitter.portPromise,
|
|
428
|
-
videoRtcpSplitter.portPromise,
|
|
429
|
-
(0, camera_utils_1.reservePorts)({ type: 'tcp' }),
|
|
430
|
-
]), rtpOptions = {
|
|
431
|
-
audio: Object.assign({ port: audioPort, rtcpPort: audioRtcpPort }, (options.audio || (0, camera_utils_1.generateSrtpOptions)())),
|
|
432
|
-
video: Object.assign({ port: videoPort, rtcpPort: videoRtcpPort }, (options.video || (0, camera_utils_1.generateSrtpOptions)())),
|
|
433
|
-
};
|
|
434
|
-
if (!ffmpegIsInstalled) {
|
|
435
|
-
throw new Error('Ffmpeg is not installed. See https://github.com/dgreif/ring/wiki/FFmpeg for directions.');
|
|
436
|
-
}
|
|
437
|
-
return new sip_session_1.SipSession(sipOptions, rtpOptions, audioSplitter, audioRtcpSplitter, videoSplitter, videoRtcpSplitter, tlsPort, this);
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
381
|
recordToFile(outputPath, duration = 30) {
|
|
441
382
|
return __awaiter(this, void 0, void 0, function* () {
|
|
442
|
-
const
|
|
383
|
+
const liveCall = yield this.streamVideo({
|
|
443
384
|
output: ['-t', duration.toString(), outputPath],
|
|
444
385
|
});
|
|
445
|
-
yield (0, rxjs_1.firstValueFrom)(
|
|
386
|
+
yield (0, rxjs_1.firstValueFrom)(liveCall.onCallEnded);
|
|
446
387
|
});
|
|
447
388
|
}
|
|
448
389
|
streamVideo(ffmpegOptions) {
|
|
449
390
|
return __awaiter(this, void 0, void 0, function* () {
|
|
450
|
-
const
|
|
451
|
-
yield
|
|
452
|
-
return
|
|
391
|
+
const liveCall = yield this.startLiveCall();
|
|
392
|
+
yield liveCall.startTranscoding(ffmpegOptions);
|
|
393
|
+
return liveCall;
|
|
453
394
|
});
|
|
454
395
|
}
|
|
455
396
|
subscribeToDingEvents() {
|
|
@@ -481,5 +422,3 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
481
422
|
}
|
|
482
423
|
}
|
|
483
424
|
exports.RingCamera = RingCamera;
|
|
484
|
-
// SOMEDAY: extract image from video file?
|
|
485
|
-
// 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"
|
|
@@ -688,6 +689,21 @@ export interface ActiveDing {
|
|
|
688
689
|
sip_token: string;
|
|
689
690
|
sip_ding_id: string;
|
|
690
691
|
}
|
|
692
|
+
export interface LiveCallResponse {
|
|
693
|
+
ding_id: string;
|
|
694
|
+
ding_kind: DingKind;
|
|
695
|
+
device_id: number;
|
|
696
|
+
device_kind: RingCameraKind;
|
|
697
|
+
device_description: string;
|
|
698
|
+
state: DingState;
|
|
699
|
+
protocol: 'ring_media_server' | unknown;
|
|
700
|
+
now: number;
|
|
701
|
+
video_jitter_buffer_ms: number;
|
|
702
|
+
audio_jitter_buffer_ms: number;
|
|
703
|
+
data: {
|
|
704
|
+
session_id: string;
|
|
705
|
+
};
|
|
706
|
+
}
|
|
691
707
|
export interface AuthTokenResponse {
|
|
692
708
|
access_token: string;
|
|
693
709
|
expires_in: number;
|
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',
|