homebridge-ring 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 CHANGED
@@ -2,6 +2,49 @@
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
+ ## [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
+
7
+
8
+ ### Features
9
+
10
+ * use new WebRTC endpoint for live calls ([6e7779f](https://github.com/dgreif/ring/commit/6e7779feef9789583921fc44bab32c099f3026d2))
11
+
12
+ ## [9.25.0-beta.0](https://github.com/dgreif/ring/compare/v9.24.0...v9.25.0-beta.0) (2022-01-29)
13
+
14
+
15
+ ### Features
16
+
17
+ * **api:** decouple ffmpeg prep from execution ([#859](https://github.com/dgreif/ring/issues/859)) ([e3cc9d1](https://github.com/dgreif/ring/commit/e3cc9d15584377bf2be94a9d457efc3dfcb4bd8b))
18
+ * handle new camera types with no battery ([11ae286](https://github.com/dgreif/ring/commit/11ae28697d8cb0e2da71e880c82e1883f04047b9))
19
+ * slightly faster snapshots via new endpoint ([b840471](https://github.com/dgreif/ring/commit/b840471bc6b7b66e5873370251bd57c1a35b267a))
20
+
21
+ ## [9.24.0](https://github.com/dgreif/ring/compare/v9.23.0...v9.24.0) (2022-01-22)
22
+
23
+
24
+ ### Features
25
+
26
+ * support glassbreak sensor ([98271f9](https://github.com/dgreif/ring/commit/98271f91f13aaae051fccaab7165cd17f2a2377c))
27
+
28
+
29
+ ### Bug Fixes
30
+
31
+ * allow process to exit when RingApi.disconnect() is called ([#851](https://github.com/dgreif/ring/issues/851)) ([8ef25ac](https://github.com/dgreif/ring/commit/8ef25acca66c533873b7fd43c7c899e34b4a7617))
32
+ * avoid error when ring terminates video stream ([7717d5e](https://github.com/dgreif/ring/commit/7717d5eaa40afed3d34f0e8e6eea1402e21afef7)), closes [#853](https://github.com/dgreif/ring/issues/853)
33
+ * update dependencies ([c80d052](https://github.com/dgreif/ring/commit/c80d052f9eaf4c0a50a495166177daa40299ffa7))
34
+
35
+ ## [9.23.0](https://github.com/dgreif/ring/compare/v9.22.1...v9.23.0) (2021-10-17)
36
+
37
+
38
+ ### Features
39
+
40
+ * **homebridge:** config option to hide camera lights ([58be17c](https://github.com/dgreif/ring/commit/58be17cfee524bfb158566e59d60438e9b6f0f5d)), closes [#802](https://github.com/dgreif/ring/issues/802)
41
+ * **homebridge:** option to treat "knock" as a ding ([7ece8b2](https://github.com/dgreif/ring/commit/7ece8b25314ef403a6c2bab6a28476a3b7229507)), closes [#514](https://github.com/dgreif/ring/issues/514)
42
+
43
+
44
+ ### Bug Fixes
45
+
46
+ * update dependencies ([b685e27](https://github.com/dgreif/ring/commit/b685e27e952a94635a277848821ebfa640d50ee2))
47
+
5
48
  ### [9.22.1](https://github.com/dgreif/ring/compare/v9.22.0...v9.22.1) (2021-10-13)
6
49
 
7
50
 
package/README.md CHANGED
@@ -57,6 +57,7 @@ Only include an optional parameter if you actually need it. Default behavior wi
57
57
  "hideDeviceIds": ["477e4800-fcde-4493-969b-d1a06f683102", "5aaed7a7-06df-4f18-b3af-291c89854d60"],
58
58
  "hideLightGroups": true,
59
59
  "hideDoorbellSwitch": true,
60
+ "hideCameraLight": true,
60
61
  "hideCameraMotionSensor": true,
61
62
  "sendCameraMotionNotificationsToTv": true,
62
63
  "sendDoorbellMotionNotificationsToTv": true,
@@ -65,6 +66,7 @@ Only include an optional parameter if you actually need it. Default behavior wi
65
66
  "hideAlarmSirenSwitch": true,
66
67
  "hideUnsupportedServices": true,
67
68
  "avoidSnapshotBatteryDrain": true,
69
+ "treatKnockAsDing": true,
68
70
  "cameraStatusPollingSeconds": 20,
69
71
  "cameraDingsPollingSeconds": 2,
70
72
  "locationModePollingSeconds": 20,
@@ -79,6 +81,7 @@ Option | Default | Explanation
79
81
  `hideDeviceIds` | `[]` | Allows you to hide specific devices by an array of ids. The id for each device is logged when homebridge starts.
80
82
  `hideLightGroups` | `false` | Ring smart lighting allows you to create lighting groups within the Ring app. These groups are convenient for detecting motion in an area of your yard and turning on/off all lights in the group. However, you may wish to group the lights differently in HomeKit and ignore the groups you have configured in Ring. If this option is `true`, your Ring groups (and their associated motion sensor) will be ignored and will not show up in HomeKit.
81
83
  `hideDoorbellSwitch` | `false` | If you have a Ring video doorbell, you will see a Programmable Switch associated with it. This switch can be used to perform actions on when the doorbell is pressed using "Single Press" actions. If you do not care to perform actions when the doorbell is pressed, you can hide the Programmable Switch by setting this option to `true`. You will still be able to receive _notifications_ from the doorbell even if the Programmable Switch is hidden (notifications can be configured in the settings for the doorbell camera in the Home app)
84
+ `hideCameraLight` | `false` | If `true`, hides the light for Ring cameras in HomeKit.
82
85
  `hideCameraMotionSensor` | `false` | If `true`, hides the motion sensor for Ring cameras in HomeKit.
83
86
  `sendCameraMotionNotificationsToTv` | `false` | If `true`, creates a 'Doorbell' accessory triggered by non-doorbell camera motion alerts for tvOS 14 notification support.
84
87
  `sendDoorbellMotionNotificationsToTv` | `false` | If `true`, motion events on doorbell cameras will trigger a doorbell press event for tvOS 14 notification support.
@@ -89,6 +92,7 @@ Option | Default | Explanation
89
92
  `showPanicButtons` | `false` | Creates a new `Panic Buttons` device in HomeKit with `Burglar Alarm` and `Fire Alarm` switches. **Use these at your own risk. I do not guarantee functionality in case of emergency, nor do I take responsibility for any false alarms**. These function just like the SOS sliders in the Ring app.
90
93
  `nightModeBypassFor` | no Night Mode | Allows you to use Night mode to "Bypass and Arm" a Ring Alarm. Can be set to `all` for Away or `some` for Home. If set and Night mode is activated from HomeKit, any open contact sensors will automatically be bypassed.
91
94
  `avoidSnapshotBatteryDrain` | false | Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery.
95
+ `treatKnockAsDing` | false | When someone knocks on your door, it will be recognized as a doorbell press in HomeKit. Limited to select camera models (Peephole Cam).
92
96
  `cameraStatusPollingSeconds` | `20` | How frequently to poll for updates to your cameras and chimes (in seconds). Information like light/siren/volume/snooze status do not update in real time and need to be requested periodically.
93
97
  `cameraDingsPollingSeconds` | `2` | How frequently to poll for new events from your cameras. These include motion and doorbell presses.
94
98
  `locationModePollingSeconds` | `20` | How frequently to poll for location mode updates (in seconds). This is only useful if you are using location modes to control camera settings and want to keep an up-to-date reference of the current mode for each location. Polling is automatically disabled for locations equipped with a Ring Alarm. Will hide Location Mode switch if set to `0`
@@ -161,6 +165,10 @@ If you are having issues with your cameras in the Home app, please see the [Came
161
165
  * Detect water leak
162
166
  * Tamper status
163
167
  * Battery status
168
+ * Glassbreak Sensor (**untested**)
169
+ * Detect glass breaking
170
+ * Tamper status
171
+ * Battery status
164
172
  * Ring Smart Lights (Motion Detector, Flood/Path/Step/Spot Lights, Bulbs, Transformer, Outdoor Smart Plug, Mailbox Sensor)
165
173
  * On/Off
166
174
  * Brightness Level
@@ -31,6 +31,10 @@
31
31
  "type": "boolean",
32
32
  "description": "Hides the Programmable Switch for Ring doorbells. This switch is only useful for triggering an automation when the doorbell is pressed, so most users can safely hide it"
33
33
  },
34
+ "hideCameraLight": {
35
+ "title": "Hide Camera Light",
36
+ "type": "boolean"
37
+ },
34
38
  "hideCameraMotionSensor": {
35
39
  "title": "Hide Camera Motion Sensors",
36
40
  "type": "boolean"
@@ -82,6 +86,11 @@
82
86
  "type": "boolean",
83
87
  "description": "Causes snapshots for battery cameras to be fetched at a minimum 10 minute interval to avoid draining the battery. This is useful if have the Home app open constantly, causing frequent snapshot requests."
84
88
  },
89
+ "treatKnockAsDing": {
90
+ "title": "Treat Knock as Ding",
91
+ "type": "boolean",
92
+ "description": "When someone knocks on your door, it will be recognized as a doorbell press in HomeKit. Limited to select camera models (Peephole Cam)"
93
+ },
85
94
  "sendCameraMotionNotificationsToTv": {
86
95
  "title": "Create a 'Doorbell' accessory triggered by non-doorbell camera motion alerts for tvOS 14 notification support",
87
96
  "type": "boolean"
@@ -161,6 +170,7 @@
161
170
  "alarmOnEntryDelay",
162
171
  "hideLightGroups",
163
172
  "hideDoorbellSwitch",
173
+ "hideCameraLight",
164
174
  "hideCameraMotionSensor",
165
175
  "hideCameraSirenSwitch",
166
176
  "hideInHomeDoorbellSwitch",
@@ -169,6 +179,7 @@
169
179
  "showPanicButtons",
170
180
  "nightModeBypassFor",
171
181
  "avoidSnapshotBatteryDrain",
182
+ "treatKnockAsDing",
172
183
  "sendCameraMotionNotificationsToTv",
173
184
  "sendDoorbellMotionNotificationsToTv",
174
185
  "beamDurationSeconds",
package/lib/api/api.d.ts CHANGED
@@ -9,6 +9,7 @@ export interface RingApiOptions extends SessionOptions {
9
9
  cameraDingsPollingSeconds?: number;
10
10
  locationModePollingSeconds?: number;
11
11
  avoidSnapshotBatteryDrain?: boolean;
12
+ treatKnockAsDing?: boolean;
12
13
  debug?: boolean;
13
14
  ffmpegPath?: string;
14
15
  externalPorts?: {
package/lib/api/api.js CHANGED
@@ -24,7 +24,8 @@ class RingApi extends subscribed_1.Subscribed {
24
24
  super();
25
25
  this.options = options;
26
26
  this.restClient = new rest_client_1.RingRestClient(this.options);
27
- this.onRefreshTokenUpdated = this.restClient.onRefreshTokenUpdated.asObservable();
27
+ this.onRefreshTokenUpdated =
28
+ this.restClient.onRefreshTokenUpdated.asObservable();
28
29
  if (options.debug) {
29
30
  (0, util_1.enableDebug)();
30
31
  }
@@ -128,7 +129,7 @@ class RingApi extends subscribed_1.Subscribed {
128
129
  return __awaiter(this, void 0, void 0, function* () {
129
130
  const rawLocations = yield this.fetchRawLocations(), { authorizedDoorbots, chimes, doorbots, allCameras, baseStations, beamBridges, } = yield this.fetchRingDevices(), locationIdsWithHubs = [...baseStations, ...beamBridges].map((x) => x.location_id), cameras = allCameras.map((data) => new ring_camera_1.RingCamera(data, doorbots.includes(data) ||
130
131
  authorizedDoorbots.includes(data) ||
131
- data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
132
+ data.kind.startsWith('doorbell'), this.restClient, this.options.avoidSnapshotBatteryDrain || false, this.options.treatKnockAsDing || false)), ringChimes = chimes.map((data) => new ring_chime_1.RingChime(data, this.restClient)), locations = rawLocations
132
133
  .filter((location) => {
133
134
  return (!Array.isArray(this.options.locationIds) ||
134
135
  this.options.locationIds.includes(location.location_id));
@@ -169,6 +170,7 @@ class RingApi extends subscribed_1.Subscribed {
169
170
  .catch((e) => {
170
171
  (0, util_1.logError)(e);
171
172
  });
173
+ this.restClient.clearTimeouts();
172
174
  }
173
175
  }
174
176
  exports.RingApi = RingApi;
@@ -0,0 +1,41 @@
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<void>;
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
+ prepareTranscoder(transcodeVideoStream: boolean, ffmpegInputOptions: SpawnInput[] | undefined, audioPort: number, videoPort: number, sdpInput: string): {
29
+ ffmpegInputArguments: SpawnInput[];
30
+ inputSdpLines: string[];
31
+ };
32
+ startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
33
+ activated: boolean;
34
+ activate(): Promise<void>;
35
+ activateCameraSpeaker(): Promise<void>;
36
+ private sendMessage;
37
+ private callEnded;
38
+ stop(): void;
39
+ sendAudioPacket(rtp: RtpPacket): void;
40
+ }
41
+ export {};
@@ -0,0 +1,201 @@
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
+ class LiveCall extends subscribed_1.Subscribed {
23
+ constructor(sessionId, camera) {
24
+ super();
25
+ this.sessionId = sessionId;
26
+ this.camera = camera;
27
+ this.onCallAnswered = new rxjs_1.ReplaySubject(1);
28
+ this.onCallEnded = new rxjs_1.ReplaySubject(1);
29
+ this.audioSplitter = new camera_utils_1.RtpSplitter();
30
+ this.videoSplitter = new camera_utils_1.RtpSplitter();
31
+ this.activated = false;
32
+ const liveCallSession = (0, rtp_utils_1.parseLiveCallSession)(sessionId);
33
+ this.pc = new peer_connection_1.PeerConnection();
34
+ this.ws = new ws_1.WebSocket(`wss://${liveCallSession.rms_fqdn}:${liveCallSession.webrtc_port}/`, {
35
+ headers: {
36
+ API_VERSION: '3.1',
37
+ API_TOKEN: sessionId,
38
+ 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',
39
+ },
40
+ });
41
+ this.onAudioRtp = this.pc.onAudioRtp;
42
+ this.onVideoRtp = this.pc.onVideoRtp;
43
+ this.onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message');
44
+ this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
45
+ const onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
46
+ this.addSubscriptions(this.onMessage
47
+ .pipe((0, operators_1.concatMap)((message) => {
48
+ return this.handleMessage(JSON.parse(message.data));
49
+ }))
50
+ .subscribe(), this.onWsOpen.subscribe(() => {
51
+ (0, util_1.logDebug)(`WebSocket connected for ${this.camera.name}`);
52
+ }), onError.subscribe((e) => {
53
+ (0, util_1.logError)(e);
54
+ this.callEnded();
55
+ }), onClose.subscribe(() => {
56
+ this.callEnded();
57
+ }));
58
+ }
59
+ handleMessage(message) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ switch (message.method) {
62
+ case 'sdp':
63
+ const answer = yield this.pc.createAnswer(message);
64
+ this.sendMessage(Object.assign({ method: 'sdp' }, answer));
65
+ this.onCallAnswered.next();
66
+ return;
67
+ case 'ice':
68
+ yield this.pc.addIceCandidate({
69
+ candidate: message.ice,
70
+ sdpMLineIndex: message.mlineindex,
71
+ });
72
+ return;
73
+ }
74
+ });
75
+ }
76
+ reservePort(bufferPorts = 0) {
77
+ return __awaiter(this, void 0, void 0, function* () {
78
+ const ports = yield (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
79
+ return ports[0];
80
+ });
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
+ startTranscoding(ffmpegOptions) {
131
+ return __awaiter(this, void 0, void 0, function* () {
132
+ const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, { ffmpegInputArguments, inputSdpLines } = this.prepareTranscoder(transcodeVideoStream, ffmpegOptions.input, audioPort, videoPort, 'pipe:'), ff = new camera_utils_1.FfmpegProcess({
133
+ ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
134
+ ? ffmpegOptions.video || ['-vcodec', 'copy']
135
+ : []), ...(ffmpegOptions.output || [])),
136
+ ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
137
+ exitCallback: () => this.callEnded(),
138
+ logLabel: `From Ring (${this.camera.name})`,
139
+ logger: {
140
+ error: util_1.logError,
141
+ info: util_1.logDebug,
142
+ },
143
+ });
144
+ this.onCallEnded.subscribe(() => ff.stop());
145
+ ff.writeStdin(inputSdpLines.filter((x) => Boolean(x)).join('\n'));
146
+ // Activate the stream now that ffmpeg is ready to receive
147
+ yield this.activate();
148
+ });
149
+ }
150
+ activate() {
151
+ return __awaiter(this, void 0, void 0, function* () {
152
+ if (this.activated) {
153
+ return;
154
+ }
155
+ this.activated = true;
156
+ yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
157
+ this.sendMessage({ method: 'activate_session' });
158
+ this.sendMessage({
159
+ video_enabled: true,
160
+ audio_enabled: true,
161
+ method: 'stream_options',
162
+ });
163
+ });
164
+ }
165
+ activateCameraSpeaker() {
166
+ return __awaiter(this, void 0, void 0, function* () {
167
+ yield (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
168
+ this.sendMessage({
169
+ stealth_mode: false,
170
+ method: 'camera_options',
171
+ });
172
+ });
173
+ }
174
+ sendMessage(message) {
175
+ this.ws.send(JSON.stringify(message));
176
+ }
177
+ callEnded() {
178
+ try {
179
+ this.sendMessage({
180
+ reason: { code: 0, text: '' },
181
+ method: 'close',
182
+ });
183
+ this.ws.close();
184
+ }
185
+ catch (_) {
186
+ // ignore any errors since we are stopping the call
187
+ }
188
+ this.unsubscribe();
189
+ this.onCallEnded.next();
190
+ this.pc.close();
191
+ this.audioSplitter.close();
192
+ this.videoSplitter.close();
193
+ }
194
+ stop() {
195
+ this.callEnded();
196
+ }
197
+ sendAudioPacket(rtp) {
198
+ this.pc.returnAudioTrack.writeRtp(rtp);
199
+ }
200
+ }
201
+ 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;
@@ -6,6 +6,7 @@ export declare function deviceApi(path: string): string;
6
6
  export declare function appApi(path: string): string;
7
7
  export interface ExtendedResponse {
8
8
  responseTimestamp: number;
9
+ timeMillis: number;
9
10
  }
10
11
  export interface EmailAuth {
11
12
  email: string;
@@ -24,6 +25,7 @@ export declare class RingRestClient {
24
25
  refreshToken: string | undefined;
25
26
  private hardwareIdPromise;
26
27
  private _authPromise;
28
+ private timeouts;
27
29
  private clearPreviousAuth;
28
30
  private get authPromise();
29
31
  private sessionPromise?;
@@ -44,4 +46,5 @@ export declare class RingRestClient {
44
46
  url: string;
45
47
  }): Promise<T & ExtendedResponse>;
46
48
  getCurrentAuth(): Promise<AuthTokenResponse>;
49
+ clearTimeouts(): void;
47
50
  }
@@ -44,8 +44,13 @@ function requestWithRetry(requestOptions) {
44
44
  return __awaiter(this, void 0, void 0, function* () {
45
45
  try {
46
46
  const options = Object.assign(Object.assign({}, defaultRequestOptions), requestOptions), { headers, body } = (yield (0, got_1.default)(options)), data = body;
47
- if (data !== null && typeof data === 'object' && headers.date) {
48
- data.responseTimestamp = new Date(headers.date).getTime();
47
+ if (data !== null && typeof data === 'object') {
48
+ if (headers.date) {
49
+ data.responseTimestamp = new Date(headers.date).getTime();
50
+ }
51
+ if (headers['x-time-millis']) {
52
+ data.timeMillis = Number(headers['x-time-millis']);
53
+ }
49
54
  }
50
55
  return data;
51
56
  }
@@ -66,12 +71,15 @@ function requestWithRetry(requestOptions) {
66
71
  class RingRestClient {
67
72
  constructor(authOptions) {
68
73
  this.authOptions = authOptions;
69
- // prettier-ignore
70
- this.refreshToken = ('refreshToken' in this.authOptions ? this.authOptions.refreshToken : undefined);
71
- this.hardwareIdPromise = (0, util_1.getHardwareId)(this.authOptions.systemId);
74
+ this.timeouts = [];
72
75
  this.sessionPromise = undefined;
73
76
  this.using2fa = false;
74
77
  this.onRefreshTokenUpdated = new rxjs_1.ReplaySubject(1);
78
+ this.refreshToken =
79
+ 'refreshToken' in this.authOptions
80
+ ? this.authOptions.refreshToken
81
+ : undefined;
82
+ this.hardwareIdPromise = (0, util_1.getHardwareId)(this.authOptions.systemId);
75
83
  }
76
84
  clearPreviousAuth() {
77
85
  this._authPromise = undefined;
@@ -83,11 +91,12 @@ class RingRestClient {
83
91
  authPromise
84
92
  .then(({ expires_in }) => {
85
93
  // clear the existing auth promise 1 minute before it expires
86
- setTimeout(() => {
94
+ const timeout = setTimeout(() => {
87
95
  if (this._authPromise === authPromise) {
88
96
  this.clearPreviousAuth();
89
97
  }
90
98
  }, ((expires_in || 3600) - 60) * 1000);
99
+ this.timeouts.push(timeout);
91
100
  })
92
101
  .catch(() => {
93
102
  // ignore these errors here, they should be handled by the function making a rest request
@@ -288,5 +297,8 @@ class RingRestClient {
288
297
  getCurrentAuth() {
289
298
  return this.authPromise;
290
299
  }
300
+ clearTimeouts() {
301
+ this.timeouts.forEach(clearTimeout);
302
+ }
291
303
  }
292
304
  exports.RingRestClient = RingRestClient;