ring-client-api 9.22.1 → 9.25.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 +43 -0
- package/README.md +2 -1
- package/lib/api/api.d.ts +1 -0
- package/lib/api/api.js +4 -2
- package/lib/api/live-call.d.ts +41 -0
- package/lib/api/live-call.js +201 -0
- package/lib/api/peer-connection.d.ts +17 -0
- package/lib/api/peer-connection.js +96 -0
- package/lib/api/rest-client.d.ts +3 -0
- package/lib/api/rest-client.js +18 -6
- package/lib/api/ring-camera.d.ts +14 -8
- package/lib/api/ring-camera.js +101 -99
- package/lib/api/ring-chime.d.ts +1 -1
- package/lib/api/ring-chime.js +1 -1
- package/lib/api/ring-types.d.ts +17 -4
- package/lib/api/ring-types.js +4 -1
- package/lib/api/rtp-utils.d.ts +26 -0
- package/lib/api/rtp-utils.js +6 -1
- package/lib/api/sip-session.d.ts +4 -0
- package/lib/api/sip-session.js +26 -22
- package/lib/api/sip.d.ts +1 -0
- package/lib/api/sip.js +1 -0
- package/lib/api/util.js +5 -2
- package/package.json +37 -31
package/lib/api/ring-camera.js
CHANGED
|
@@ -18,8 +18,8 @@ const camera_utils_1 = require("@homebridge/camera-utils");
|
|
|
18
18
|
const util_1 = require("./util");
|
|
19
19
|
const sip_session_1 = require("./sip-session");
|
|
20
20
|
const subscribed_1 = require("./subscribed");
|
|
21
|
-
const
|
|
22
|
-
|
|
21
|
+
const live_call_1 = require("./live-call");
|
|
22
|
+
const maxSnapshotRefreshSeconds = 15, fullDayMs = 24 * 60 * 60 * 1000;
|
|
23
23
|
function parseBatteryLife(batteryLife) {
|
|
24
24
|
if (batteryLife === null || batteryLife === undefined) {
|
|
25
25
|
return null;
|
|
@@ -66,37 +66,36 @@ function getSearchQueryString(options) {
|
|
|
66
66
|
}
|
|
67
67
|
exports.getSearchQueryString = getSearchQueryString;
|
|
68
68
|
class RingCamera extends subscribed_1.Subscribed {
|
|
69
|
-
constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain) {
|
|
69
|
+
constructor(initialData, isDoorbot, restClient, avoidSnapshotBatteryDrain, treatKnockAsDing) {
|
|
70
70
|
super();
|
|
71
71
|
this.initialData = initialData;
|
|
72
72
|
this.isDoorbot = isDoorbot;
|
|
73
73
|
this.restClient = restClient;
|
|
74
74
|
this.avoidSnapshotBatteryDrain = avoidSnapshotBatteryDrain;
|
|
75
|
-
this.
|
|
76
|
-
this.deviceType = this.initialData.kind;
|
|
77
|
-
this.model = ring_types_1.RingCameraModel[this.initialData.kind] || 'Unknown Model';
|
|
78
|
-
this.onData = new rxjs_1.BehaviorSubject(this.initialData);
|
|
79
|
-
this.hasLight = this.initialData.led_status !== undefined;
|
|
80
|
-
this.hasSiren = this.initialData.siren_status !== undefined;
|
|
81
|
-
this.hasBattery = (0, ring_types_1.isBatteryCameraKind)(this.deviceType) ||
|
|
82
|
-
(typeof this.initialData.battery_life === 'string' &&
|
|
83
|
-
this.batteryLevel !== null &&
|
|
84
|
-
this.batteryLevel < 100 &&
|
|
85
|
-
this.batteryLevel >= 0);
|
|
75
|
+
this.treatKnockAsDing = treatKnockAsDing;
|
|
86
76
|
this.onRequestUpdate = new rxjs_1.Subject();
|
|
87
77
|
this.onRequestActiveDings = new rxjs_1.Subject();
|
|
88
78
|
this.onNewDing = new rxjs_1.Subject();
|
|
89
79
|
this.onActiveDings = new rxjs_1.BehaviorSubject([]);
|
|
90
|
-
this.onDoorbellPressed = this.onNewDing.pipe((0, operators_1.filter)((ding) => ding.kind === 'ding'
|
|
80
|
+
this.onDoorbellPressed = this.onNewDing.pipe((0, operators_1.filter)((ding) => ding.kind === 'ding' ||
|
|
81
|
+
(this.treatKnockAsDing && ding.kind === 'door_activity')), (0, operators_1.share)());
|
|
91
82
|
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)());
|
|
92
83
|
this.onMotionStarted = this.onMotionDetected.pipe((0, operators_1.filter)((currentlyDetected) => currentlyDetected), (0, operators_1.mapTo)(null), // no value needed, event is what matters
|
|
93
84
|
(0, operators_1.share)());
|
|
85
|
+
this.expiredDingIds = [];
|
|
86
|
+
this.lastSnapshotTimestamp = 0;
|
|
87
|
+
this.lastSnapshotTimestampLocal = 0;
|
|
88
|
+
this.fetchingSnapshot = false;
|
|
89
|
+
this.id = this.initialData.id;
|
|
90
|
+
this.deviceType = this.initialData.kind;
|
|
91
|
+
this.model = ring_types_1.RingCameraModel[this.initialData.kind] || 'Unknown Model';
|
|
92
|
+
this.onData = new rxjs_1.BehaviorSubject(this.initialData);
|
|
93
|
+
this.hasLight = this.initialData.led_status !== undefined;
|
|
94
|
+
this.hasSiren = this.initialData.siren_status !== undefined;
|
|
94
95
|
this.onBatteryLevel = this.onData.pipe((0, operators_1.map)(getBatteryLevel), (0, operators_1.distinctUntilChanged)());
|
|
95
96
|
this.onInHomeDoorbellStatus = this.onData.pipe((0, operators_1.map)(({ settings: { chime_settings } }) => {
|
|
96
97
|
return Boolean(chime_settings === null || chime_settings === void 0 ? void 0 : chime_settings.enable);
|
|
97
98
|
}), (0, operators_1.distinctUntilChanged)());
|
|
98
|
-
this.expiredDingIds = [];
|
|
99
|
-
this.lastSnapshotTimestampLocal = 0;
|
|
100
99
|
if (!initialData.subscribed) {
|
|
101
100
|
this.subscribeToDingEvents().catch((e) => {
|
|
102
101
|
(0, util_1.logError)('Failed to subscribe ' + initialData.description + ' to ding events');
|
|
@@ -128,6 +127,15 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
128
127
|
get batteryLevel() {
|
|
129
128
|
return getBatteryLevel(this.data);
|
|
130
129
|
}
|
|
130
|
+
get hasBattery() {
|
|
131
|
+
if (this.batteryLevel === null) {
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
return ((0, ring_types_1.isBatteryCameraKind)(this.deviceType) ||
|
|
135
|
+
(typeof this.initialData.battery_life === 'string' &&
|
|
136
|
+
this.batteryLevel < 100 &&
|
|
137
|
+
this.batteryLevel >= 0));
|
|
138
|
+
}
|
|
131
139
|
get hasLowBattery() {
|
|
132
140
|
return this.data.alerts.battery === 'low';
|
|
133
141
|
}
|
|
@@ -224,6 +232,25 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
224
232
|
return response.device_health;
|
|
225
233
|
});
|
|
226
234
|
}
|
|
235
|
+
startLiveCall() {
|
|
236
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
237
|
+
const liveCall = yield this.restClient
|
|
238
|
+
.request({
|
|
239
|
+
method: 'POST',
|
|
240
|
+
url: this.doorbotUrl('live_call'),
|
|
241
|
+
})
|
|
242
|
+
.catch((e) => {
|
|
243
|
+
var _a;
|
|
244
|
+
if (((_a = e.response) === null || _a === void 0 ? void 0 : _a.statusCode) === 403) {
|
|
245
|
+
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.`;
|
|
246
|
+
(0, util_1.logError)(errorMessage);
|
|
247
|
+
throw new Error(errorMessage);
|
|
248
|
+
}
|
|
249
|
+
throw e;
|
|
250
|
+
});
|
|
251
|
+
return new live_call_1.LiveCall(liveCall.data.session_id, this);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
227
254
|
startVideoOnDemand() {
|
|
228
255
|
return this.restClient
|
|
229
256
|
.request({
|
|
@@ -309,34 +336,14 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
309
336
|
get snapshotsAreBlocked() {
|
|
310
337
|
return this.data.settings.motion_detection_enabled === false;
|
|
311
338
|
}
|
|
312
|
-
getSnapshotTimestamp() {
|
|
313
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
314
|
-
const { timestamps, responseTimestamp } = yield this.restClient.request({
|
|
315
|
-
url: (0, rest_client_1.clientApi)('snapshots/timestamps'),
|
|
316
|
-
method: 'POST',
|
|
317
|
-
json: {
|
|
318
|
-
doorbot_ids: [this.id],
|
|
319
|
-
},
|
|
320
|
-
}), deviceTimestamp = timestamps[0], timestamp = deviceTimestamp ? deviceTimestamp.timestamp : 0, timestampAge = Math.abs(responseTimestamp - timestamp);
|
|
321
|
-
this.lastSnapshotTimestampLocal = timestamp ? Date.now() - timestampAge : 0;
|
|
322
|
-
return {
|
|
323
|
-
timestamp,
|
|
324
|
-
inLifeTime: this.isTimestampInLifeTime(timestampAge),
|
|
325
|
-
};
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
339
|
get snapshotLifeTime() {
|
|
329
340
|
return this.avoidSnapshotBatteryDrain && this.operatingOnBattery
|
|
330
341
|
? 600 * 1000 // battery cams only refresh timestamp every 10 minutes
|
|
331
|
-
: 10 * 1000; // snapshot updates will be forced. Limit to
|
|
342
|
+
: 10 * 1000; // snapshot updates will be forced. Limit to 10s lifetime
|
|
332
343
|
}
|
|
333
344
|
get currentTimestampAge() {
|
|
334
345
|
return Date.now() - this.lastSnapshotTimestampLocal;
|
|
335
346
|
}
|
|
336
|
-
get currentTimestampExpiresIn() {
|
|
337
|
-
// Gets 0 if stale snapshot is used because snapshot timestamp refused to update (recording in progress on battery cam)
|
|
338
|
-
return Math.max(this.lastSnapshotTimestampLocal - Date.now() + this.snapshotLifeTime, 0);
|
|
339
|
-
}
|
|
340
347
|
get hasSnapshotWithinLifetime() {
|
|
341
348
|
return this.isTimestampInLifeTime(this.currentTimestampAge);
|
|
342
349
|
}
|
|
@@ -344,79 +351,73 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
344
351
|
if (this.snapshotsAreBlocked) {
|
|
345
352
|
throw new Error(`Motion detection is disabled for ${this.name}, which prevents snapshots from this camera. This can be caused by Modes settings or by turning off the Record Motion setting.`);
|
|
346
353
|
}
|
|
354
|
+
if (this.isOffline) {
|
|
355
|
+
throw new Error(`Cannot fetch snapshot for ${this.name} because it is offline`);
|
|
356
|
+
}
|
|
347
357
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
(0, util_1.logDebug)(`Snapshot for ${this.name} is still within its life time (${this.currentTimestampAge / 1000}s old)`);
|
|
362
|
-
return true;
|
|
363
|
-
}
|
|
364
|
-
this.checkIfSnapshotsAreBlocked();
|
|
365
|
-
if (!this.avoidSnapshotBatteryDrain || !this.operatingOnBattery) {
|
|
366
|
-
// tell the camera to update snapshot immediately.
|
|
367
|
-
// avoidSnapshotBatteryDrain is best if you have a battery cam that you request snapshots for frequently. This can lead to battery drain if snapshot updates are forced.
|
|
368
|
-
yield this.requestSnapshotUpdate();
|
|
369
|
-
}
|
|
370
|
-
for (let i = 0; i < maxSnapshotRefreshAttempts; i++) {
|
|
371
|
-
this.checkIfSnapshotsAreBlocked();
|
|
372
|
-
const { timestamp, inLifeTime } = yield this.getSnapshotTimestamp();
|
|
373
|
-
if (!timestamp && this.isOffline) {
|
|
374
|
-
throw new Error(`No snapshot available and device ${this.name} is offline`);
|
|
375
|
-
}
|
|
376
|
-
if (inLifeTime) {
|
|
377
|
-
return false;
|
|
378
|
-
}
|
|
379
|
-
yield (0, util_1.delay)(snapshotRefreshDelay);
|
|
380
|
-
}
|
|
381
|
-
const extraMessageForBatteryCam = this.operatingOnBattery
|
|
382
|
-
? '. This is normal behavior since this camera is unable to capture snapshots while streaming'
|
|
383
|
-
: '';
|
|
384
|
-
throw new Error(`Snapshot for ${this.name} (${this.deviceType} - ${this.model}) failed to refresh after ${maxSnapshotRefreshAttempts} attempts${extraMessageForBatteryCam}`);
|
|
385
|
-
});
|
|
358
|
+
shouldUseExistingSnapshotPromise() {
|
|
359
|
+
if (this.fetchingSnapshot) {
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
if (this.hasSnapshotWithinLifetime) {
|
|
363
|
+
(0, util_1.logDebug)(`Snapshot for ${this.name} is still within its life time (${this.currentTimestampAge / 1000}s old)`);
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
if (!this.avoidSnapshotBatteryDrain || !this.operatingOnBattery) {
|
|
367
|
+
// tell the camera to update snapshot immediately.
|
|
368
|
+
// avoidSnapshotBatteryDrain is best if you have a battery cam that you request snapshots for frequently. This can lead to battery drain if snapshot updates are forced.
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
386
371
|
}
|
|
387
372
|
getSnapshot() {
|
|
388
373
|
return __awaiter(this, void 0, void 0, function* () {
|
|
389
|
-
this.
|
|
390
|
-
this.
|
|
391
|
-
this.refreshSnapshot().catch((e) => {
|
|
392
|
-
(0, util_1.logError)(e.message);
|
|
393
|
-
throw e;
|
|
394
|
-
});
|
|
395
|
-
try {
|
|
396
|
-
const useLastSnapshot = yield this.refreshSnapshotInProgress;
|
|
397
|
-
this.refreshSnapshotInProgress = undefined;
|
|
398
|
-
if (useLastSnapshot && this.lastSnapshotPromise) {
|
|
399
|
-
return this.lastSnapshotPromise;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
catch (e) {
|
|
403
|
-
this.refreshSnapshotInProgress = undefined;
|
|
404
|
-
throw e;
|
|
374
|
+
if (this.lastSnapshotPromise && this.shouldUseExistingSnapshotPromise()) {
|
|
375
|
+
return this.lastSnapshotPromise;
|
|
405
376
|
}
|
|
406
|
-
this.
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
377
|
+
this.checkIfSnapshotsAreBlocked();
|
|
378
|
+
this.lastSnapshotPromise = Promise.race([
|
|
379
|
+
this.getNextSnapshot({
|
|
380
|
+
afterMs: this.lastSnapshotTimestamp,
|
|
381
|
+
force: true,
|
|
382
|
+
}),
|
|
383
|
+
(0, util_1.delay)(maxSnapshotRefreshSeconds * 1000).then(() => {
|
|
384
|
+
const extraMessageForBatteryCam = this.operatingOnBattery
|
|
385
|
+
? '. This is normal behavior since this camera is unable to capture snapshots while streaming'
|
|
386
|
+
: '';
|
|
387
|
+
throw new Error(`Snapshot for ${this.name} (${this.deviceType} - ${this.model}) failed to refresh after ${maxSnapshotRefreshSeconds} seconds${extraMessageForBatteryCam}`);
|
|
388
|
+
}),
|
|
389
|
+
]);
|
|
410
390
|
try {
|
|
411
391
|
yield this.lastSnapshotPromise;
|
|
412
392
|
}
|
|
413
|
-
catch (
|
|
393
|
+
catch (e) {
|
|
414
394
|
// snapshot request failed, don't use it again
|
|
415
395
|
this.lastSnapshotPromise = undefined;
|
|
396
|
+
throw e;
|
|
416
397
|
}
|
|
398
|
+
this.fetchingSnapshot = false;
|
|
417
399
|
return this.lastSnapshotPromise;
|
|
418
400
|
});
|
|
419
401
|
}
|
|
402
|
+
getNextSnapshot({ afterMs, maxWaitMs, force, }) {
|
|
403
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
404
|
+
const response = yield this.restClient.request({
|
|
405
|
+
url: `https://app-snaps.ring.com/snapshots/next/${this.id}?extras=force`,
|
|
406
|
+
responseType: 'buffer',
|
|
407
|
+
searchParams: {
|
|
408
|
+
'after-ms': afterMs,
|
|
409
|
+
'max-wait-ms': maxWaitMs,
|
|
410
|
+
extras: force ? 'force' : undefined,
|
|
411
|
+
},
|
|
412
|
+
headers: {
|
|
413
|
+
accept: 'image/jpeg',
|
|
414
|
+
},
|
|
415
|
+
}), { responseTimestamp, timeMillis } = response, timestampAge = Math.abs(responseTimestamp - timeMillis);
|
|
416
|
+
this.lastSnapshotTimestamp = timeMillis;
|
|
417
|
+
this.lastSnapshotTimestampLocal = Date.now() - timestampAge;
|
|
418
|
+
return response;
|
|
419
|
+
});
|
|
420
|
+
}
|
|
420
421
|
getSipOptions() {
|
|
421
422
|
return __awaiter(this, void 0, void 0, function* () {
|
|
422
423
|
const activeDings = this.onActiveDings.getValue(), existingDing = activeDings
|
|
@@ -458,10 +459,11 @@ class RingCamera extends subscribed_1.Subscribed {
|
|
|
458
459
|
}
|
|
459
460
|
recordToFile(outputPath, duration = 30) {
|
|
460
461
|
return __awaiter(this, void 0, void 0, function* () {
|
|
461
|
-
const
|
|
462
|
+
const liveCall = yield this.startLiveCall();
|
|
463
|
+
yield liveCall.startTranscoding({
|
|
462
464
|
output: ['-t', duration.toString(), outputPath],
|
|
463
465
|
});
|
|
464
|
-
yield (0, rxjs_1.firstValueFrom)(
|
|
466
|
+
yield (0, rxjs_1.firstValueFrom)(liveCall.onCallEnded);
|
|
465
467
|
});
|
|
466
468
|
}
|
|
467
469
|
streamVideo(ffmpegOptions) {
|
package/lib/api/ring-chime.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare class RingChime {
|
|
|
5
5
|
private initialData;
|
|
6
6
|
private restClient;
|
|
7
7
|
id: number;
|
|
8
|
-
deviceType: "
|
|
8
|
+
deviceType: import("./ring-types").ChimeKind;
|
|
9
9
|
model: string;
|
|
10
10
|
onData: BehaviorSubject<ChimeData>;
|
|
11
11
|
onRequestUpdate: Subject<unknown>;
|
package/lib/api/ring-chime.js
CHANGED
|
@@ -23,11 +23,11 @@ class RingChime {
|
|
|
23
23
|
constructor(initialData, restClient) {
|
|
24
24
|
this.initialData = initialData;
|
|
25
25
|
this.restClient = restClient;
|
|
26
|
+
this.onRequestUpdate = new rxjs_1.Subject();
|
|
26
27
|
this.id = this.initialData.id;
|
|
27
28
|
this.deviceType = this.initialData.kind;
|
|
28
29
|
this.model = ring_types_1.ChimeModel[this.deviceType] || 'Chime';
|
|
29
30
|
this.onData = new rxjs_1.BehaviorSubject(this.initialData);
|
|
30
|
-
this.onRequestUpdate = new rxjs_1.Subject();
|
|
31
31
|
}
|
|
32
32
|
updateData(update) {
|
|
33
33
|
this.onData.next(update);
|
package/lib/api/ring-types.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ export declare enum RingDeviceType {
|
|
|
10
10
|
TemperatureSensor = "sensor.temperature",
|
|
11
11
|
WaterSensor = "sensor.water",
|
|
12
12
|
TiltSensor = "sensor.tilt",
|
|
13
|
+
GlassbreakSensor = "sensor.glassbreak",
|
|
13
14
|
RangeExtender = "range-extender.zwave",
|
|
14
15
|
ZigbeeAdapter = "adapter.zigbee",
|
|
15
16
|
AccessCodeVault = "access-code.vault",
|
|
@@ -67,6 +68,7 @@ export declare enum RingCameraKind {
|
|
|
67
68
|
doorbell_portal = "doorbell_portal",
|
|
68
69
|
doorbell_scallop = "doorbell_scallop",
|
|
69
70
|
doorbell_scallop_lite = "doorbell_scallop_lite",
|
|
71
|
+
doorbell_graham_cracker = "doorbell_graham_cracker",
|
|
70
72
|
lpd_v1 = "lpd_v1",
|
|
71
73
|
lpd_v2 = "lpd_v2",
|
|
72
74
|
jbox_v1 = "jbox_v1",
|
|
@@ -214,7 +216,7 @@ export interface BeamBridge {
|
|
|
214
216
|
role: string;
|
|
215
217
|
updated_at: string;
|
|
216
218
|
}
|
|
217
|
-
declare type ChimeKind = 'chime' | 'chime_pro' | 'chime_v2' | 'chime_pro_v2';
|
|
219
|
+
export declare type ChimeKind = 'chime' | 'chime_pro' | 'chime_v2' | 'chime_pro_v2';
|
|
218
220
|
export declare const ChimeModel: {
|
|
219
221
|
readonly [P in ChimeKind]: string;
|
|
220
222
|
};
|
|
@@ -686,9 +688,20 @@ export interface ActiveDing {
|
|
|
686
688
|
sip_token: string;
|
|
687
689
|
sip_ding_id: string;
|
|
688
690
|
}
|
|
689
|
-
export interface
|
|
690
|
-
|
|
691
|
-
|
|
691
|
+
export interface LiveCallResponse {
|
|
692
|
+
ding_id: string;
|
|
693
|
+
ding_kind: DingKind;
|
|
694
|
+
device_id: number;
|
|
695
|
+
device_kind: RingCameraKind;
|
|
696
|
+
device_description: string;
|
|
697
|
+
state: DingState;
|
|
698
|
+
protocol: 'ring_media_server' | unknown;
|
|
699
|
+
now: number;
|
|
700
|
+
video_jitter_buffer_ms: number;
|
|
701
|
+
audio_jitter_buffer_ms: number;
|
|
702
|
+
data: {
|
|
703
|
+
session_id: string;
|
|
704
|
+
};
|
|
692
705
|
}
|
|
693
706
|
export interface AuthTokenResponse {
|
|
694
707
|
access_token: string;
|
package/lib/api/ring-types.js
CHANGED
|
@@ -14,6 +14,7 @@ var RingDeviceType;
|
|
|
14
14
|
RingDeviceType["TemperatureSensor"] = "sensor.temperature";
|
|
15
15
|
RingDeviceType["WaterSensor"] = "sensor.water";
|
|
16
16
|
RingDeviceType["TiltSensor"] = "sensor.tilt";
|
|
17
|
+
RingDeviceType["GlassbreakSensor"] = "sensor.glassbreak";
|
|
17
18
|
RingDeviceType["RangeExtender"] = "range-extender.zwave";
|
|
18
19
|
RingDeviceType["ZigbeeAdapter"] = "adapter.zigbee";
|
|
19
20
|
RingDeviceType["AccessCodeVault"] = "access-code.vault";
|
|
@@ -75,6 +76,7 @@ var RingCameraKind;
|
|
|
75
76
|
RingCameraKind["doorbell_portal"] = "doorbell_portal";
|
|
76
77
|
RingCameraKind["doorbell_scallop"] = "doorbell_scallop";
|
|
77
78
|
RingCameraKind["doorbell_scallop_lite"] = "doorbell_scallop_lite";
|
|
79
|
+
RingCameraKind["doorbell_graham_cracker"] = "doorbell_graham_cracker";
|
|
78
80
|
RingCameraKind["lpd_v1"] = "lpd_v1";
|
|
79
81
|
RingCameraKind["lpd_v2"] = "lpd_v2";
|
|
80
82
|
RingCameraKind["jbox_v1"] = "jbox_v1";
|
|
@@ -93,7 +95,7 @@ var RingCameraKind;
|
|
|
93
95
|
RingCameraKind["stickup_cam_mini"] = "stickup_cam_mini";
|
|
94
96
|
})(RingCameraKind = exports.RingCameraKind || (exports.RingCameraKind = {}));
|
|
95
97
|
// RegExp taken from ring.com app
|
|
96
|
-
const isWiredCameraRegExp = new RegExp(/(^(lpd|jbox|stickup_cam_elite|stickup_cam_mini|hp_cam|spotlightw|floodlight))/);
|
|
98
|
+
const isWiredCameraRegExp = new RegExp(/(^(lpd|jbox|stickup_cam_elite|stickup_cam_mini|hp_cam|spotlightw|floodlight|doorbell_graham_cracker))/);
|
|
97
99
|
function isBatteryCameraKind(kind) {
|
|
98
100
|
return !kind.match(isWiredCameraRegExp);
|
|
99
101
|
}
|
|
@@ -107,6 +109,7 @@ exports.RingCameraModel = {
|
|
|
107
109
|
doorbell_portal: 'Door View Cam',
|
|
108
110
|
doorbell_scallop: 'Doorbell 3 Plus',
|
|
109
111
|
doorbell_scallop_lite: 'Doorbell 3',
|
|
112
|
+
doorbell_graham_cracker: 'Doorbell Wired',
|
|
110
113
|
lpd_v1: 'Doorbell Pro',
|
|
111
114
|
lpd_v2: 'Doorbell Pro',
|
|
112
115
|
jbox_v1: 'Doorbell Elite',
|
package/lib/api/rtp-utils.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/// <reference types="node" />
|
|
2
2
|
import { RtpSplitter, SrtpOptions } from '@homebridge/camera-utils';
|
|
3
|
+
import { DingKind } from './ring-types';
|
|
3
4
|
export interface RtpStreamOptions extends SrtpOptions {
|
|
4
5
|
port: number;
|
|
5
6
|
rtcpPort: number;
|
|
@@ -27,3 +28,28 @@ export declare function sendStunBindingRequest({ rtpDescription, rtpSplitter, rt
|
|
|
27
28
|
type: 'video' | 'audio';
|
|
28
29
|
}): void;
|
|
29
30
|
export declare function createStunResponder(rtpSplitter: RtpSplitter): void;
|
|
31
|
+
export interface LiveCallSession {
|
|
32
|
+
alexa_port: 18443;
|
|
33
|
+
app_session_token: string;
|
|
34
|
+
availability_zone: 'availability-zone';
|
|
35
|
+
custom_timer: {
|
|
36
|
+
max_sec: number;
|
|
37
|
+
};
|
|
38
|
+
ding_id: string;
|
|
39
|
+
ding_kind: DingKind;
|
|
40
|
+
doorbot_id: number;
|
|
41
|
+
exp: number;
|
|
42
|
+
ip: string;
|
|
43
|
+
port: number;
|
|
44
|
+
private_ip: string;
|
|
45
|
+
rms_fqdn: string;
|
|
46
|
+
rms_version: string;
|
|
47
|
+
rsp_port: number;
|
|
48
|
+
rtsp_port: number;
|
|
49
|
+
session_id: string;
|
|
50
|
+
sip_port: number;
|
|
51
|
+
webrtc_port: number;
|
|
52
|
+
webrtc_url: string;
|
|
53
|
+
wwr_port: number;
|
|
54
|
+
}
|
|
55
|
+
export declare function parseLiveCallSession(sessionId: string): LiveCallSession;
|
package/lib/api/rtp-utils.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.createStunResponder = exports.sendStunBindingRequest = exports.isStunMessage = void 0;
|
|
3
|
+
exports.parseLiveCallSession = exports.createStunResponder = exports.sendStunBindingRequest = exports.isStunMessage = void 0;
|
|
4
4
|
const util_1 = require("./util");
|
|
5
5
|
const stun = require('stun');
|
|
6
6
|
const stunMagicCookie = 0x2112a442; // https://tools.ietf.org/html/rfc5389#section-6
|
|
@@ -54,3 +54,8 @@ function createStunResponder(rtpSplitter) {
|
|
|
54
54
|
});
|
|
55
55
|
}
|
|
56
56
|
exports.createStunResponder = createStunResponder;
|
|
57
|
+
function parseLiveCallSession(sessionId) {
|
|
58
|
+
const encodedSession = sessionId.split('.')[1], buff = new Buffer(encodedSession, 'base64'), text = buff.toString('ascii');
|
|
59
|
+
return JSON.parse(text);
|
|
60
|
+
}
|
|
61
|
+
exports.parseLiveCallSession = parseLiveCallSession;
|
package/lib/api/sip-session.d.ts
CHANGED
|
@@ -27,6 +27,10 @@ export declare class SipSession extends Subscribed {
|
|
|
27
27
|
constructor(sipOptions: SipOptions, rtpOptions: RtpOptions, audioSplitter: RtpSplitter, audioRtcpSplitter: RtpSplitter, videoSplitter: RtpSplitter, videoRtcpSplitter: RtpSplitter, tlsPort: number, camera: RingCamera);
|
|
28
28
|
createSipCall(sipOptions: SipOptions): SipCall;
|
|
29
29
|
start(ffmpegOptions?: FfmpegOptions): Promise<RtpDescription>;
|
|
30
|
+
prepareTranscoder(transcodeVideoStream: boolean, ffmpegInputOptions: SpawnInput[] | undefined, remoteRtpOptions: RtpOptions, audioPort: number, videoPort: number, sdpInput: string): {
|
|
31
|
+
ffmpegInputArguments: SpawnInput[];
|
|
32
|
+
inputSdpLines: string[];
|
|
33
|
+
};
|
|
30
34
|
private startTranscoder;
|
|
31
35
|
reservePort(bufferPorts?: number): Promise<number>;
|
|
32
36
|
requestKeyFrame(): Promise<void>;
|
package/lib/api/sip-session.js
CHANGED
|
@@ -32,8 +32,8 @@ class SipSession extends subscribed_1.Subscribed {
|
|
|
32
32
|
this.hasStarted = false;
|
|
33
33
|
this.hasCallEnded = false;
|
|
34
34
|
this.onCallEndedSubject = new rxjs_1.ReplaySubject(1);
|
|
35
|
-
this.sipCall = this.createSipCall(this.sipOptions);
|
|
36
35
|
this.onCallEnded = this.onCallEndedSubject.asObservable();
|
|
36
|
+
this.sipCall = this.createSipCall(this.sipOptions);
|
|
37
37
|
}
|
|
38
38
|
createSipCall(sipOptions) {
|
|
39
39
|
if (this.sipCall) {
|
|
@@ -114,31 +114,17 @@ class SipSession extends subscribed_1.Subscribed {
|
|
|
114
114
|
}
|
|
115
115
|
});
|
|
116
116
|
}
|
|
117
|
-
|
|
118
|
-
const
|
|
117
|
+
prepareTranscoder(transcodeVideoStream, ffmpegInputOptions, remoteRtpOptions, audioPort, videoPort, sdpInput) {
|
|
118
|
+
const ffmpegInputArguments = [
|
|
119
119
|
'-hide_banner',
|
|
120
120
|
'-protocol_whitelist',
|
|
121
121
|
'pipe,udp,rtp,file,crypto',
|
|
122
122
|
'-f',
|
|
123
123
|
'sdp',
|
|
124
|
-
...(
|
|
124
|
+
...(ffmpegInputOptions || []),
|
|
125
125
|
'-i',
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
...(transcodeVideoStream
|
|
129
|
-
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
130
|
-
: []),
|
|
131
|
-
...(ffmpegOptions.output || []),
|
|
132
|
-
], ff = new camera_utils_1.FfmpegProcess({
|
|
133
|
-
ffmpegArgs,
|
|
134
|
-
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
135
|
-
exitCallback: () => this.callEnded(true),
|
|
136
|
-
logLabel: `From Ring (${this.camera.name})`,
|
|
137
|
-
logger: {
|
|
138
|
-
error: util_1.logError,
|
|
139
|
-
info: util_1.logDebug,
|
|
140
|
-
},
|
|
141
|
-
}), inputSdpLines = [
|
|
126
|
+
sdpInput,
|
|
127
|
+
], inputSdpLines = [
|
|
142
128
|
'v=0',
|
|
143
129
|
'o=105202070 3747 461 IN IP4 127.0.0.1',
|
|
144
130
|
's=Talk',
|
|
@@ -167,8 +153,6 @@ class SipSession extends subscribed_1.Subscribed {
|
|
|
167
153
|
};
|
|
168
154
|
});
|
|
169
155
|
}
|
|
170
|
-
this.onCallEnded.subscribe(() => ff.stop());
|
|
171
|
-
ff.writeStdin(inputSdpLines.filter((x) => Boolean(x)).join('\n'));
|
|
172
156
|
this.audioSplitter.addMessageHandler(({ isRtpMessage, message }) => {
|
|
173
157
|
if ((0, rtp_utils_1.isStunMessage)(message)) {
|
|
174
158
|
return null;
|
|
@@ -177,6 +161,26 @@ class SipSession extends subscribed_1.Subscribed {
|
|
|
177
161
|
port: isRtpMessage ? audioPort : audioPort + 1,
|
|
178
162
|
};
|
|
179
163
|
});
|
|
164
|
+
return {
|
|
165
|
+
ffmpegInputArguments,
|
|
166
|
+
inputSdpLines,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
startTranscoder(ffmpegOptions, remoteRtpOptions, audioPort, videoPort) {
|
|
170
|
+
const transcodeVideoStream = ffmpegOptions.video !== false, { ffmpegInputArguments, inputSdpLines } = this.prepareTranscoder(transcodeVideoStream, ffmpegOptions.input, remoteRtpOptions, audioPort, videoPort, 'pipe:'), ff = new camera_utils_1.FfmpegProcess({
|
|
171
|
+
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
172
|
+
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
173
|
+
: []), ...(ffmpegOptions.output || [])),
|
|
174
|
+
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
175
|
+
exitCallback: () => this.callEnded(true),
|
|
176
|
+
logLabel: `From Ring (${this.camera.name})`,
|
|
177
|
+
logger: {
|
|
178
|
+
error: util_1.logError,
|
|
179
|
+
info: util_1.logDebug,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
this.onCallEnded.subscribe(() => ff.stop());
|
|
183
|
+
ff.writeStdin(inputSdpLines.filter((x) => Boolean(x)).join('\n'));
|
|
180
184
|
}
|
|
181
185
|
reservePort(bufferPorts = 0) {
|
|
182
186
|
return __awaiter(this, void 0, void 0, function* () {
|
package/lib/api/sip.d.ts
CHANGED
|
@@ -19,6 +19,7 @@ export function create(options: any, callback: any): {
|
|
|
19
19
|
isFlowUri: (uri: any) => boolean;
|
|
20
20
|
hostname: () => any;
|
|
21
21
|
destroy: () => void;
|
|
22
|
+
makeResponse: typeof makeResponse;
|
|
22
23
|
};
|
|
23
24
|
export function start(options: any, callback: any): void;
|
|
24
25
|
export function parseAOR(data: any): any;
|
package/lib/api/sip.js
CHANGED
package/lib/api/util.js
CHANGED
|
@@ -8,14 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
11
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
15
|
exports.randomString = exports.randomInteger = exports.mapAsync = exports.stringify = exports.requestInput = exports.getHardwareId = exports.generateUuid = exports.enableDebug = exports.useLogger = exports.logError = exports.logInfo = exports.logDebug = exports.delay = void 0;
|
|
13
|
-
const
|
|
16
|
+
const debug_1 = __importDefault(require("debug"));
|
|
14
17
|
const colors_1 = require("colors");
|
|
15
18
|
const readline_1 = require("readline");
|
|
16
19
|
const uuid_1 = require("uuid");
|
|
17
20
|
const systeminformation_1 = require("systeminformation");
|
|
18
|
-
const debugLogger =
|
|
21
|
+
const debugLogger = (0, debug_1.default)('ring'), uuidNamespace = 'e53ffdc0-e91d-4ce1-bec2-df939d94739c';
|
|
19
22
|
let logger = {
|
|
20
23
|
logInfo(message) {
|
|
21
24
|
debugLogger(message);
|