ring-client-api 9.25.0-beta.1 → 10.0.0-beta.0
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 +25 -0
- package/lib/api/index.d.ts +0 -1
- package/lib/api/index.js +0 -1
- package/lib/api/live-call.d.ts +1 -5
- package/lib/api/live-call.js +32 -51
- package/lib/api/ring-camera.d.ts +2 -16
- package/lib/api/ring-camera.js +4 -86
- package/lib/api/ring-types.d.ts +1 -0
- package/lib/api/ring-types.js +2 -0
- package/lib/api/rtp-utils.d.ts +0 -29
- package/lib/api/rtp-utils.js +2 -55
- package/package.json +4 -6
- package/lib/api/sip-call.d.ts +0 -76
- package/lib/api/sip-call.js +0 -242
- package/lib/api/sip-session.d.ts +0 -41
- package/lib/api/sip-session.js +0 -216
- package/lib/api/sip.d.ts +0 -87
- package/lib/api/sip.js +0 -1314
package/lib/api/sip-call.js
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
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.SipCall = exports.expiredDingError = void 0;
|
|
13
|
-
const rxjs_1 = require("rxjs");
|
|
14
|
-
const util_1 = require("./util");
|
|
15
|
-
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
16
|
-
const sip = require('./sip.js'), sdp = require('sdp');
|
|
17
|
-
exports.expiredDingError = new Error('Ding expired, received 480');
|
|
18
|
-
function getRandomId() {
|
|
19
|
-
return Math.floor(Math.random() * 1e6).toString();
|
|
20
|
-
}
|
|
21
|
-
function getRtpDescription(sections, mediaType) {
|
|
22
|
-
var _a, _b, _c, _d;
|
|
23
|
-
try {
|
|
24
|
-
const section = sections.find((s) => s.startsWith('m=' + mediaType)), { port } = sdp.parseMLine(section), lines = sdp.splitLines(section), rtcpLine = lines.find((l) => l.startsWith('a=rtcp:')), cryptoLine = lines.find((l) => l.startsWith('a=crypto')), ssrcLine = lines.find((l) => l.startsWith('a=ssrc')), iceUFragLine = lines.find((l) => l.startsWith('a=ice-ufrag')), icePwdLine = lines.find((l) => l.startsWith('a=ice-pwd')), encodedCrypto = cryptoLine.match(/inline:(\S*)/)[1];
|
|
25
|
-
return Object.assign({ port, rtcpPort: (rtcpLine && Number((_a = rtcpLine.match(/rtcp:(\S*)/)) === null || _a === void 0 ? void 0 : _a[1])) || port, ssrc: (ssrcLine && Number((_b = ssrcLine.match(/ssrc:(\S*)/)) === null || _b === void 0 ? void 0 : _b[1])) || undefined, iceUFrag: (iceUFragLine && ((_c = iceUFragLine.match(/ice-ufrag:(\S*)/)) === null || _c === void 0 ? void 0 : _c[1])) ||
|
|
26
|
-
undefined, icePwd: (icePwdLine && ((_d = icePwdLine.match(/ice-pwd:(\S*)/)) === null || _d === void 0 ? void 0 : _d[1])) || undefined }, (0, camera_utils_1.decodeSrtpOptions)(encodedCrypto));
|
|
27
|
-
}
|
|
28
|
-
catch (e) {
|
|
29
|
-
(0, util_1.logError)('Failed to parse SDP from Ring');
|
|
30
|
-
(0, util_1.logError)(sections.join('\r\n'));
|
|
31
|
-
throw e;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
function parseRtpDescription(inviteResponse) {
|
|
35
|
-
const sections = sdp.splitSections(inviteResponse.content), lines = sdp.splitLines(sections[0]), cLine = lines.find((line) => line.startsWith('c='));
|
|
36
|
-
return {
|
|
37
|
-
address: cLine.match(/c=IN IP4 (\S*)/)[1],
|
|
38
|
-
audio: getRtpDescription(sections, 'audio'),
|
|
39
|
-
video: getRtpDescription(sections, 'video'),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
class SipCall {
|
|
43
|
-
constructor(sipOptions, rtpOptions, tlsPort) {
|
|
44
|
-
this.sipOptions = sipOptions;
|
|
45
|
-
this.seq = 20;
|
|
46
|
-
this.fromParams = { tag: getRandomId() };
|
|
47
|
-
this.toParams = {};
|
|
48
|
-
this.callId = getRandomId();
|
|
49
|
-
this.onEndedByRemote = new rxjs_1.Subject();
|
|
50
|
-
this.destroyed = false;
|
|
51
|
-
this.cameraConnectedPromise = new Promise((resolve) => {
|
|
52
|
-
this.cameraConnected = resolve;
|
|
53
|
-
});
|
|
54
|
-
this.audioUfrag = (0, util_1.randomString)(16);
|
|
55
|
-
this.videoUfrag = (0, util_1.randomString)(16);
|
|
56
|
-
this.speakerActivated = false;
|
|
57
|
-
const { audio, video } = rtpOptions, { from } = this.sipOptions, host = this.sipOptions.localIp;
|
|
58
|
-
this.sipClient = sip.create({
|
|
59
|
-
host,
|
|
60
|
-
hostname: host,
|
|
61
|
-
tls_port: tlsPort,
|
|
62
|
-
tls: {
|
|
63
|
-
rejectUnauthorized: false,
|
|
64
|
-
},
|
|
65
|
-
tcp: false,
|
|
66
|
-
udp: false,
|
|
67
|
-
}, (request) => {
|
|
68
|
-
var _a;
|
|
69
|
-
if (request.method === 'BYE') {
|
|
70
|
-
(0, util_1.logDebug)('received BYE from ring server');
|
|
71
|
-
this.sipClient.send(this.sipClient.makeResponse(request, 200, 'Ok'));
|
|
72
|
-
if (this.destroyed) {
|
|
73
|
-
this.onEndedByRemote.next(null);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
else if (request.method === 'MESSAGE' &&
|
|
77
|
-
request.content === 'event=camera_connected') {
|
|
78
|
-
(0, util_1.logDebug)('camera connected to ring server');
|
|
79
|
-
(_a = this.cameraConnected) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
this.sdp = [
|
|
83
|
-
'v=0',
|
|
84
|
-
`o=${from.split(':')[1].split('@')[0]} 3747 461 IN IP4 ${host}`,
|
|
85
|
-
's=Talk',
|
|
86
|
-
`c=IN IP4 ${host}`,
|
|
87
|
-
'b=AS:380',
|
|
88
|
-
't=0 0',
|
|
89
|
-
`m=audio ${audio.port} RTP/SAVPF 0`,
|
|
90
|
-
'a=rtpmap:0 PCMU/8000',
|
|
91
|
-
(0, camera_utils_1.createCryptoLine)(audio),
|
|
92
|
-
'a=rtcp-mux',
|
|
93
|
-
'a=rtcp-fb:* trr-int 5',
|
|
94
|
-
'a=rtcp-fb:* ccm tmmbr',
|
|
95
|
-
`a=ice-ufrag:${this.audioUfrag}`,
|
|
96
|
-
`a=ice-pwd:${(0, util_1.randomString)(22)}`,
|
|
97
|
-
`a=candidate:${(0, util_1.randomInteger)()} 1 udp ${(0, util_1.randomInteger)()} ${host} ${audio.port} typ host generation 0 network-id 1 network-cost 50`,
|
|
98
|
-
`m=video ${video.port} RTP/SAVPF 99`,
|
|
99
|
-
'a=rtpmap:99 H264/90000',
|
|
100
|
-
'a=fmtp:99 profile-level-id=42801F',
|
|
101
|
-
(0, camera_utils_1.createCryptoLine)(video),
|
|
102
|
-
'a=rtcp-mux',
|
|
103
|
-
'a=rtcp-fb:* trr-int 5',
|
|
104
|
-
'a=rtcp-fb:* ccm tmmbr',
|
|
105
|
-
'a=rtcp-fb:99 nack pli',
|
|
106
|
-
'a=rtcp-fb:99 ccm tstr',
|
|
107
|
-
'a=rtcp-fb:99 ccm fir',
|
|
108
|
-
`a=ice-ufrag:${this.videoUfrag}`,
|
|
109
|
-
`a=ice-pwd:${(0, util_1.randomString)(22)}`,
|
|
110
|
-
`a=candidate:${(0, util_1.randomInteger)()} 1 udp ${(0, util_1.randomInteger)()} ${host} ${video.port} typ host generation 0 network-id 1 network-cost 50`,
|
|
111
|
-
]
|
|
112
|
-
.filter((l) => l)
|
|
113
|
-
.join('\r\n');
|
|
114
|
-
}
|
|
115
|
-
request({ method, headers, content, seq, }) {
|
|
116
|
-
if (this.destroyed) {
|
|
117
|
-
return Promise.reject(new Error('SIP request made after call was destroyed'));
|
|
118
|
-
}
|
|
119
|
-
return new Promise((resolve, reject) => {
|
|
120
|
-
seq = seq || this.seq++;
|
|
121
|
-
this.sipClient.send({
|
|
122
|
-
method,
|
|
123
|
-
uri: this.sipOptions.to,
|
|
124
|
-
headers: Object.assign({ to: {
|
|
125
|
-
name: '"FS Doorbot"',
|
|
126
|
-
uri: this.sipOptions.to,
|
|
127
|
-
params: this.toParams,
|
|
128
|
-
}, from: {
|
|
129
|
-
uri: this.sipOptions.from,
|
|
130
|
-
params: this.fromParams,
|
|
131
|
-
}, 'max-forwards': 70, 'call-id': this.callId, 'X-Ding': this.sipOptions.dingId, 'X-Authorization': '', 'User-Agent': 'Android/3.23.0 (belle-sip/1.6.3)', cseq: { seq, method } }, headers),
|
|
132
|
-
content: content || '',
|
|
133
|
-
}, (response) => {
|
|
134
|
-
if (response.headers.to.params && response.headers.to.params.tag) {
|
|
135
|
-
this.toParams.tag = response.headers.to.params.tag;
|
|
136
|
-
}
|
|
137
|
-
if (response.status >= 300) {
|
|
138
|
-
if (response.status === 480 && method === 'INVITE') {
|
|
139
|
-
const { dingId } = this.sipOptions;
|
|
140
|
-
(0, util_1.logInfo)(`Ding ${dingId} is expired (${response.status}). Fetching a new ding and trying video stream again`);
|
|
141
|
-
reject(exports.expiredDingError);
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
if (response.status !== 408 || method !== 'BYE') {
|
|
145
|
-
(0, util_1.logError)(`sip ${method} request failed with status ` + response.status);
|
|
146
|
-
}
|
|
147
|
-
reject(new Error(`sip ${method} request failed with status ` + response.status));
|
|
148
|
-
}
|
|
149
|
-
else if (response.status < 200) {
|
|
150
|
-
// call made progress, do nothing and wait for another response
|
|
151
|
-
// console.log('call progress status ' + response.status)
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
if (method === 'INVITE') {
|
|
155
|
-
// The ACK must be sent with every OK to keep the connection alive.
|
|
156
|
-
this.ackWithInfo(seq).catch((e) => {
|
|
157
|
-
(0, util_1.logError)('Failed to send SDP ACK and INFO');
|
|
158
|
-
(0, util_1.logError)(e);
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
resolve(response);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
ackWithInfo(seq) {
|
|
167
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
168
|
-
// Don't wait for ack, it won't ever come back.
|
|
169
|
-
this.request({
|
|
170
|
-
method: 'ACK',
|
|
171
|
-
seq, // The ACK must have the original sequence number.
|
|
172
|
-
}).catch(rxjs_1.noop);
|
|
173
|
-
// SIP session will be terminated after 60 seconds if these aren't sent
|
|
174
|
-
yield this.sendDtmf('2');
|
|
175
|
-
yield this.sendKeyFrameRequest();
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
sendDtmf(key) {
|
|
179
|
-
return this.request({
|
|
180
|
-
method: 'INFO',
|
|
181
|
-
headers: {
|
|
182
|
-
'Content-Type': 'application/dtmf-relay',
|
|
183
|
-
},
|
|
184
|
-
content: `Signal=${key}\r\nDuration=250`,
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
sendKeyFrameRequest() {
|
|
188
|
-
return this.request({
|
|
189
|
-
method: 'INFO',
|
|
190
|
-
headers: {
|
|
191
|
-
'Content-Type': 'application/media_control+xml',
|
|
192
|
-
},
|
|
193
|
-
content: '<?xml version="1.0" encoding="utf-8" ?><media_control> <vc_primitive> <to_encoder> <picture_fast_update></picture_fast_update> </to_encoder> </vc_primitive></media_control>',
|
|
194
|
-
});
|
|
195
|
-
}
|
|
196
|
-
invite() {
|
|
197
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
198
|
-
const { from } = this.sipOptions, inviteResponse = yield this.request({
|
|
199
|
-
method: 'INVITE',
|
|
200
|
-
headers: {
|
|
201
|
-
supported: 'replaces, outbound',
|
|
202
|
-
allow: 'INVITE, ACK, CANCEL, OPTIONS, BYE, REFER, NOTIFY, MESSAGE, SUBSCRIBE, INFO, UPDATE',
|
|
203
|
-
'content-type': 'application/sdp',
|
|
204
|
-
contact: [{ uri: from }],
|
|
205
|
-
},
|
|
206
|
-
content: this.sdp,
|
|
207
|
-
});
|
|
208
|
-
return parseRtpDescription(inviteResponse);
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
requestKeyFrame() {
|
|
212
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
213
|
-
// camera connected event doesn't always happen if cam is already streaming. 2 second fallback
|
|
214
|
-
yield Promise.race([this.cameraConnectedPromise, (0, util_1.delay)(2000)]);
|
|
215
|
-
(0, util_1.logDebug)('requesting key frame');
|
|
216
|
-
yield this.sendKeyFrameRequest();
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
activateCameraSpeaker() {
|
|
220
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
221
|
-
if (this.speakerActivated) {
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
this.speakerActivated = true;
|
|
225
|
-
(0, util_1.logDebug)('Activating camera speaker');
|
|
226
|
-
yield this.sendDtmf('1').catch((e) => {
|
|
227
|
-
(0, util_1.logError)('Failed to activate camera speaker');
|
|
228
|
-
(0, util_1.logError)(e);
|
|
229
|
-
});
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
sendBye() {
|
|
233
|
-
return this.request({ method: 'BYE' }).catch(() => {
|
|
234
|
-
// Don't care if we get an exception here.
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
destroy() {
|
|
238
|
-
this.destroyed = true;
|
|
239
|
-
this.sipClient.destroy();
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
exports.SipCall = SipCall;
|
package/lib/api/sip-session.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { RtpDescription, RtpOptions } from './rtp-utils';
|
|
2
|
-
import { RtpSplitter } from '@homebridge/camera-utils';
|
|
3
|
-
import { SipCall, SipOptions } from './sip-call';
|
|
4
|
-
import { RingCamera } from './ring-camera';
|
|
5
|
-
import { Subscribed } from './subscribed';
|
|
6
|
-
declare type SpawnInput = string | number;
|
|
7
|
-
export interface FfmpegOptions {
|
|
8
|
-
input?: SpawnInput[];
|
|
9
|
-
video?: SpawnInput[] | false;
|
|
10
|
-
audio?: SpawnInput[];
|
|
11
|
-
output: SpawnInput[];
|
|
12
|
-
}
|
|
13
|
-
export declare class SipSession extends Subscribed {
|
|
14
|
-
readonly sipOptions: SipOptions;
|
|
15
|
-
readonly rtpOptions: RtpOptions;
|
|
16
|
-
readonly audioSplitter: RtpSplitter;
|
|
17
|
-
audioRtcpSplitter: RtpSplitter;
|
|
18
|
-
readonly videoSplitter: RtpSplitter;
|
|
19
|
-
videoRtcpSplitter: RtpSplitter;
|
|
20
|
-
private readonly tlsPort;
|
|
21
|
-
readonly camera: RingCamera;
|
|
22
|
-
private hasStarted;
|
|
23
|
-
private hasCallEnded;
|
|
24
|
-
private onCallEndedSubject;
|
|
25
|
-
private sipCall;
|
|
26
|
-
onCallEnded: import("rxjs").Observable<unknown>;
|
|
27
|
-
constructor(sipOptions: SipOptions, rtpOptions: RtpOptions, audioSplitter: RtpSplitter, audioRtcpSplitter: RtpSplitter, videoSplitter: RtpSplitter, videoRtcpSplitter: RtpSplitter, tlsPort: number, camera: RingCamera);
|
|
28
|
-
createSipCall(sipOptions: SipOptions): SipCall;
|
|
29
|
-
start(ffmpegOptions?: FfmpegOptions): Promise<RtpDescription>;
|
|
30
|
-
prepareTranscoder(transcodeVideoStream: boolean, ffmpegInputOptions: SpawnInput[] | undefined, remoteRtpOptions: RtpOptions, audioPort: number, videoPort: number, sdpInput: string): {
|
|
31
|
-
ffmpegInputArguments: SpawnInput[];
|
|
32
|
-
inputSdpLines: string[];
|
|
33
|
-
};
|
|
34
|
-
private startTranscoder;
|
|
35
|
-
reservePort(bufferPorts?: number): Promise<number>;
|
|
36
|
-
requestKeyFrame(): Promise<void>;
|
|
37
|
-
activateCameraSpeaker(): Promise<void>;
|
|
38
|
-
private callEnded;
|
|
39
|
-
stop(): void;
|
|
40
|
-
}
|
|
41
|
-
export {};
|
package/lib/api/sip-session.js
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
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.SipSession = void 0;
|
|
13
|
-
const rxjs_1 = require("rxjs");
|
|
14
|
-
const rtp_utils_1 = require("./rtp-utils");
|
|
15
|
-
const camera_utils_1 = require("@homebridge/camera-utils");
|
|
16
|
-
const sip_call_1 = require("./sip-call");
|
|
17
|
-
const subscribed_1 = require("./subscribed");
|
|
18
|
-
const util_1 = require("./util");
|
|
19
|
-
const ffmpeg_1 = require("./ffmpeg");
|
|
20
|
-
const operators_1 = require("rxjs/operators");
|
|
21
|
-
class SipSession extends subscribed_1.Subscribed {
|
|
22
|
-
constructor(sipOptions, rtpOptions, audioSplitter, audioRtcpSplitter, videoSplitter, videoRtcpSplitter, tlsPort, camera) {
|
|
23
|
-
super();
|
|
24
|
-
this.sipOptions = sipOptions;
|
|
25
|
-
this.rtpOptions = rtpOptions;
|
|
26
|
-
this.audioSplitter = audioSplitter;
|
|
27
|
-
this.audioRtcpSplitter = audioRtcpSplitter;
|
|
28
|
-
this.videoSplitter = videoSplitter;
|
|
29
|
-
this.videoRtcpSplitter = videoRtcpSplitter;
|
|
30
|
-
this.tlsPort = tlsPort;
|
|
31
|
-
this.camera = camera;
|
|
32
|
-
this.hasStarted = false;
|
|
33
|
-
this.hasCallEnded = false;
|
|
34
|
-
this.onCallEndedSubject = new rxjs_1.ReplaySubject(1);
|
|
35
|
-
this.onCallEnded = this.onCallEndedSubject.asObservable();
|
|
36
|
-
this.sipCall = this.createSipCall(this.sipOptions);
|
|
37
|
-
}
|
|
38
|
-
createSipCall(sipOptions) {
|
|
39
|
-
if (this.sipCall) {
|
|
40
|
-
this.sipCall.destroy();
|
|
41
|
-
}
|
|
42
|
-
const call = (this.sipCall = new sip_call_1.SipCall(sipOptions, this.rtpOptions, this.tlsPort));
|
|
43
|
-
this.addSubscriptions(call.onEndedByRemote.subscribe(() => this.callEnded(false)));
|
|
44
|
-
return this.sipCall;
|
|
45
|
-
}
|
|
46
|
-
start(ffmpegOptions) {
|
|
47
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
48
|
-
if (this.hasStarted) {
|
|
49
|
-
throw new Error('SIP Session has already been started');
|
|
50
|
-
}
|
|
51
|
-
this.hasStarted = true;
|
|
52
|
-
if (this.hasCallEnded) {
|
|
53
|
-
throw new Error('SIP Session has already ended');
|
|
54
|
-
}
|
|
55
|
-
try {
|
|
56
|
-
const videoPort = yield this.reservePort(1), audioPort = yield this.reservePort(1), rtpDescription = yield this.sipCall.invite(), sendStunRequests = () => {
|
|
57
|
-
(0, rtp_utils_1.sendStunBindingRequest)({
|
|
58
|
-
rtpSplitter: this.audioSplitter,
|
|
59
|
-
rtcpSplitter: this.audioRtcpSplitter,
|
|
60
|
-
rtpDescription,
|
|
61
|
-
localUfrag: this.sipCall.audioUfrag,
|
|
62
|
-
type: 'audio',
|
|
63
|
-
});
|
|
64
|
-
(0, rtp_utils_1.sendStunBindingRequest)({
|
|
65
|
-
rtpSplitter: this.videoSplitter,
|
|
66
|
-
rtcpSplitter: this.videoRtcpSplitter,
|
|
67
|
-
rtpDescription,
|
|
68
|
-
localUfrag: this.sipCall.videoUfrag,
|
|
69
|
-
type: 'video',
|
|
70
|
-
});
|
|
71
|
-
};
|
|
72
|
-
if (ffmpegOptions) {
|
|
73
|
-
this.startTranscoder(ffmpegOptions, rtpDescription, audioPort, videoPort);
|
|
74
|
-
}
|
|
75
|
-
// if rtcp-mux is supported, rtp splitter will be used for both rtp and rtcp
|
|
76
|
-
if (rtpDescription.audio.port === rtpDescription.audio.rtcpPort) {
|
|
77
|
-
this.audioRtcpSplitter.close();
|
|
78
|
-
this.audioRtcpSplitter = this.audioSplitter;
|
|
79
|
-
}
|
|
80
|
-
if (rtpDescription.video.port === rtpDescription.video.rtcpPort) {
|
|
81
|
-
this.videoRtcpSplitter.close();
|
|
82
|
-
this.videoRtcpSplitter = this.videoSplitter;
|
|
83
|
-
}
|
|
84
|
-
if (rtpDescription.video.iceUFrag) {
|
|
85
|
-
// ICE is supported
|
|
86
|
-
(0, util_1.logDebug)(`Connecting to ${this.camera.name} using ICE`);
|
|
87
|
-
(0, rtp_utils_1.createStunResponder)(this.audioSplitter);
|
|
88
|
-
(0, rtp_utils_1.createStunResponder)(this.videoSplitter);
|
|
89
|
-
sendStunRequests();
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
// ICE is not supported, use stun as keep alive
|
|
93
|
-
(0, util_1.logDebug)(`Connecting to ${this.camera.name} using STUN`);
|
|
94
|
-
this.addSubscriptions(
|
|
95
|
-
// hole punch every .5 seconds to keep stream alive and port open (matches behavior from Ring app)
|
|
96
|
-
(0, rxjs_1.timer)(0, 500).subscribe(sendStunRequests));
|
|
97
|
-
}
|
|
98
|
-
this.addSubscriptions(this.audioSplitter.onMessage.pipe((0, operators_1.take)(1)).subscribe(() => {
|
|
99
|
-
(0, util_1.logDebug)(`Audio stream latched for ${this.camera.name}`);
|
|
100
|
-
}), this.videoSplitter.onMessage.pipe((0, operators_1.take)(1)).subscribe(() => {
|
|
101
|
-
(0, util_1.logDebug)(`Video stream latched for ${this.camera.name}`);
|
|
102
|
-
}));
|
|
103
|
-
return rtpDescription;
|
|
104
|
-
}
|
|
105
|
-
catch (e) {
|
|
106
|
-
if (e === sip_call_1.expiredDingError) {
|
|
107
|
-
const sipOptions = yield this.camera.getUpdatedSipOptions(this.sipOptions.dingId);
|
|
108
|
-
this.createSipCall(sipOptions);
|
|
109
|
-
this.hasStarted = false;
|
|
110
|
-
return this.start(ffmpegOptions);
|
|
111
|
-
}
|
|
112
|
-
this.callEnded(true);
|
|
113
|
-
throw e;
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
prepareTranscoder(transcodeVideoStream, ffmpegInputOptions, remoteRtpOptions, audioPort, videoPort, sdpInput) {
|
|
118
|
-
const ffmpegInputArguments = [
|
|
119
|
-
'-hide_banner',
|
|
120
|
-
'-protocol_whitelist',
|
|
121
|
-
'pipe,udp,rtp,file,crypto',
|
|
122
|
-
'-f',
|
|
123
|
-
'sdp',
|
|
124
|
-
...(ffmpegInputOptions || []),
|
|
125
|
-
'-i',
|
|
126
|
-
sdpInput,
|
|
127
|
-
], inputSdpLines = [
|
|
128
|
-
'v=0',
|
|
129
|
-
'o=105202070 3747 461 IN IP4 127.0.0.1',
|
|
130
|
-
's=Talk',
|
|
131
|
-
'c=IN IP4 127.0.0.1',
|
|
132
|
-
'b=AS:380',
|
|
133
|
-
't=0 0',
|
|
134
|
-
'a=rtcp-xr:rcvr-rtt=all:10000 stat-summary=loss,dup,jitt,TTL voip-metrics',
|
|
135
|
-
`m=audio ${audioPort} RTP/SAVP 0 101`,
|
|
136
|
-
'a=rtpmap:0 PCMU/8000',
|
|
137
|
-
(0, camera_utils_1.createCryptoLine)(remoteRtpOptions.audio),
|
|
138
|
-
'a=rtcp-mux',
|
|
139
|
-
];
|
|
140
|
-
if (transcodeVideoStream) {
|
|
141
|
-
inputSdpLines.push(`m=video ${videoPort} RTP/SAVP 99`, 'a=rtpmap:99 H264/90000', (0, camera_utils_1.createCryptoLine)(remoteRtpOptions.video), 'a=rtcp-mux');
|
|
142
|
-
let haveReceivedStreamPacket = false;
|
|
143
|
-
this.videoSplitter.addMessageHandler(({ isRtpMessage, message }) => {
|
|
144
|
-
if ((0, rtp_utils_1.isStunMessage)(message)) {
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
if (!haveReceivedStreamPacket) {
|
|
148
|
-
this.sipCall.requestKeyFrame().catch(rxjs_1.noop);
|
|
149
|
-
haveReceivedStreamPacket = true;
|
|
150
|
-
}
|
|
151
|
-
return {
|
|
152
|
-
port: isRtpMessage ? videoPort : videoPort + 1,
|
|
153
|
-
};
|
|
154
|
-
});
|
|
155
|
-
}
|
|
156
|
-
this.audioSplitter.addMessageHandler(({ isRtpMessage, message }) => {
|
|
157
|
-
if ((0, rtp_utils_1.isStunMessage)(message)) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
return {
|
|
161
|
-
port: isRtpMessage ? audioPort : audioPort + 1,
|
|
162
|
-
};
|
|
163
|
-
});
|
|
164
|
-
return {
|
|
165
|
-
ffmpegInputArguments,
|
|
166
|
-
inputSdpLines,
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
startTranscoder(ffmpegOptions, remoteRtpOptions, audioPort, videoPort) {
|
|
170
|
-
const transcodeVideoStream = ffmpegOptions.video !== false, { ffmpegInputArguments, inputSdpLines } = this.prepareTranscoder(transcodeVideoStream, ffmpegOptions.input, remoteRtpOptions, audioPort, videoPort, 'pipe:'), ff = new camera_utils_1.FfmpegProcess({
|
|
171
|
-
ffmpegArgs: ffmpegInputArguments.concat(...(ffmpegOptions.audio || ['-acodec', 'aac']), ...(transcodeVideoStream
|
|
172
|
-
? ffmpegOptions.video || ['-vcodec', 'copy']
|
|
173
|
-
: []), ...(ffmpegOptions.output || [])),
|
|
174
|
-
ffmpegPath: (0, ffmpeg_1.getFfmpegPath)(),
|
|
175
|
-
exitCallback: () => this.callEnded(true),
|
|
176
|
-
logLabel: `From Ring (${this.camera.name})`,
|
|
177
|
-
logger: {
|
|
178
|
-
error: util_1.logError,
|
|
179
|
-
info: util_1.logDebug,
|
|
180
|
-
},
|
|
181
|
-
});
|
|
182
|
-
this.onCallEnded.subscribe(() => ff.stop());
|
|
183
|
-
ff.writeStdin(inputSdpLines.filter((x) => Boolean(x)).join('\n'));
|
|
184
|
-
}
|
|
185
|
-
reservePort(bufferPorts = 0) {
|
|
186
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
187
|
-
const ports = yield (0, camera_utils_1.reservePorts)({ count: bufferPorts + 1 });
|
|
188
|
-
return ports[0];
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
requestKeyFrame() {
|
|
192
|
-
return this.sipCall.requestKeyFrame();
|
|
193
|
-
}
|
|
194
|
-
activateCameraSpeaker() {
|
|
195
|
-
return this.sipCall.activateCameraSpeaker();
|
|
196
|
-
}
|
|
197
|
-
callEnded(sendBye) {
|
|
198
|
-
if (this.hasCallEnded) {
|
|
199
|
-
return;
|
|
200
|
-
}
|
|
201
|
-
this.hasCallEnded = true;
|
|
202
|
-
if (sendBye) {
|
|
203
|
-
this.sipCall.sendBye().catch(util_1.logError);
|
|
204
|
-
}
|
|
205
|
-
// clean up
|
|
206
|
-
this.onCallEndedSubject.next(null);
|
|
207
|
-
this.sipCall.destroy();
|
|
208
|
-
this.videoSplitter.close();
|
|
209
|
-
this.audioSplitter.close();
|
|
210
|
-
this.unsubscribe();
|
|
211
|
-
}
|
|
212
|
-
stop() {
|
|
213
|
-
this.callEnded(true);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
exports.SipSession = SipSession;
|
package/lib/api/sip.d.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
export function copyMessage(msg: any, deep: any): any;
|
|
2
|
-
export function create(options: any, callback: any): {
|
|
3
|
-
send: (m: any, callback: any) => void;
|
|
4
|
-
encodeFlowUri: (flow: any) => {
|
|
5
|
-
schema: string;
|
|
6
|
-
user: any;
|
|
7
|
-
host: any;
|
|
8
|
-
params: {};
|
|
9
|
-
};
|
|
10
|
-
decodeFlowUri: (uri: any) => {
|
|
11
|
-
protocol: any;
|
|
12
|
-
address: any;
|
|
13
|
-
port: number;
|
|
14
|
-
local: {
|
|
15
|
-
address: any;
|
|
16
|
-
port: number;
|
|
17
|
-
};
|
|
18
|
-
} | undefined;
|
|
19
|
-
isFlowUri: (uri: any) => boolean;
|
|
20
|
-
hostname: () => any;
|
|
21
|
-
destroy: () => void;
|
|
22
|
-
makeResponse: typeof makeResponse;
|
|
23
|
-
};
|
|
24
|
-
export function start(options: any, callback: any): void;
|
|
25
|
-
export function parseAOR(data: any): any;
|
|
26
|
-
export function parseUri(s: any): any;
|
|
27
|
-
export function stringifyUri(uri: any): string;
|
|
28
|
-
export function stringifyAuthHeader(a: any): string;
|
|
29
|
-
export function stringify(m: any): string;
|
|
30
|
-
export function makeResponse(rq: any, status: any, reason: any, extension: any): {
|
|
31
|
-
status: any;
|
|
32
|
-
reason: any;
|
|
33
|
-
version: any;
|
|
34
|
-
headers: {
|
|
35
|
-
via: any;
|
|
36
|
-
to: any;
|
|
37
|
-
from: any;
|
|
38
|
-
'call-id': any;
|
|
39
|
-
cseq: any;
|
|
40
|
-
};
|
|
41
|
-
};
|
|
42
|
-
export function makeStreamParser(onMessage: any, onFlood: any, maxBytesHeaders: any, maxContentLength: any): (data: any) => void;
|
|
43
|
-
declare function parseMessage(s: any): {
|
|
44
|
-
headers: {};
|
|
45
|
-
} | undefined;
|
|
46
|
-
export function makeTransport(options: any, callback: any): {
|
|
47
|
-
open: (target: any, error: any) => any;
|
|
48
|
-
get: (target: any, error: any) => any;
|
|
49
|
-
send: (target: any, message: any) => void;
|
|
50
|
-
destroy: () => void;
|
|
51
|
-
};
|
|
52
|
-
export function resolve(uri: any, action: any): any;
|
|
53
|
-
export function generateBranch(): string;
|
|
54
|
-
export function makeTransactionLayer(options: any, transport: any): {
|
|
55
|
-
createServerTransaction: (rq: any, cn: any) => {
|
|
56
|
-
send: (...args: any[]) => void;
|
|
57
|
-
message: (...args: any[]) => void;
|
|
58
|
-
shutdown: () => void;
|
|
59
|
-
};
|
|
60
|
-
createClientTransaction: (connection: any, rq: any, callback: any) => {
|
|
61
|
-
message: (...args: any[]) => void;
|
|
62
|
-
shutdown: () => void;
|
|
63
|
-
};
|
|
64
|
-
getServer: (m: any) => any;
|
|
65
|
-
getClient: (m: any) => any;
|
|
66
|
-
destroy: () => void;
|
|
67
|
-
};
|
|
68
|
-
export function send(m: any, callback: any): void;
|
|
69
|
-
declare function destroy(): void;
|
|
70
|
-
export function encodeFlowUri(flow: any): {
|
|
71
|
-
schema: string;
|
|
72
|
-
user: any;
|
|
73
|
-
host: any;
|
|
74
|
-
params: {};
|
|
75
|
-
};
|
|
76
|
-
export function decodeFlowUri(uri: any): {
|
|
77
|
-
protocol: any;
|
|
78
|
-
address: any;
|
|
79
|
-
port: number;
|
|
80
|
-
local: {
|
|
81
|
-
address: any;
|
|
82
|
-
port: number;
|
|
83
|
-
};
|
|
84
|
-
} | undefined;
|
|
85
|
-
export function isFlowUri(uri: any): boolean;
|
|
86
|
-
export function hostname(): any;
|
|
87
|
-
export { parseMessage as parse, destroy as stop };
|