homebridge-ring 11.0.2-beta.0 → 11.0.3

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,12 +2,29 @@
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
- ### [11.0.2-beta.0](https://github.com/dgreif/ring/compare/v11.0.1...v11.0.2-beta.0) (2022-05-21)
5
+ ### [11.0.3](https://github.com/dgreif/ring/compare/v11.0.2...v11.0.3) (2022-05-28)
6
6
 
7
7
 
8
8
  ### Bug Fixes
9
9
 
10
+ * automatically activate streams and allow key frame requests ([b129f29](https://github.com/dgreif/ring/commit/b129f29d0a3774d779e9c159fe381e4e09066c0d))
11
+ * end streaming session when connection ends ([b6bf08b](https://github.com/dgreif/ring/commit/b6bf08ba01e344faf59f6b42ea0c3b5ce384e0bf))
12
+ * **homebridge:** allow ffmpeg in config schema ([#946](https://github.com/dgreif/ring/issues/946)) ([3242bbd](https://github.com/dgreif/ring/commit/3242bbd34dd6847500839396a9e10509e8216023))
13
+ * remove race condition for sending messages to ring edge connections ([a9438d3](https://github.com/dgreif/ring/commit/a9438d3dfc5e7ad8b96881b689534c84fab637f1))
14
+ * start ring edge pings as soon as session is started ([04ad614](https://github.com/dgreif/ring/commit/04ad614cb25c9aa4b11459e5fecde9ff7766a32d))
15
+ * update dependencies including ffmpeg ([f5cd553](https://github.com/dgreif/ring/commit/f5cd553506cf0ea31a309d1350eff19c48552809))
16
+ * use random client id for every ring edge connection ([3ba78e3](https://github.com/dgreif/ring/commit/3ba78e39b9da392779005d7d76828ddd2e425e6b))
17
+
18
+ ### [11.0.2](https://github.com/dgreif/ring/compare/v11.0.1...v11.0.2) (2022-05-22)
19
+
20
+
21
+ ### Bug Fixes
22
+
23
+ * **api:** prevent duplicate events from `onCallEnded` ([14ee6eb](https://github.com/dgreif/ring/commit/14ee6eb90bb6ae568dcfd9e8e176c3001e422f9c)), closes [#941](https://github.com/dgreif/ring/issues/941)
10
24
  * handle pcmu audio streams for incoming audio ([06f6ff1](https://github.com/dgreif/ring/commit/06f6ff10ce7c8f2b05b1a12b8f244daa200fa9ab)), closes [#930](https://github.com/dgreif/ring/issues/930)
25
+ * handle pcmu return audio ([7b86182](https://github.com/dgreif/ring/commit/7b8618261a6ccb85b9f760ee1fcc228be00fe5a6)), closes [#930](https://github.com/dgreif/ring/issues/930)
26
+ * **homebride:** use shared engines config to enforce node 16 requirement ([b748cb0](https://github.com/dgreif/ring/commit/b748cb027db0c43ae109e1ea77c6bc83f86e4913))
27
+ * prevent hanging promises from streams that fail to connect ([dd7638e](https://github.com/dgreif/ring/commit/dd7638e32626499c0be501be6b19b5d874fa974f))
11
28
 
12
29
  ### [11.0.1](https://github.com/dgreif/ring/compare/v11.0.0...v11.0.1) (2022-05-17)
13
30
 
@@ -62,6 +62,11 @@
62
62
  "type": "boolean",
63
63
  "description": "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."
64
64
  },
65
+ "ffmpegPath": {
66
+ "title": "Custom FFmpeg Path",
67
+ "type": "string",
68
+ "description": "A custom path to the ffmpeg executable. By default, the static binaries built in ffmpeg-for-homebridge will be used. If you prefer to use your own version of ffmpeg, you can pass a complete path, or simply [ ffmpeg ] to use ffmpeg from your PATH."
69
+ },
65
70
  "nightModeBypassFor": {
66
71
  "title": "Night Mode as Bypass For Mode",
67
72
  "type": "string",
@@ -1,5 +1,6 @@
1
1
  import { ConnectionState, MediaStreamTrack, RTCIceCandidate, RtcpPacket, RtpPacket } from 'werift';
2
2
  import { Observable, ReplaySubject, Subject } from 'rxjs';
3
+ import { Subscribed } from '../subscribed';
3
4
  export interface BasicPeerConnection {
4
5
  createOffer(): Promise<{
5
6
  sdp: string;
@@ -16,8 +17,9 @@ export interface BasicPeerConnection {
16
17
  onIceCandidate: Observable<RTCIceCandidate>;
17
18
  onConnectionState: Observable<ConnectionState>;
18
19
  close(): void;
20
+ requestKeyFrame?: () => void;
19
21
  }
20
- export declare class WeriftPeerConnection implements BasicPeerConnection {
22
+ export declare class WeriftPeerConnection extends Subscribed implements BasicPeerConnection {
21
23
  private pc;
22
24
  onAudioRtp: Subject<RtpPacket>;
23
25
  onAudioRtcp: Subject<RtcpPacket>;
@@ -26,6 +28,7 @@ export declare class WeriftPeerConnection implements BasicPeerConnection {
26
28
  onIceCandidate: Subject<RTCIceCandidate>;
27
29
  onConnectionState: ReplaySubject<"failed" | "closed" | "connected" | "connecting" | "new" | "disconnected">;
28
30
  returnAudioTrack: MediaStreamTrack;
31
+ private onRequestKeyFrame;
29
32
  constructor();
30
33
  createOffer(): Promise<import("werift").RTCSessionDescription>;
31
34
  createAnswer(offer: {
@@ -37,5 +40,6 @@ export declare class WeriftPeerConnection implements BasicPeerConnection {
37
40
  sdp: string;
38
41
  }): Promise<void>;
39
42
  addIceCandidate(candidate: RTCIceCandidate): Promise<void>;
43
+ requestKeyFrame(): void;
40
44
  close(): void;
41
45
  }
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WeriftPeerConnection = void 0;
4
+ /* eslint-disable brace-style */
4
5
  const werift_1 = require("werift");
5
6
  const rxjs_1 = require("rxjs");
6
7
  const util_1 = require("../util");
8
+ const subscribed_1 = require("../subscribed");
7
9
  const debug = false, ringIceServers = [
8
10
  'stun:stun.kinesisvideo.us-east-1.amazonaws.com:443',
9
11
  'stun:stun.kinesisvideo.us-east-2.amazonaws.com:443',
@@ -14,8 +16,9 @@ const debug = false, ringIceServers = [
14
16
  'stun:stun3.l.google.com:19302',
15
17
  'stun:stun4.l.google.com:19302',
16
18
  ];
17
- class WeriftPeerConnection {
19
+ class WeriftPeerConnection extends subscribed_1.Subscribed {
18
20
  constructor() {
21
+ super();
19
22
  this.onAudioRtp = new rxjs_1.Subject();
20
23
  this.onAudioRtcp = new rxjs_1.Subject();
21
24
  this.onVideoRtp = new rxjs_1.Subject();
@@ -23,6 +26,7 @@ class WeriftPeerConnection {
23
26
  this.onIceCandidate = new rxjs_1.Subject();
24
27
  this.onConnectionState = new rxjs_1.ReplaySubject(1);
25
28
  this.returnAudioTrack = new werift_1.MediaStreamTrack({ kind: 'audio' });
29
+ this.onRequestKeyFrame = new rxjs_1.Subject();
26
30
  const pc = (this.pc = new werift_1.RTCPeerConnection({
27
31
  codecs: {
28
32
  audio: [
@@ -84,7 +88,12 @@ class WeriftPeerConnection {
84
88
  if (debug) {
85
89
  (0, util_1.logInfo)('received first video packet');
86
90
  }
87
- setInterval(() => videoTransceiver.receiver.sendRtcpPLI(track.ssrc), 2000);
91
+ this.addSubscriptions((0, rxjs_1.merge)(this.onRequestKeyFrame, (0, rxjs_1.interval)(4000)).subscribe(() => {
92
+ videoTransceiver.receiver
93
+ .sendRtcpPLI(track.ssrc)
94
+ .catch((e) => (0, util_1.logError)(e));
95
+ }));
96
+ this.requestKeyFrame();
88
97
  });
89
98
  });
90
99
  this.pc.onIceCandidate.subscribe((iceCandidate) => {
@@ -118,8 +127,12 @@ class WeriftPeerConnection {
118
127
  addIceCandidate(candidate) {
119
128
  return this.pc.addIceCandidate(candidate);
120
129
  }
130
+ requestKeyFrame() {
131
+ this.onRequestKeyFrame.next();
132
+ }
121
133
  close() {
122
134
  this.pc.close().catch(util_1.logError);
135
+ this.unsubscribe();
123
136
  }
124
137
  }
125
138
  exports.WeriftPeerConnection = WeriftPeerConnection;
@@ -1,11 +1,14 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.RingEdgeConnection = void 0;
4
7
  const ws_1 = require("ws");
5
8
  const rxjs_1 = require("rxjs");
6
9
  const util_1 = require("../util");
7
- const operators_1 = require("rxjs/operators");
8
10
  const streaming_connection_base_1 = require("./streaming-connection-base");
11
+ const crypto_1 = __importDefault(require("crypto"));
9
12
  // eslint-disable-next-line no-shadow
10
13
  var CloseReasonCode;
11
14
  (function (CloseReasonCode) {
@@ -23,7 +26,9 @@ class RingEdgeConnection extends streaming_connection_base_1.StreamingConnection
23
26
  headers: {
24
27
  Authorization: `Bearer ${authToken}`,
25
28
  'X-Sig-API-Version': '4.0',
26
- 'X-Sig-Client-ID': 'ring_android-aabb123',
29
+ 'X-Sig-Client-ID': `ring_android-${crypto_1.default
30
+ .randomBytes(4)
31
+ .toString('hex')}`,
27
32
  'X-Sig-Client-Info': 'Ring/3.49.0;Platform/Android;OS/7.0;Density/2.0;Device/samsung-SM-T710;Locale/en-US;TimeZone/GMT-07:00',
28
33
  'X-Sig-Auth-Type': 'ring_oauth',
29
34
  },
@@ -40,12 +45,7 @@ class RingEdgeConnection extends streaming_connection_base_1.StreamingConnection
40
45
  });
41
46
  }),
42
47
  // The ring-edge session needs a ping every 5 seconds to keep the connection alive
43
- this.onCallAnswered
44
- .pipe((0, operators_1.switchMap)(() => (0, rxjs_1.interval)(5000)))
45
- .subscribe(() => {
46
- if (!this.sessionId) {
47
- return;
48
- }
48
+ (0, rxjs_1.interval)(5000).subscribe(() => {
49
49
  this.sendSessionMessage('ping');
50
50
  }), this.pc.onIceCandidate.subscribe(async (iceCandidate) => {
51
51
  await (0, rxjs_1.firstValueFrom)(this.onOfferSent);
@@ -104,6 +104,7 @@ class RingEdgeConnection extends streaming_connection_base_1.StreamingConnection
104
104
  case 'sdp':
105
105
  await this.pc.acceptAnswer(message.body);
106
106
  this.onCallAnswered.next(message.body.sdp);
107
+ this.activate();
107
108
  return;
108
109
  case 'ice':
109
110
  await this.pc.addIceCandidate({
@@ -123,22 +124,25 @@ class RingEdgeConnection extends streaming_connection_base_1.StreamingConnection
123
124
  (0, util_1.logError)(message);
124
125
  }
125
126
  sendSessionMessage(method, body = {}) {
126
- const message = {
127
- method,
128
- body: {
129
- ...body,
130
- doorbot_id: this.camera.id,
131
- session_id: this.sessionId,
132
- },
127
+ const sendSessionMessage = () => {
128
+ const message = {
129
+ method,
130
+ body: {
131
+ ...body,
132
+ doorbot_id: this.camera.id,
133
+ session_id: this.sessionId,
134
+ },
135
+ };
136
+ this.sendMessage(message);
133
137
  };
134
138
  if (this.sessionId) {
135
139
  // Send immediately if we already have a session id
136
140
  // This is needed to send `close` before closing the websocket
137
- this.sendMessage(message);
141
+ sendSessionMessage();
138
142
  }
139
143
  else {
140
144
  (0, rxjs_1.firstValueFrom)(this.onSessionId)
141
- .then(() => this.sendMessage(message))
145
+ .then(sendSessionMessage)
142
146
  .catch((e) => (0, util_1.logError)(e));
143
147
  }
144
148
  }
@@ -11,6 +11,9 @@ export declare abstract class StreamingConnectionBase extends Subscribed {
11
11
  protected options: StreamingConnectionOptions;
12
12
  readonly onCallAnswered: ReplaySubject<string>;
13
13
  readonly onCallEnded: ReplaySubject<void>;
14
+ readonly onMessage: ReplaySubject<{
15
+ method: string;
16
+ }>;
14
17
  readonly onWsOpen: import("rxjs").Observable<unknown>;
15
18
  protected readonly pc: BasicPeerConnection;
16
19
  readonly onAudioRtp: Subject<RtpPacket>;
@@ -20,11 +23,12 @@ export declare abstract class StreamingConnectionBase extends Subscribed {
20
23
  method: string;
21
24
  }): Promise<void>;
22
25
  protected abstract sendSessionMessage(method: string, body?: Record<any, any>): void;
23
- private activated;
24
- activate(): Promise<void>;
25
- activateCameraSpeaker(): Promise<void>;
26
+ protected activate(): void;
27
+ activateCameraSpeaker(): void;
26
28
  protected sendMessage(message: Record<any, any>): void;
27
29
  sendAudioPacket(rtp: RtpPacket): void;
30
+ private hasEnded;
28
31
  protected callEnded(): void;
29
32
  stop(): void;
33
+ requestKeyFrame(): void;
30
34
  }
@@ -13,7 +13,8 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
13
13
  this.options = options;
14
14
  this.onCallAnswered = new rxjs_1.ReplaySubject(1);
15
15
  this.onCallEnded = new rxjs_1.ReplaySubject(1);
16
- this.activated = false;
16
+ this.onMessage = new rxjs_1.ReplaySubject();
17
+ this.hasEnded = false;
17
18
  if (options.createPeerConnection) {
18
19
  // we were passed a custom peer connection factory
19
20
  this.pc = options.createPeerConnection();
@@ -31,8 +32,10 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
31
32
  this.onWsOpen = (0, rxjs_1.fromEvent)(this.ws, 'open');
32
33
  const onMessage = (0, rxjs_1.fromEvent)(this.ws, 'message'), onError = (0, rxjs_1.fromEvent)(this.ws, 'error'), onClose = (0, rxjs_1.fromEvent)(this.ws, 'close');
33
34
  this.addSubscriptions(onMessage
34
- .pipe((0, operators_1.concatMap)((message) => {
35
- return this.handleMessage(JSON.parse(message.data));
35
+ .pipe((0, operators_1.concatMap)((event) => {
36
+ const message = JSON.parse(event.data);
37
+ this.onMessage.next(message);
38
+ return this.handleMessage(message);
36
39
  }))
37
40
  .subscribe(), onError.subscribe((e) => {
38
41
  (0, util_1.logError)(e);
@@ -50,25 +53,37 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
50
53
  }
51
54
  }));
52
55
  }
53
- async activate() {
54
- if (this.activated) {
55
- return;
56
- }
57
- this.activated = true;
58
- await (0, rxjs_1.firstValueFrom)(this.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')));
56
+ activate() {
59
57
  (0, util_1.logInfo)('Activating Session');
58
+ // the activate_session message is required to keep the stream alive longer than 70 seconds
60
59
  this.sendSessionMessage('activate_session');
60
+ this.sendSessionMessage('stream_options', {
61
+ audio_enabled: true,
62
+ video_enabled: true,
63
+ });
61
64
  }
62
- async activateCameraSpeaker() {
63
- await (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
64
- this.sendSessionMessage('camera_options', {
65
- stealth_mode: false,
65
+ activateCameraSpeaker() {
66
+ // Fire and forget this call so that callers don't get hung up waiting for answer (which might not happen)
67
+ (0, rxjs_1.firstValueFrom)(this.onCallAnswered)
68
+ .then(() => {
69
+ this.sendSessionMessage('camera_options', {
70
+ stealth_mode: false,
71
+ });
72
+ })
73
+ .catch((e) => {
74
+ (0, util_1.logError)(e);
66
75
  });
67
76
  }
68
77
  sendMessage(message) {
78
+ if (this.hasEnded) {
79
+ return;
80
+ }
69
81
  this.ws.send(JSON.stringify(message));
70
82
  }
71
83
  sendAudioPacket(rtp) {
84
+ if (this.hasEnded) {
85
+ return;
86
+ }
72
87
  if (this.pc instanceof peer_connection_1.WeriftPeerConnection) {
73
88
  this.pc.returnAudioTrack.writeRtp(rtp);
74
89
  }
@@ -77,6 +92,9 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
77
92
  }
78
93
  }
79
94
  callEnded() {
95
+ if (this.hasEnded) {
96
+ return;
97
+ }
80
98
  try {
81
99
  this.sendMessage({
82
100
  reason: { code: 0, text: '' },
@@ -87,6 +105,7 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
87
105
  catch (_) {
88
106
  // ignore any errors since we are stopping the call
89
107
  }
108
+ this.hasEnded = true;
90
109
  this.unsubscribe();
91
110
  this.onCallEnded.next();
92
111
  this.pc.close();
@@ -94,5 +113,8 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
94
113
  stop() {
95
114
  this.callEnded();
96
115
  }
116
+ requestKeyFrame() {
117
+ this.pc.requestKeyFrame?.();
118
+ }
97
119
  }
98
120
  exports.StreamingConnectionBase = StreamingConnectionBase;
@@ -20,16 +20,26 @@ export declare class StreamingSession extends Subscribed {
20
20
  readonly onAudioRtp: Subject<RtpPacket>;
21
21
  private readonly audioSplitter;
22
22
  private readonly videoSplitter;
23
+ private readonly returnAudioSplitter;
23
24
  constructor(camera: RingCamera, connection: RingEdgeConnection | WebrtcConnection);
24
25
  private bindToConnection;
25
- activated: boolean;
26
- activate(): Promise<void>;
26
+ /**
27
+ * @deprecated
28
+ * activate will be removed in the future. Please use requestKeyFrame if you want to explicitly request an initial key frame
29
+ */
30
+ activate(): void;
27
31
  cameraSpeakerActivated: boolean;
28
- activateCameraSpeaker(): Promise<void>;
32
+ activateCameraSpeaker(): void;
29
33
  reservePort(bufferPorts?: number): Promise<number>;
34
+ get isUsingOpus(): Promise<boolean>;
30
35
  startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
36
+ transcodeReturnAudio(ffmpegOptions: {
37
+ input: SpawnInput[];
38
+ }): Promise<void>;
39
+ private hasEnded;
31
40
  private callEnded;
32
41
  stop(): void;
33
42
  sendAudioPacket(rtp: RtpPacket): void;
43
+ requestKeyFrame(): void;
34
44
  }
35
45
  export {};
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.StreamingSession = void 0;
4
+ const werift_1 = require("werift");
4
5
  const camera_utils_1 = require("@homebridge/camera-utils");
5
6
  const rxjs_1 = require("rxjs");
6
7
  const ffmpeg_1 = require("../ffmpeg");
@@ -26,35 +27,42 @@ class StreamingSession extends subscribed_1.Subscribed {
26
27
  this.onAudioRtp = new rxjs_1.Subject();
27
28
  this.audioSplitter = new camera_utils_1.RtpSplitter();
28
29
  this.videoSplitter = new camera_utils_1.RtpSplitter();
29
- this.activated = false;
30
+ this.returnAudioSplitter = new camera_utils_1.RtpSplitter();
30
31
  this.cameraSpeakerActivated = false;
32
+ this.hasEnded = false;
31
33
  this.bindToConnection(connection);
32
34
  }
33
35
  bindToConnection(connection) {
34
36
  this.addSubscriptions(connection.onAudioRtp.subscribe(this.onAudioRtp), connection.onVideoRtp.subscribe(this.onVideoRtp), connection.onCallAnswered.subscribe((sdp) => {
35
37
  this.onUsingOpus.next(sdp.toLocaleLowerCase().includes(' opus/'));
36
- }));
38
+ }), connection.onCallEnded.subscribe(() => this.callEnded()));
37
39
  }
38
- async activate() {
39
- if (this.activated) {
40
- return;
41
- }
42
- this.activated = true;
43
- await this.connection.activate();
40
+ /**
41
+ * @deprecated
42
+ * activate will be removed in the future. Please use requestKeyFrame if you want to explicitly request an initial key frame
43
+ */
44
+ activate() {
45
+ this.requestKeyFrame();
44
46
  }
45
- async activateCameraSpeaker() {
46
- if (this.cameraSpeakerActivated) {
47
+ activateCameraSpeaker() {
48
+ if (this.cameraSpeakerActivated || this.hasEnded) {
47
49
  return;
48
50
  }
49
51
  this.cameraSpeakerActivated = true;
50
- await this.connection.activateCameraSpeaker();
52
+ this.connection.activateCameraSpeaker();
51
53
  }
52
54
  async reservePort(bufferPorts = 0) {
53
55
  const ports = await (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
54
56
  return ports[0];
55
57
  }
58
+ get isUsingOpus() {
59
+ return (0, rxjs_1.firstValueFrom)(this.onUsingOpus);
60
+ }
56
61
  async startTranscoding(ffmpegOptions) {
57
- const videoPort = await this.reservePort(1), audioPort = await this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, ringSdp = await (0, rxjs_1.firstValueFrom)(this.connection.onCallAnswered), usingOpus = await (0, rxjs_1.firstValueFrom)(this.onUsingOpus), ffmpegInputArguments = [
62
+ if (this.hasEnded) {
63
+ return;
64
+ }
65
+ const videoPort = await this.reservePort(1), audioPort = await this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, ringSdp = await (0, rxjs_1.firstValueFrom)(this.connection.onCallAnswered), usingOpus = await this.isUsingOpus, ffmpegInputArguments = [
58
66
  '-hide_banner',
59
67
  '-protocol_whitelist',
60
68
  'pipe,udp,rtp,file,crypto',
@@ -97,21 +105,68 @@ class StreamingSession extends subscribed_1.Subscribed {
97
105
  }
98
106
  this.onCallEnded.subscribe(() => ff.stop());
99
107
  ff.writeStdin(inputSdp);
100
- // Activate the stream now that ffmpeg is ready to receive
101
- await this.activate();
108
+ // Request a key frame now that ffmpeg is ready to receive
109
+ this.requestKeyFrame();
110
+ }
111
+ async transcodeReturnAudio(ffmpegOptions) {
112
+ if (this.hasEnded) {
113
+ return;
114
+ }
115
+ const audioOutForwarder = new camera_utils_1.RtpSplitter(({ message }) => {
116
+ const rtp = werift_1.RtpPacket.deSerialize(message);
117
+ this.connection.sendAudioPacket(rtp);
118
+ return null;
119
+ }), usingOpus = await this.isUsingOpus, ff = new camera_utils_1.FfmpegProcess({
120
+ ffmpegArgs: [
121
+ '-hide_banner',
122
+ '-protocol_whitelist',
123
+ 'pipe,udp,rtp,file,crypto',
124
+ '-re',
125
+ '-i',
126
+ ...ffmpegOptions.input,
127
+ '-acodec',
128
+ ...(usingOpus
129
+ ? ['libopus', '-ac', 2, '-ar', '48k']
130
+ : ['pcm_mulaw', '-ac', 1, '-ar', '8k']),
131
+ '-flags',
132
+ '+global_header',
133
+ '-f',
134
+ 'rtp',
135
+ `rtp://127.0.0.1:${await audioOutForwarder.portPromise}`,
136
+ ],
137
+ ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
138
+ exitCallback: () => this.callEnded(),
139
+ logLabel: `Return Audio (${this.camera.name})`,
140
+ logger: {
141
+ error: util_1.logError,
142
+ info: util_1.logDebug,
143
+ },
144
+ });
145
+ this.onCallEnded.subscribe(() => ff.stop());
102
146
  }
103
147
  callEnded() {
148
+ if (this.hasEnded) {
149
+ return;
150
+ }
151
+ this.hasEnded = true;
104
152
  this.unsubscribe();
105
153
  this.onCallEnded.next();
106
154
  this.connection.stop();
107
155
  this.audioSplitter.close();
108
156
  this.videoSplitter.close();
157
+ this.returnAudioSplitter.close();
109
158
  }
110
159
  stop() {
111
160
  this.callEnded();
112
161
  }
113
162
  sendAudioPacket(rtp) {
163
+ if (this.hasEnded) {
164
+ return;
165
+ }
114
166
  this.connection.sendAudioPacket(rtp);
115
167
  }
168
+ requestKeyFrame() {
169
+ this.connection.requestKeyFrame();
170
+ }
116
171
  }
117
172
  exports.StreamingSession = StreamingSession;
@@ -29,10 +29,7 @@ class WebrtcConnection extends streaming_connection_base_1.StreamingConnectionBa
29
29
  const answer = await this.pc.createAnswer(message);
30
30
  this.sendSessionMessage('sdp', answer);
31
31
  this.onCallAnswered.next(message.sdp);
32
- this.sendSessionMessage('stream_options', {
33
- audio_enabled: true,
34
- video_enabled: true,
35
- });
32
+ this.activate();
36
33
  return;
37
34
  case 'ice':
38
35
  await this.pc.addIceCandidate({
@@ -45,7 +45,7 @@ class BaseAccessory {
45
45
  // Only register for GET if an async request should be made to get an updated value
46
46
  onCachedValue.pipe((0, operators_1.take)(1)).subscribe(() => {
47
47
  // allow GET once a value is cached
48
- characteristic.on("get" /* GET */, async (callback) => {
48
+ characteristic.on("get" /* CharacteristicEventTypes.GET */, async (callback) => {
49
49
  try {
50
50
  const value = await (0, rxjs_1.firstValueFrom)(onCachedValue);
51
51
  callback(null, value);
@@ -58,7 +58,7 @@ class BaseAccessory {
58
58
  });
59
59
  }
60
60
  if (setValue) {
61
- characteristic.on("set" /* SET */, (newValue, callback) => {
61
+ characteristic.on("set" /* CharacteristicEventTypes.SET */, (newValue, callback) => {
62
62
  Promise.resolve(setValue(newValue)).catch((e) => {
63
63
  this.logger.error(e);
64
64
  });
@@ -9,7 +9,7 @@ class BaseDataAccessory extends base_accessory_1.BaseAccessory {
9
9
  const service = this.getService(serviceType, name, serviceSubType), characteristic = service.getCharacteristic(characteristicType), { device } = this;
10
10
  if (requestUpdate) {
11
11
  // Only register for GET if an async request should be made to get an updated value
12
- characteristic.on("get" /* GET */, (callback) => {
12
+ characteristic.on("get" /* CharacteristicEventTypes.GET */, (callback) => {
13
13
  try {
14
14
  const value = getValue(device.data);
15
15
  callback(null, value);
@@ -22,14 +22,14 @@ class BaseDataAccessory extends base_accessory_1.BaseAccessory {
22
22
  }
23
23
  if (setValue && setValueDebounceTime) {
24
24
  const onValueToSet = new rxjs_1.Subject();
25
- characteristic.on("set" /* SET */, (newValue, callback) => {
25
+ characteristic.on("set" /* CharacteristicEventTypes.SET */, (newValue, callback) => {
26
26
  onValueToSet.next(newValue);
27
27
  callback();
28
28
  });
29
29
  onValueToSet.pipe((0, operators_1.debounceTime)(setValueDebounceTime)).subscribe(setValue);
30
30
  }
31
31
  else if (setValue) {
32
- characteristic.on("set" /* SET */, (newValue, callback) => {
32
+ characteristic.on("set" /* CharacteristicEventTypes.SET */, (newValue, callback) => {
33
33
  Promise.resolve(setValue(newValue)).catch((e) => {
34
34
  this.logger.error(e);
35
35
  });
@@ -26,8 +26,8 @@ function getSessionConfig(srtpOptions) {
26
26
  };
27
27
  }
28
28
  class StreamingSessionWrapper {
29
- constructor(liveCall, prepareStreamRequest, ringCamera, start) {
30
- this.liveCall = liveCall;
29
+ constructor(streamingSession, prepareStreamRequest, ringCamera, start) {
30
+ this.streamingSession = streamingSession;
31
31
  this.prepareStreamRequest = prepareStreamRequest;
32
32
  this.ringCamera = ringCamera;
33
33
  this.start = start;
@@ -63,15 +63,15 @@ class StreamingSessionWrapper {
63
63
  onReturnPacketReceived.next(null);
64
64
  return null;
65
65
  });
66
- liveCall.addSubscriptions((0, rxjs_1.merge)((0, rxjs_1.of)(true).pipe((0, operators_1.delay)(15000)), onReturnPacketReceived)
66
+ streamingSession.addSubscriptions((0, rxjs_1.merge)((0, rxjs_1.of)(true).pipe((0, operators_1.delay)(15000)), onReturnPacketReceived)
67
67
  .pipe((0, operators_1.debounceTime)(5000))
68
68
  .subscribe(() => {
69
69
  (0, util_1.logInfo)(`Live stream for ${this.ringCamera.name} appears to be inactive. (${getDurationSeconds(start)}s)`);
70
- liveCall.stop();
70
+ streamingSession.stop();
71
71
  }));
72
72
  // Periodically send a blank RTCP packet to the HomeKit video port
73
73
  // Without this, HomeKit assumes the stream is dead after 30 second and sends a stop request
74
- liveCall.addSubscriptions((0, rxjs_1.interval)(500).subscribe(() => {
74
+ streamingSession.addSubscriptions((0, rxjs_1.interval)(500).subscribe(() => {
75
75
  const senderInfo = new werift_1.RtcpSenderInfo({
76
76
  ntpTimestamp: BigInt(0),
77
77
  packetCount: 0,
@@ -95,7 +95,7 @@ class StreamingSessionWrapper {
95
95
  // use to encrypt Ring video to HomeKit
96
96
  videoSrtpSession = new werift_1.SrtpSession(getSessionConfig(this.videoSrtp));
97
97
  // Set up packet forwarding for video stream
98
- this.liveCall.addSubscriptions(this.liveCall.onVideoRtp.subscribe(({ header, payload }) => {
98
+ this.streamingSession.addSubscriptions(this.streamingSession.onVideoRtp.subscribe(({ header, payload }) => {
99
99
  header.ssrc = this.videoSsrc;
100
100
  header.payloadType = request.video.pt;
101
101
  const encryptedPacket = videoSrtpSession.encrypt(payload, header);
@@ -112,9 +112,9 @@ class StreamingSessionWrapper {
112
112
  }));
113
113
  const shouldTranscodeAudio = await this.libfdkAacInstalledPromise;
114
114
  if (!shouldTranscodeAudio) {
115
- return this.liveCall.activate();
115
+ return this.streamingSession.requestKeyFrame();
116
116
  }
117
- const transcodingPromise = this.liveCall.startTranscoding({
117
+ const transcodingPromise = this.streamingSession.startTranscoding({
118
118
  input: ['-vn'],
119
119
  audio: [
120
120
  '-map',
@@ -167,20 +167,20 @@ class StreamingSessionWrapper {
167
167
  }, audioSrtpSession = new werift_1.SrtpSession(getSessionConfig(remoteAudioSrtpOptions)), returnAudioTranscodedSplitter = new camera_utils_1.RtpSplitter(({ message }) => {
168
168
  if (!cameraSpeakerActive) {
169
169
  cameraSpeakerActive = true;
170
- this.liveCall.activateCameraSpeaker().catch(util_1.logError);
170
+ this.streamingSession.activateCameraSpeaker();
171
171
  }
172
172
  // decrypt the message
173
173
  try {
174
174
  const rtp = werift_1.RtpPacket.deSerialize(message);
175
175
  rtp.payload = audioSrtpSession.decrypt(rtp.payload);
176
176
  // send to Ring - werift will handle encryption and other header params
177
- this.liveCall.sendAudioPacket(rtp);
177
+ this.streamingSession.sendAudioPacket(rtp);
178
178
  }
179
179
  catch (_) {
180
180
  // deSerialize will sometimes fail, but the errors can be ignored
181
181
  }
182
182
  return null;
183
- }), returnAudioTranscoder = new camera_utils_1.ReturnAudioTranscoder({
183
+ }), usingOpus = await this.streamingSession.isUsingOpus, returnAudioTranscoder = new camera_utils_1.ReturnAudioTranscoder({
184
184
  prepareStreamRequest: this.prepareStreamRequest,
185
185
  incomingAudioOptions: {
186
186
  ssrc: this.audioSsrc,
@@ -188,13 +188,11 @@ class StreamingSessionWrapper {
188
188
  },
189
189
  outputArgs: [
190
190
  '-acodec',
191
- 'libopus',
191
+ ...(usingOpus
192
+ ? ['libopus', '-ac', 2, '-ar', '48k']
193
+ : ['pcm_mulaw', '-ac', 1, '-ar', '8k']),
192
194
  '-flags',
193
195
  '+global_header',
194
- '-ac',
195
- 2,
196
- '-ar',
197
- '48k',
198
196
  '-f',
199
197
  'rtp',
200
198
  `rtp://127.0.0.1:${await returnAudioTranscodedSplitter.portPromise}`,
@@ -207,7 +205,7 @@ class StreamingSessionWrapper {
207
205
  logLabel: `Return Audio (${this.ringCamera.name})`,
208
206
  returnAudioSplitter: this.audioSplitter,
209
207
  });
210
- this.liveCall.onCallEnded.pipe((0, operators_1.take)(1)).subscribe(() => {
208
+ this.streamingSession.onCallEnded.pipe((0, operators_1.take)(1)).subscribe(() => {
211
209
  returnAudioTranscoder.stop();
212
210
  returnAudioTranscodedSplitter.close();
213
211
  });
@@ -217,7 +215,7 @@ class StreamingSessionWrapper {
217
215
  stop() {
218
216
  this.audioSplitter.close();
219
217
  this.videoSplitter.close();
220
- this.liveCall.stop();
218
+ this.streamingSession.stop();
221
219
  }
222
220
  }
223
221
  class CameraSource {
@@ -227,7 +225,7 @@ class CameraSource {
227
225
  cameraStreamCount: 10,
228
226
  delegate: this,
229
227
  streamingOptions: {
230
- supportedCryptoSuites: [0 /* AES_CM_128_HMAC_SHA1_80 */],
228
+ supportedCryptoSuites: [0 /* SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80 */],
231
229
  video: {
232
230
  resolutions: [
233
231
  [1280, 720, 30],
@@ -241,15 +239,15 @@ class CameraSource {
241
239
  [320, 180, 30],
242
240
  ],
243
241
  codec: {
244
- profiles: [0 /* BASELINE */],
245
- levels: [0 /* LEVEL3_1 */],
242
+ profiles: [0 /* H264Profile.BASELINE */],
243
+ levels: [0 /* H264Level.LEVEL3_1 */],
246
244
  },
247
245
  },
248
246
  audio: {
249
247
  codecs: [
250
248
  {
251
- type: "AAC-eld" /* AAC_ELD */,
252
- samplerate: 16 /* KHZ_16 */,
249
+ type: "AAC-eld" /* AudioStreamingCodecType.AAC_ELD */,
250
+ samplerate: 16 /* AudioStreamingSamplerate.KHZ_16 */,
253
251
  },
254
252
  ],
255
253
  },
@@ -40,7 +40,7 @@ class LocationModeSwitch extends base_accessory_1.BaseAccessory {
40
40
  }
41
41
  currentState.updateValue(state);
42
42
  });
43
- currentState.on("get" /* GET */, async (callback) => {
43
+ currentState.on("get" /* CharacteristicEventTypes.GET */, async (callback) => {
44
44
  location.getLocationMode().catch((e) => {
45
45
  (0, util_1.logError)('Failed to retrieve location mode for ' + location.name);
46
46
  (0, util_1.logError)(e);
@@ -51,12 +51,12 @@ class LocationModeSwitch extends base_accessory_1.BaseAccessory {
51
51
  }
52
52
  callback(null, state);
53
53
  });
54
- targetState.on("get" /* GET */, async (callback) => {
54
+ targetState.on("get" /* CharacteristicEventTypes.GET */, async (callback) => {
55
55
  callback(null, this.targetState !== undefined
56
56
  ? this.targetState
57
57
  : await getCurrentState());
58
58
  });
59
- targetState.on("set" /* SET */, async (state, callback) => {
59
+ targetState.on("set" /* CharacteristicEventTypes.SET */, async (state, callback) => {
60
60
  const { Characteristic: { SecuritySystemTargetState: State }, } = hap_1.hap;
61
61
  callback();
62
62
  if (state === State.NIGHT_ARM) {
@@ -217,8 +217,8 @@ class RingPlatform {
217
217
  }
218
218
  const createHomebridgeAccessory = () => {
219
219
  const accessory = new api.platformAccessory(displayName, uuid, isCamera
220
- ? 17 /* CAMERA */
221
- : 11 /* SECURITY_SYSTEM */);
220
+ ? 17 /* hap.Categories.CAMERA */
221
+ : 11 /* hap.Categories.SECURITY_SYSTEM */);
222
222
  this.log.info(`Adding new accessory ${uuid} ${deviceType} ${displayName}`);
223
223
  platformAccessories.push(accessory);
224
224
  if (isCamera &&
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-ring",
3
- "version": "11.0.2-beta.0",
3
+ "version": "11.0.3",
4
4
  "description": "Homebridge plugin for Ring doorbells, cameras, security alarm system and smart lighting",
5
5
  "main": "lib/homebridge/index.js",
6
6
  "bin": {
@@ -40,9 +40,9 @@
40
40
  "license": "MIT",
41
41
  "dependencies": {
42
42
  "@eneris/push-receiver": "^3.0.1",
43
- "@homebridge/camera-utils": "^2.1.0",
43
+ "@homebridge/camera-utils": "^2.1.1",
44
44
  "@homebridge/plugin-ui-utils": "^0.0.19",
45
- "werift": "^0.15.1",
45
+ "werift": "^0.15.3",
46
46
  "@types/socket.io-client": "1.4.36",
47
47
  "colors": "^1.4.0",
48
48
  "debug": "^4.3.4",
@@ -51,24 +51,24 @@
51
51
  "socket.io-client": "^2.4.0",
52
52
  "systeminformation": "^5.11.15",
53
53
  "uuid": "^8.3.2",
54
- "ws": "^8.6.0"
54
+ "ws": "^8.7.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@swc-node/register": "^1.5.1",
58
58
  "@types/debug": "4.1.7",
59
59
  "@types/jest": "27.5.1",
60
- "@types/node": "17.0.34",
60
+ "@types/node": "17.0.36",
61
61
  "@types/uuid": "8.3.4",
62
62
  "@types/ws": "^8.5.3",
63
- "@typescript-eslint/eslint-plugin": "5.25.0",
64
- "@typescript-eslint/parser": "5.25.0",
65
- "concurrently": "^7.2.0",
63
+ "@typescript-eslint/eslint-plugin": "5.26.0",
64
+ "@typescript-eslint/parser": "5.26.0",
65
+ "concurrently": "^7.2.1",
66
66
  "conventional-github-releaser": "3.1.5",
67
67
  "dotenv": "16.0.1",
68
- "esbuild": "^0.14.39",
69
- "eslint": "8.15.0",
68
+ "esbuild": "^0.14.41",
69
+ "eslint": "8.16.0",
70
70
  "eslint-config-prettier": "8.5.0",
71
- "eslint-plugin-jest": "26.2.2",
71
+ "eslint-plugin-jest": "26.4.2",
72
72
  "eslint-plugin-prettier": "4.0.0",
73
73
  "express": "4.18.1",
74
74
  "homebridge": "1.4.1",
@@ -77,12 +77,12 @@
77
77
  "reflect-metadata": "0.1.13",
78
78
  "rimraf": "3.0.2",
79
79
  "standard-version": "9.5.0",
80
- "ts-jest": "28.0.2",
81
- "typescript": "4.6.4"
80
+ "ts-jest": "28.0.3",
81
+ "typescript": "4.7.2"
82
82
  },
83
83
  "engines": {
84
- "node": ">=8.3.0",
85
- "homebridge": ">=0.4.0"
84
+ "node": "^16",
85
+ "homebridge": ">=1.4.0"
86
86
  },
87
87
  "bugs": {
88
88
  "url": "https://github.com/dgreif/ring/issues"