homebridge-ring 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 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
 
@@ -4,4 +4,3 @@ export * from './ring-camera';
4
4
  export * from './ring-chime';
5
5
  export * from './ring-device';
6
6
  export * from './ring-types';
7
- export * from './sip-session';
package/lib/api/index.js CHANGED
@@ -16,4 +16,3 @@ __exportStar(require("./ring-camera"), exports);
16
16
  __exportStar(require("./ring-chime"), exports);
17
17
  __exportStar(require("./ring-device"), exports);
18
18
  __exportStar(require("./ring-types"), exports);
19
- __exportStar(require("./sip-session"), exports);
@@ -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;
@@ -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
- startVideoOnDemand(): Promise<("" | ActiveDing) & import("./rest-client").ExtendedResponse>;
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<SipSession>;
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>;
@@ -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
- startVideoOnDemand() {
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 vodPromise = (0, rxjs_1.firstValueFrom)(this.onNewDing), videoOnDemandDing = yield this.startVideoOnDemand();
261
- if (videoOnDemandDing && 'sip_from' in videoOnDemandDing) {
262
- // wired cams return a ding from live_view so we don't need to wait
263
- return videoOnDemandDing;
264
- }
265
- // battery cams return '' from live_view so we need to request active dings and wait
266
- this.pollForActiveDing();
267
- return vodPromise;
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 sipSession = yield this.streamVideo({
383
+ const liveCall = yield this.streamVideo({
443
384
  output: ['-t', duration.toString(), outputPath],
444
385
  });
445
- yield (0, rxjs_1.firstValueFrom)(sipSession.onCallEnded);
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 sipSession = yield this.createSipSession();
451
- yield sipSession.start(ffmpegOptions);
452
- return sipSession;
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
@@ -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;
@@ -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',