homebridge-ring 11.0.0 → 11.0.2-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,28 @@
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.1](https://github.com/dgreif/ring/compare/v11.0.2-beta.0...v11.0.2-beta.1) (2022-05-21)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **api:** prevent duplicate events from `onCallEnded` ([75696d5](https://github.com/dgreif/ring/commit/75696d510969f706248a0f83e98fb1c99fc07213)), closes [#941](https://github.com/dgreif/ring/issues/941)
11
+ * handle pcmu return audio ([ce29fb5](https://github.com/dgreif/ring/commit/ce29fb5638bc488c515518a0fadcaca900ebe750)), closes [#930](https://github.com/dgreif/ring/issues/930)
12
+
13
+ ### [11.0.2-beta.0](https://github.com/dgreif/ring/compare/v11.0.1...v11.0.2-beta.0) (2022-05-21)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * handle pcmu audio streams for incoming audio ([06f6ff1](https://github.com/dgreif/ring/commit/06f6ff10ce7c8f2b05b1a12b8f244daa200fa9ab)), closes [#930](https://github.com/dgreif/ring/issues/930)
19
+
20
+ ### [11.0.1](https://github.com/dgreif/ring/compare/v11.0.0...v11.0.1) (2022-05-17)
21
+
22
+
23
+ ### Bug Fixes
24
+
25
+ * update webrtc package ([63d003c](https://github.com/dgreif/ring/commit/63d003c1517c158663d230e6daf911c7fc765c55)), closes [#930](https://github.com/dgreif/ring/issues/930)
26
+
5
27
  ## [11.0.0](https://github.com/dgreif/ring/compare/v10.0.0...v11.0.0) (2022-05-08)
6
28
 
7
29
 
@@ -1,4 +1,4 @@
1
- import { ConnectionState, MediaStreamTrack, RTCIceCandidate, RtcpPacket, RtpPacket } from '@koush/werift';
1
+ import { ConnectionState, MediaStreamTrack, RTCIceCandidate, RtcpPacket, RtpPacket } from 'werift';
2
2
  import { Observable, ReplaySubject, Subject } from 'rxjs';
3
3
  export interface BasicPeerConnection {
4
4
  createOffer(): Promise<{
@@ -27,11 +27,11 @@ export declare class WeriftPeerConnection implements BasicPeerConnection {
27
27
  onConnectionState: ReplaySubject<"failed" | "closed" | "connected" | "connecting" | "new" | "disconnected">;
28
28
  returnAudioTrack: MediaStreamTrack;
29
29
  constructor();
30
- createOffer(): Promise<import("@koush/werift").RTCSessionDescription>;
30
+ createOffer(): Promise<import("werift").RTCSessionDescription>;
31
31
  createAnswer(offer: {
32
32
  type: 'offer';
33
33
  sdp: string;
34
- }): Promise<import("@koush/werift").RTCSessionDescription>;
34
+ }): Promise<import("werift").RTCSessionDescription>;
35
35
  acceptAnswer(answer: {
36
36
  type: 'answer';
37
37
  sdp: string;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WeriftPeerConnection = void 0;
4
- const werift_1 = require("@koush/werift");
4
+ const werift_1 = require("werift");
5
5
  const rxjs_1 = require("rxjs");
6
6
  const util_1 = require("../util");
7
7
  const debug = false, ringIceServers = [
@@ -31,6 +31,12 @@ class WeriftPeerConnection {
31
31
  clockRate: 48000,
32
32
  channels: 2,
33
33
  }),
34
+ new werift_1.RTCRtpCodecParameters({
35
+ mimeType: 'audio/PCMU',
36
+ clockRate: 8000,
37
+ channels: 1,
38
+ payloadType: 0,
39
+ }),
34
40
  ],
35
41
  video: [
36
42
  new werift_1.RTCRtpCodecParameters({
@@ -1,7 +1,7 @@
1
1
  import { BasicPeerConnection } from './peer-connection';
2
2
  import { Subscribed } from '../subscribed';
3
3
  import { WebSocket } from 'ws';
4
- import { RtpPacket } from '@koush/werift';
4
+ import { RtpPacket } from 'werift';
5
5
  import { ReplaySubject, Subject } from 'rxjs';
6
6
  export interface StreamingConnectionOptions {
7
7
  createPeerConnection?: () => BasicPeerConnection;
@@ -1,4 +1,4 @@
1
- import { RtpPacket } from '@koush/werift';
1
+ import { RtpPacket } from 'werift';
2
2
  import { ReplaySubject, Subject } from 'rxjs';
3
3
  import { RingEdgeConnection } from './ring-edge-connection';
4
4
  import { WebrtcConnection } from './webrtc-connection';
@@ -15,10 +15,12 @@ export declare class StreamingSession extends Subscribed {
15
15
  private readonly camera;
16
16
  private connection;
17
17
  readonly onCallEnded: ReplaySubject<void>;
18
+ private readonly onUsingOpus;
18
19
  readonly onVideoRtp: Subject<RtpPacket>;
19
20
  readonly onAudioRtp: Subject<RtpPacket>;
20
21
  private readonly audioSplitter;
21
22
  private readonly videoSplitter;
23
+ private readonly returnAudioSplitter;
22
24
  constructor(camera: RingCamera, connection: RingEdgeConnection | WebrtcConnection);
23
25
  private bindToConnection;
24
26
  activated: boolean;
@@ -26,7 +28,12 @@ export declare class StreamingSession extends Subscribed {
26
28
  cameraSpeakerActivated: boolean;
27
29
  activateCameraSpeaker(): Promise<void>;
28
30
  reservePort(bufferPorts?: number): Promise<number>;
31
+ get isUsingOpus(): Promise<boolean>;
29
32
  startTranscoding(ffmpegOptions: FfmpegOptions): Promise<void>;
33
+ transcodeReturnAudio(ffmpegOptions: {
34
+ input: SpawnInput[];
35
+ }): Promise<void>;
36
+ private hasEnded;
30
37
  private callEnded;
31
38
  stop(): void;
32
39
  sendAudioPacket(rtp: RtpPacket): void;
@@ -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");
@@ -21,26 +22,31 @@ class StreamingSession extends subscribed_1.Subscribed {
21
22
  this.camera = camera;
22
23
  this.connection = connection;
23
24
  this.onCallEnded = new rxjs_1.ReplaySubject(1);
25
+ this.onUsingOpus = new rxjs_1.ReplaySubject(1);
24
26
  this.onVideoRtp = new rxjs_1.Subject();
25
27
  this.onAudioRtp = new rxjs_1.Subject();
26
28
  this.audioSplitter = new camera_utils_1.RtpSplitter();
27
29
  this.videoSplitter = new camera_utils_1.RtpSplitter();
30
+ this.returnAudioSplitter = new camera_utils_1.RtpSplitter();
28
31
  this.activated = false;
29
32
  this.cameraSpeakerActivated = false;
33
+ this.hasEnded = false;
30
34
  this.bindToConnection(connection);
31
35
  }
32
36
  bindToConnection(connection) {
33
- this.addSubscriptions(connection.onAudioRtp.subscribe(this.onAudioRtp), connection.onVideoRtp.subscribe(this.onVideoRtp));
37
+ this.addSubscriptions(connection.onAudioRtp.subscribe(this.onAudioRtp), connection.onVideoRtp.subscribe(this.onVideoRtp), connection.onCallAnswered.subscribe((sdp) => {
38
+ this.onUsingOpus.next(sdp.toLocaleLowerCase().includes(' opus/'));
39
+ }));
34
40
  }
35
41
  async activate() {
36
- if (this.activated) {
42
+ if (this.activated || this.hasEnded) {
37
43
  return;
38
44
  }
39
45
  this.activated = true;
40
46
  await this.connection.activate();
41
47
  }
42
48
  async activateCameraSpeaker() {
43
- if (this.cameraSpeakerActivated) {
49
+ if (this.cameraSpeakerActivated || this.hasEnded) {
44
50
  return;
45
51
  }
46
52
  this.cameraSpeakerActivated = true;
@@ -50,19 +56,25 @@ class StreamingSession extends subscribed_1.Subscribed {
50
56
  const ports = await (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
51
57
  return ports[0];
52
58
  }
59
+ get isUsingOpus() {
60
+ return (0, rxjs_1.firstValueFrom)(this.onUsingOpus);
61
+ }
53
62
  async startTranscoding(ffmpegOptions) {
54
- const videoPort = await this.reservePort(1), audioPort = await this.reservePort(1), transcodeVideoStream = ffmpegOptions.video !== false, ffmpegInputArguments = [
63
+ if (this.hasEnded) {
64
+ return;
65
+ }
66
+ 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 = [
55
67
  '-hide_banner',
56
68
  '-protocol_whitelist',
57
69
  'pipe,udp,rtp,file,crypto',
58
- '-acodec',
59
- 'libopus',
70
+ // Ring will answer with either opus or pcmu
71
+ ...(usingOpus ? ['-acodec', 'libopus'] : []),
60
72
  '-f',
61
73
  'sdp',
62
74
  ...(ffmpegOptions.input || []),
63
75
  '-i',
64
76
  'pipe:',
65
- ], ringSdp = await (0, rxjs_1.firstValueFrom)(this.connection.onCallAnswered), inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
77
+ ], inputSdp = getCleanSdp(ringSdp, transcodeVideoStream)
66
78
  .replace(/m=audio \d+/, `m=audio ${audioPort}`)
67
79
  .replace(/m=video \d+/, `m=video ${videoPort}`), ff = new camera_utils_1.FfmpegProcess({
68
80
  ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
@@ -97,17 +109,61 @@ class StreamingSession extends subscribed_1.Subscribed {
97
109
  // Activate the stream now that ffmpeg is ready to receive
98
110
  await this.activate();
99
111
  }
112
+ async transcodeReturnAudio(ffmpegOptions) {
113
+ if (this.hasEnded) {
114
+ return;
115
+ }
116
+ const audioOutForwarder = new camera_utils_1.RtpSplitter(({ message }) => {
117
+ const rtp = werift_1.RtpPacket.deSerialize(message);
118
+ this.connection.sendAudioPacket(rtp);
119
+ return null;
120
+ }), usingOpus = await this.isUsingOpus, ff = new camera_utils_1.FfmpegProcess({
121
+ ffmpegArgs: [
122
+ '-hide_banner',
123
+ '-protocol_whitelist',
124
+ 'pipe,udp,rtp,file,crypto',
125
+ '-re',
126
+ '-i',
127
+ ...ffmpegOptions.input,
128
+ '-acodec',
129
+ ...(usingOpus
130
+ ? ['libopus', '-ac', 2, '-ar', '48k']
131
+ : ['pcm_mulaw', '-ac', 1, '-ar', '8k']),
132
+ '-flags',
133
+ '+global_header',
134
+ '-f',
135
+ 'rtp',
136
+ `rtp://127.0.0.1:${await audioOutForwarder.portPromise}`,
137
+ ],
138
+ ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
139
+ exitCallback: () => this.callEnded(),
140
+ logLabel: `Return Audio (${this.camera.name})`,
141
+ logger: {
142
+ error: util_1.logError,
143
+ info: util_1.logDebug,
144
+ },
145
+ });
146
+ this.onCallEnded.subscribe(() => ff.stop());
147
+ }
100
148
  callEnded() {
149
+ if (this.hasEnded) {
150
+ return;
151
+ }
152
+ this.hasEnded = true;
101
153
  this.unsubscribe();
102
154
  this.onCallEnded.next();
103
155
  this.connection.stop();
104
156
  this.audioSplitter.close();
105
157
  this.videoSplitter.close();
158
+ this.returnAudioSplitter.close();
106
159
  }
107
160
  stop() {
108
161
  this.callEnded();
109
162
  }
110
163
  sendAudioPacket(rtp) {
164
+ if (this.hasEnded) {
165
+ return;
166
+ }
111
167
  this.connection.sendAudioPacket(rtp);
112
168
  }
113
169
  }
@@ -9,7 +9,7 @@ const rxjs_1 = require("rxjs");
9
9
  const fs_1 = require("fs");
10
10
  const util_2 = require("util");
11
11
  const ffmpeg_1 = require("../api/ffmpeg");
12
- const werift_1 = require("@koush/werift");
12
+ const werift_1 = require("werift");
13
13
  const readFileAsync = (0, util_2.promisify)(fs_1.readFile), cameraOfflinePath = require.resolve('../../media/camera-offline.jpg'), snapshotsBlockedPath = require.resolve('../../media/snapshots-blocked.jpg');
14
14
  function getDurationSeconds(start) {
15
15
  return (Date.now() - start) / 1000;
@@ -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.activate();
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().catch(util_1.logError);
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 {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "homebridge-ring",
3
- "version": "11.0.0",
3
+ "version": "11.0.2-beta.1",
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": {
@@ -42,33 +42,33 @@
42
42
  "@eneris/push-receiver": "^3.0.1",
43
43
  "@homebridge/camera-utils": "^2.1.0",
44
44
  "@homebridge/plugin-ui-utils": "^0.0.19",
45
- "@koush/werift": "0.14.5-beta9",
45
+ "werift": "^0.15.1",
46
46
  "@types/socket.io-client": "1.4.36",
47
47
  "colors": "^1.4.0",
48
48
  "debug": "^4.3.4",
49
49
  "got": "^11.8.3",
50
50
  "rxjs": "^7.5.5",
51
51
  "socket.io-client": "^2.4.0",
52
- "systeminformation": "^5.11.14",
52
+ "systeminformation": "^5.11.15",
53
53
  "uuid": "^8.3.2",
54
54
  "ws": "^8.6.0"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@swc-node/register": "^1.5.1",
58
58
  "@types/debug": "4.1.7",
59
- "@types/jest": "27.5.0",
60
- "@types/node": "17.0.31",
59
+ "@types/jest": "27.5.1",
60
+ "@types/node": "17.0.34",
61
61
  "@types/uuid": "8.3.4",
62
62
  "@types/ws": "^8.5.3",
63
- "@typescript-eslint/eslint-plugin": "5.22.0",
64
- "@typescript-eslint/parser": "5.22.0",
65
- "concurrently": "^7.1.0",
63
+ "@typescript-eslint/eslint-plugin": "5.25.0",
64
+ "@typescript-eslint/parser": "5.25.0",
65
+ "concurrently": "^7.2.0",
66
66
  "conventional-github-releaser": "3.1.5",
67
- "dotenv": "16.0.0",
68
- "esbuild": "^0.14.38",
69
- "eslint": "8.14.0",
67
+ "dotenv": "16.0.1",
68
+ "esbuild": "^0.14.39",
69
+ "eslint": "8.15.0",
70
70
  "eslint-config-prettier": "8.5.0",
71
- "eslint-plugin-jest": "26.1.5",
71
+ "eslint-plugin-jest": "26.2.2",
72
72
  "eslint-plugin-prettier": "4.0.0",
73
73
  "express": "4.18.1",
74
74
  "homebridge": "1.4.1",
@@ -76,8 +76,8 @@
76
76
  "prettier": "2.6.2",
77
77
  "reflect-metadata": "0.1.13",
78
78
  "rimraf": "3.0.2",
79
- "standard-version": "9.3.2",
80
- "ts-jest": "28.0.1",
79
+ "standard-version": "9.5.0",
80
+ "ts-jest": "28.0.2",
81
81
  "typescript": "4.6.4"
82
82
  },
83
83
  "engines": {