ring-client-api 11.0.1 → 11.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
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](https://github.com/dgreif/ring/compare/v11.0.1...v11.0.2) (2022-05-22)
6
+
7
+
8
+ ### Bug Fixes
9
+
10
+ * **api:** prevent duplicate events from `onCallEnded` ([14ee6eb](https://github.com/dgreif/ring/commit/14ee6eb90bb6ae568dcfd9e8e176c3001e422f9c)), closes [#941](https://github.com/dgreif/ring/issues/941)
11
+ * handle pcmu audio streams for incoming audio ([06f6ff1](https://github.com/dgreif/ring/commit/06f6ff10ce7c8f2b05b1a12b8f244daa200fa9ab)), closes [#930](https://github.com/dgreif/ring/issues/930)
12
+ * handle pcmu return audio ([7b86182](https://github.com/dgreif/ring/commit/7b8618261a6ccb85b9f760ee1fcc228be00fe5a6)), closes [#930](https://github.com/dgreif/ring/issues/930)
13
+ * **homebride:** use shared engines config to enforce node 16 requirement ([b748cb0](https://github.com/dgreif/ring/commit/b748cb027db0c43ae109e1ea77c6bc83f86e4913))
14
+ * prevent hanging promises from streams that fail to connect ([dd7638e](https://github.com/dgreif/ring/commit/dd7638e32626499c0be501be6b19b5d874fa974f))
15
+
5
16
  ### [11.0.1](https://github.com/dgreif/ring/compare/v11.0.0...v11.0.1) (2022-05-17)
6
17
 
7
18
 
@@ -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({
@@ -21,10 +21,11 @@ export declare abstract class StreamingConnectionBase extends Subscribed {
21
21
  }): Promise<void>;
22
22
  protected abstract sendSessionMessage(method: string, body?: Record<any, any>): void;
23
23
  private activated;
24
- activate(): Promise<void>;
25
- activateCameraSpeaker(): Promise<void>;
24
+ activate(): void;
25
+ activateCameraSpeaker(): void;
26
26
  protected sendMessage(message: Record<any, any>): void;
27
27
  sendAudioPacket(rtp: RtpPacket): void;
28
+ private hasEnded;
28
29
  protected callEnded(): void;
29
30
  stop(): void;
30
31
  }
@@ -14,6 +14,7 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
14
14
  this.onCallAnswered = new rxjs_1.ReplaySubject(1);
15
15
  this.onCallEnded = new rxjs_1.ReplaySubject(1);
16
16
  this.activated = false;
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();
@@ -50,25 +51,43 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
50
51
  }
51
52
  }));
52
53
  }
53
- async activate() {
54
+ activate() {
54
55
  if (this.activated) {
55
56
  return;
56
57
  }
57
58
  this.activated = true;
58
- await (0, rxjs_1.firstValueFrom)(this.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')));
59
- (0, util_1.logInfo)('Activating Session');
60
- this.sendSessionMessage('activate_session');
59
+ // Fire and forget this call so that callers don't get hung up waiting for connection (which might not happen)
60
+ (0, rxjs_1.firstValueFrom)(this.pc.onConnectionState.pipe((0, operators_1.filter)((state) => state === 'connected')))
61
+ .then(() => {
62
+ (0, util_1.logInfo)('Activating Session');
63
+ this.sendSessionMessage('activate_session');
64
+ })
65
+ .catch((e) => {
66
+ (0, util_1.logError)(e);
67
+ });
61
68
  }
62
- async activateCameraSpeaker() {
63
- await (0, rxjs_1.firstValueFrom)(this.onCallAnswered);
64
- this.sendSessionMessage('camera_options', {
65
- stealth_mode: false,
69
+ activateCameraSpeaker() {
70
+ // Fire and forget this call so that callers don't get hung up waiting for answer (which might not happen)
71
+ (0, rxjs_1.firstValueFrom)(this.onCallAnswered)
72
+ .then(() => {
73
+ this.sendSessionMessage('camera_options', {
74
+ stealth_mode: false,
75
+ });
76
+ })
77
+ .catch((e) => {
78
+ (0, util_1.logError)(e);
66
79
  });
67
80
  }
68
81
  sendMessage(message) {
82
+ if (this.hasEnded) {
83
+ return;
84
+ }
69
85
  this.ws.send(JSON.stringify(message));
70
86
  }
71
87
  sendAudioPacket(rtp) {
88
+ if (this.hasEnded) {
89
+ return;
90
+ }
72
91
  if (this.pc instanceof peer_connection_1.WeriftPeerConnection) {
73
92
  this.pc.returnAudioTrack.writeRtp(rtp);
74
93
  }
@@ -77,6 +96,9 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
77
96
  }
78
97
  }
79
98
  callEnded() {
99
+ if (this.hasEnded) {
100
+ return;
101
+ }
80
102
  try {
81
103
  this.sendMessage({
82
104
  reason: { code: 0, text: '' },
@@ -87,6 +109,7 @@ class StreamingConnectionBase extends subscribed_1.Subscribed {
87
109
  catch (_) {
88
110
  // ignore any errors since we are stopping the call
89
111
  }
112
+ this.hasEnded = true;
90
113
  this.unsubscribe();
91
114
  this.onCallEnded.next();
92
115
  this.pc.close();
@@ -15,18 +15,25 @@ 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;
25
- activate(): Promise<void>;
27
+ activate(): void;
26
28
  cameraSpeakerActivated: boolean;
27
- activateCameraSpeaker(): Promise<void>;
29
+ activateCameraSpeaker(): 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,48 +22,59 @@ 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
- async activate() {
36
- if (this.activated) {
41
+ activate() {
42
+ if (this.activated || this.hasEnded) {
37
43
  return;
38
44
  }
39
45
  this.activated = true;
40
- await this.connection.activate();
46
+ this.connection.activate();
41
47
  }
42
- async activateCameraSpeaker() {
43
- if (this.cameraSpeakerActivated) {
48
+ activateCameraSpeaker() {
49
+ if (this.cameraSpeakerActivated || this.hasEnded) {
44
50
  return;
45
51
  }
46
52
  this.cameraSpeakerActivated = true;
47
- await this.connection.activateCameraSpeaker();
53
+ this.connection.activateCameraSpeaker();
48
54
  }
49
55
  async reservePort(bufferPorts = 0) {
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
@@ -95,19 +107,63 @@ class StreamingSession extends subscribed_1.Subscribed {
95
107
  this.onCallEnded.subscribe(() => ff.stop());
96
108
  ff.writeStdin(inputSdp);
97
109
  // Activate the stream now that ffmpeg is ready to receive
98
- await this.activate();
110
+ this.activate();
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());
99
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ring-client-api",
3
- "version": "11.0.1",
3
+ "version": "11.0.2",
4
4
  "description": "Unofficial API for Ring doorbells, cameras, security alarm system and smart lighting",
5
5
  "main": "lib/api/index.js",
6
6
  "bin": {
@@ -15,12 +15,12 @@
15
15
  "lint": "eslint '**/*.ts' --fix",
16
16
  "build": "rm -rf lib && tsc --declaration",
17
17
  "build-quick": "rm -rf lib && esbuild ./{api,examples,homebridge}/*.ts ./api/streaming/*.ts --target=es2017 --format=cjs --outdir=lib",
18
- "example": "node -r @swc-node/register ./lib/examples/example.js",
19
- "api-example": "node -r @swc-node/register ./lib/examples/api-example.js",
20
- "record-example": "node -r @swc-node/register ./lib/examples/record-example.js",
21
- "return-audio-example": "node -r @swc-node/register ./lib/examples/return-audio-example.js",
22
- "stream-example": "node -r @swc-node/register ./lib/examples/stream-example.js",
23
- "browser-example": "node -r @swc-node/register ./lib/examples/browser-example.js",
18
+ "example": "node -r @swc-node/register ./examples/example.ts",
19
+ "api-example": "node -r @swc-node/register ./examples/api-example.ts",
20
+ "record-example": "node -r @swc-node/register ./examples/record-example.ts",
21
+ "return-audio-example": "node -r @swc-node/register ./examples/return-audio-example.ts",
22
+ "stream-example": "node -r @swc-node/register ./examples/stream-example.ts",
23
+ "browser-example": "node -r @swc-node/register ./examples/browser-example.ts",
24
24
  "auth-cli": "npm run build-quick && ./ring-auth-cli.js",
25
25
  "device-data-cli": "npm run build-quick && ./ring-device-data-cli.js",
26
26
  "hb": "npm run build-quick && ./build/package-homebridge.sh && homebridge -P ./release-homebridge -U ./.homebridge",
@@ -58,7 +58,7 @@
58
58
  "@eneris/push-receiver": "^3.0.1",
59
59
  "@homebridge/camera-utils": "^2.1.0",
60
60
  "@homebridge/plugin-ui-utils": "^0.0.19",
61
- "werift": "^0.15.0",
61
+ "werift": "^0.15.1",
62
62
  "@types/socket.io-client": "1.4.36",
63
63
  "colors": "^1.4.0",
64
64
  "debug": "^4.3.4",