ring-client-api 9.25.0-beta.1 → 10.0.0-beta.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.
@@ -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;
@@ -148,6 +145,9 @@ class RingCamera extends subscribed_1.Subscribed {
148
145
  get isOffline() {
149
146
  return this.data.alerts.connection === 'offline';
150
147
  }
148
+ get isRingEdgeEnabled() {
149
+ return this.data.settings.sheila_settings.local_storage_enabled === true;
150
+ }
151
151
  get hasInHomeDoorbell() {
152
152
  const { chime_settings } = this.data.settings;
153
153
  return (this.isDoorbot &&
@@ -234,57 +234,8 @@ class RingCamera extends subscribed_1.Subscribed {
234
234
  }
235
235
  startLiveCall() {
236
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
- }
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;
237
+ const auth = yield this.restClient.getCurrentAuth();
238
+ return new live_call_1.LiveCall(auth.access_token, this);
288
239
  });
289
240
  }
290
241
  removeDingById(idToRemove) {
@@ -297,7 +248,6 @@ class RingCamera extends subscribed_1.Subscribed {
297
248
  this.onNewDing.next(ding);
298
249
  setTimeout(() => {
299
250
  this.removeDingById(ding.id_str);
300
- this.expiredDingIds = this.expiredDingIds.filter((id) => id !== dingId);
301
251
  }, 65 * 1000); // dings last ~1 minute
302
252
  }
303
253
  getEvents(options = {}) {
@@ -402,7 +352,7 @@ class RingCamera extends subscribed_1.Subscribed {
402
352
  getNextSnapshot({ afterMs, maxWaitMs, force, }) {
403
353
  return __awaiter(this, void 0, void 0, function* () {
404
354
  const response = yield this.restClient.request({
405
- url: `https://app-snaps.ring.com/snapshots/next/${this.id}?extras=force`,
355
+ url: `https://app-snaps.ring.com/snapshots/next/${this.id}`,
406
356
  responseType: 'buffer',
407
357
  searchParams: {
408
358
  'after-ms': afterMs,
@@ -418,49 +368,9 @@ class RingCamera extends subscribed_1.Subscribed {
418
368
  return response;
419
369
  });
420
370
  }
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
371
  recordToFile(outputPath, duration = 30) {
461
372
  return __awaiter(this, void 0, void 0, function* () {
462
- const liveCall = yield this.startLiveCall();
463
- yield liveCall.startTranscoding({
373
+ const liveCall = yield this.streamVideo({
464
374
  output: ['-t', duration.toString(), outputPath],
465
375
  });
466
376
  yield (0, rxjs_1.firstValueFrom)(liveCall.onCallEnded);
@@ -468,9 +378,9 @@ class RingCamera extends subscribed_1.Subscribed {
468
378
  }
469
379
  streamVideo(ffmpegOptions) {
470
380
  return __awaiter(this, void 0, void 0, function* () {
471
- const sipSession = yield this.createSipSession();
472
- yield sipSession.start(ffmpegOptions);
473
- return sipSession;
381
+ const liveCall = yield this.startLiveCall();
382
+ yield liveCall.startTranscoding(ffmpegOptions);
383
+ return liveCall;
474
384
  });
475
385
  }
476
386
  subscribeToDingEvents() {
@@ -502,5 +412,3 @@ class RingCamera extends subscribed_1.Subscribed {
502
412
  }
503
413
  }
504
414
  exports.RingCamera = RingCamera;
505
- // SOMEDAY: extract image from video file?
506
- // ffmpeg -i input.mp4 -r 1 -f image2 image-%2d.png
@@ -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"
@@ -378,6 +379,13 @@ export interface CameraData {
378
379
  live_view_disabled?: boolean;
379
380
  motion_detection_enabled?: boolean;
380
381
  power_mode?: 'battery' | 'wired';
382
+ sheila_settings: {
383
+ cv_processing_enabled: boolean | null;
384
+ local_storage_enabled: boolean | null;
385
+ };
386
+ server_settings: {
387
+ ring_media_server_enabled: boolean;
388
+ };
381
389
  };
382
390
  features: {
383
391
  motions_enabled: boolean;
@@ -688,21 +696,6 @@ export interface ActiveDing {
688
696
  sip_token: string;
689
697
  sip_ding_id: string;
690
698
  }
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
- };
705
- }
706
699
  export interface AuthTokenResponse {
707
700
  access_token: string;
708
701
  expires_in: number;
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ring-client-api",
3
- "version": "9.25.0-beta.1",
3
+ "version": "10.0.0-beta.2",
4
4
  "description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
5
5
  "main": "lib/api/index.js",
6
6
  "bin": {
@@ -14,7 +14,7 @@
14
14
  "watch:tests": "jest --watch",
15
15
  "lint": "eslint '**/*.ts' --fix",
16
16
  "build": "rm -rf lib && tsc --declaration",
17
- "build-quick": "rm -rf lib && esbuild ./{api,examples,homebridge}/*.ts ./api/*.js --target=es2017 --format=cjs --outdir=lib",
17
+ "build-quick": "rm -rf lib && esbuild ./{api,examples,homebridge}/*.ts --target=es2017 --format=cjs --outdir=lib",
18
18
  "example": "npm run build-quick && node ./lib/examples/example.js",
19
19
  "api-example": "npm run build-quick && node ./lib/examples/api-example.js",
20
20
  "record-example": "npm run build-quick && node ./lib/examples/record-example.js",
@@ -57,15 +57,13 @@
57
57
  "dependencies": {
58
58
  "@homebridge/camera-utils": "2.1.0",
59
59
  "@homebridge/plugin-ui-utils": "0.0.19",
60
- "@koush/werift": "0.14.5-beta5",
60
+ "@koush/werift": "0.14.5-beta7",
61
61
  "@types/socket.io-client": "1.4.36",
62
62
  "colors": "1.4.0",
63
63
  "debug": "4.3.3",
64
64
  "got": "11.8.3",
65
65
  "rxjs": "7.5.2",
66
- "sdp": "3.0.3",
67
66
  "socket.io-client": "2.4.0",
68
- "stun": "2.1.0",
69
67
  "systeminformation": "5.10.7",
70
68
  "uuid": "8.3.2",
71
69
  "ws": "8.4.2"
@@ -97,8 +95,8 @@
97
95
  "typescript": "4.5.5"
98
96
  },
99
97
  "engines": {
100
- "node": "^10 || ^12.20 || ^14 || ^16",
101
- "homebridge": ">=1.0.0"
98
+ "node": "^12.20 || ^14 || ^16",
99
+ "homebridge": ">=1.4.0"
102
100
  },
103
101
  "bugs": {
104
102
  "url": "https://github.com/dgreif/ring/issues"
@@ -1,55 +0,0 @@
1
- /// <reference types="node" />
2
- import { RtpSplitter, SrtpOptions } from '@homebridge/camera-utils';
3
- 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
- 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;
@@ -1,61 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseLiveCallSession = exports.createStunResponder = exports.sendStunBindingRequest = exports.isStunMessage = void 0;
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;
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;
@@ -1,76 +0,0 @@
1
- import { Subject } from 'rxjs';
2
- import { RtpDescription, RtpOptions } from './rtp-utils';
3
- export declare const expiredDingError: Error;
4
- interface UriOptions {
5
- name?: string;
6
- uri: string;
7
- params?: {
8
- tag?: string;
9
- };
10
- }
11
- interface SipHeaders {
12
- [name: string]: string | any;
13
- cseq: {
14
- seq: number;
15
- method: string;
16
- };
17
- to: UriOptions;
18
- from: UriOptions;
19
- contact?: UriOptions[];
20
- via?: UriOptions[];
21
- }
22
- export interface SipRequest {
23
- uri: UriOptions | string;
24
- method: string;
25
- headers: SipHeaders;
26
- content: string;
27
- }
28
- export interface SipResponse {
29
- status: number;
30
- reason: string;
31
- headers: SipHeaders;
32
- content: string;
33
- }
34
- export interface SipClient {
35
- send: (request: SipRequest | SipResponse, handler?: (response: SipResponse) => void) => void;
36
- destroy: () => void;
37
- makeResponse: (response: SipRequest, status: number, method: string) => SipResponse;
38
- }
39
- export interface SipOptions {
40
- to: string;
41
- from: string;
42
- dingId: string;
43
- localIp: string;
44
- }
45
- export declare class SipCall {
46
- private sipOptions;
47
- private seq;
48
- private fromParams;
49
- private toParams;
50
- private callId;
51
- private sipClient;
52
- readonly onEndedByRemote: Subject<unknown>;
53
- private destroyed;
54
- private cameraConnected?;
55
- private cameraConnectedPromise;
56
- readonly sdp: string;
57
- readonly audioUfrag: string;
58
- readonly videoUfrag: string;
59
- constructor(sipOptions: SipOptions, rtpOptions: RtpOptions, tlsPort: number);
60
- request({ method, headers, content, seq, }: {
61
- method: string;
62
- headers?: Partial<SipHeaders>;
63
- content?: string;
64
- seq?: number;
65
- }): Promise<SipResponse>;
66
- private ackWithInfo;
67
- sendDtmf(key: string): Promise<SipResponse>;
68
- private sendKeyFrameRequest;
69
- invite(): Promise<RtpDescription>;
70
- requestKeyFrame(): Promise<void>;
71
- private speakerActivated;
72
- activateCameraSpeaker(): Promise<void>;
73
- sendBye(): Promise<void | SipResponse>;
74
- destroy(): void;
75
- }
76
- export {};