ringcentral-softphone 1.3.2 → 1.3.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/README.md +1 -1
- package/dist/call-session/inbound.cjs +93 -0
- package/dist/call-session/inbound.js +64 -0
- package/dist/call-session/index.cjs +270 -0
- package/dist/{esm/call-session → call-session}/index.d.ts +3 -3
- package/dist/call-session/index.js +244 -0
- package/dist/call-session/outbound.cjs +100 -0
- package/dist/call-session/outbound.js +71 -0
- package/dist/call-session/streamer.cjs +112 -0
- package/dist/call-session/streamer.js +83 -0
- package/dist/codec.cjs +85 -0
- package/dist/codec.js +66 -0
- package/dist/dtmf.cjs +65 -0
- package/dist/dtmf.js +46 -0
- package/dist/index.cjs +258 -0
- package/dist/{cjs/index.d.ts → index.d.ts} +3 -3
- package/dist/index.js +240 -0
- package/dist/sip-message/inbound/index.cjs +51 -0
- package/dist/sip-message/inbound/index.js +22 -0
- package/dist/sip-message/index.cjs +49 -0
- package/dist/sip-message/index.js +12 -0
- package/dist/sip-message/outbound/index.cjs +41 -0
- package/dist/sip-message/outbound/index.js +12 -0
- package/dist/sip-message/outbound/request.cjs +62 -0
- package/dist/sip-message/outbound/request.js +33 -0
- package/dist/sip-message/outbound/response.cjs +55 -0
- package/dist/sip-message/outbound/response.js +26 -0
- package/dist/sip-message/response-codes.cjs +102 -0
- package/dist/sip-message/response-codes.js +83 -0
- package/dist/sip-message/sip-message.cjs +53 -0
- package/dist/sip-message/sip-message.js +34 -0
- package/dist/types.cjs +15 -0
- package/dist/types.js +0 -0
- package/dist/utils.cjs +80 -0
- package/dist/{cjs/utils.d.ts → utils.d.ts} +2 -2
- package/dist/utils.js +41 -0
- package/package.json +19 -13
- package/dist/cjs/call-session/inbound.js +0 -57
- package/dist/cjs/call-session/index.d.ts +0 -44
- package/dist/cjs/call-session/index.js +0 -239
- package/dist/cjs/call-session/outbound.js +0 -66
- package/dist/cjs/call-session/streamer.d.ts +0 -17
- package/dist/cjs/call-session/streamer.js +0 -76
- package/dist/cjs/codec.js +0 -65
- package/dist/cjs/dtmf.js +0 -45
- package/dist/cjs/index.js +0 -209
- package/dist/cjs/sip-message/inbound/index.js +0 -22
- package/dist/cjs/sip-message/index.d.ts +0 -5
- package/dist/cjs/sip-message/index.js +0 -16
- package/dist/cjs/sip-message/outbound/index.js +0 -14
- package/dist/cjs/sip-message/outbound/request.js +0 -28
- package/dist/cjs/sip-message/outbound/response.js +0 -25
- package/dist/cjs/sip-message/response-codes.js +0 -83
- package/dist/cjs/sip-message/sip-message.js +0 -34
- package/dist/cjs/types.js +0 -2
- package/dist/cjs/utils.js +0 -40
- package/dist/esm/call-session/inbound.d.ts +0 -8
- package/dist/esm/call-session/inbound.js +0 -52
- package/dist/esm/call-session/index.js +0 -234
- package/dist/esm/call-session/outbound.d.ts +0 -11
- package/dist/esm/call-session/outbound.js +0 -61
- package/dist/esm/call-session/streamer.js +0 -71
- package/dist/esm/codec.d.ts +0 -15
- package/dist/esm/codec.js +0 -63
- package/dist/esm/dtmf.d.ts +0 -8
- package/dist/esm/dtmf.js +0 -43
- package/dist/esm/index.d.ts +0 -28
- package/dist/esm/index.js +0 -204
- package/dist/esm/sip-message/inbound/index.d.ts +0 -5
- package/dist/esm/sip-message/inbound/index.js +0 -17
- package/dist/esm/sip-message/index.js +0 -5
- package/dist/esm/sip-message/outbound/index.d.ts +0 -5
- package/dist/esm/sip-message/outbound/index.js +0 -9
- package/dist/esm/sip-message/outbound/request.d.ts +0 -7
- package/dist/esm/sip-message/outbound/request.js +0 -23
- package/dist/esm/sip-message/outbound/response.d.ts +0 -6
- package/dist/esm/sip-message/outbound/response.js +0 -20
- package/dist/esm/sip-message/response-codes.d.ts +0 -4
- package/dist/esm/sip-message/response-codes.js +0 -81
- package/dist/esm/sip-message/sip-message.d.ts +0 -11
- package/dist/esm/sip-message/sip-message.js +0 -32
- package/dist/esm/types.d.ts +0 -9
- package/dist/esm/types.js +0 -1
- package/dist/esm/utils.d.ts +0 -8
- package/dist/esm/utils.js +0 -28
- package/dist/{cjs/call-session → call-session}/inbound.d.ts +2 -2
- package/dist/{cjs/call-session → call-session}/outbound.d.ts +2 -2
- package/dist/{esm/call-session → call-session}/streamer.d.ts +1 -1
- package/dist/{cjs/codec.d.ts → codec.d.ts} +0 -0
- package/dist/{cjs/dtmf.d.ts → dtmf.d.ts} +0 -0
- package/dist/{cjs/sip-message → sip-message}/inbound/index.d.ts +0 -0
- package/dist/{esm/sip-message → sip-message}/index.d.ts +2 -2
- package/dist/{cjs/sip-message → sip-message}/outbound/index.d.ts +0 -0
- package/dist/{cjs/sip-message → sip-message}/outbound/request.d.ts +0 -0
- package/dist/{cjs/sip-message → sip-message}/outbound/response.d.ts +1 -1
- /package/dist/{cjs/sip-message → sip-message}/response-codes.d.ts +0 -0
- /package/dist/{cjs/sip-message → sip-message}/sip-message.d.ts +0 -0
- /package/dist/{cjs/types.d.ts → types.d.ts} +0 -0
package/dist/cjs/utils.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.localKey = exports.extractAddress = exports.withoutTag = exports.randomInt = exports.branch = exports.uuid = exports.generateAuthorization = void 0;
|
|
7
|
-
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
8
|
-
const md5 = (s) => node_crypto_1.default.createHash("md5").update(s).digest("hex");
|
|
9
|
-
const generateResponse = (sipInfo, endpoint, nonce) => {
|
|
10
|
-
const ha1 = md5(`${sipInfo.authorizationId}:${sipInfo.domain}:${sipInfo.password}`);
|
|
11
|
-
const ha2 = md5(endpoint);
|
|
12
|
-
const response = md5(`${ha1}:${nonce}:${ha2}`);
|
|
13
|
-
return response;
|
|
14
|
-
};
|
|
15
|
-
const generateAuthorization = (sipInfo, nonce, method) => {
|
|
16
|
-
const authObj = {
|
|
17
|
-
"Digest algorithm": "MD5",
|
|
18
|
-
username: sipInfo.authorizationId,
|
|
19
|
-
realm: sipInfo.domain,
|
|
20
|
-
nonce,
|
|
21
|
-
uri: `sip:${sipInfo.domain}`,
|
|
22
|
-
response: generateResponse(sipInfo, `${method}:sip:${sipInfo.domain}`, nonce),
|
|
23
|
-
};
|
|
24
|
-
return Object.keys(authObj)
|
|
25
|
-
.map((key) => `${key}="${authObj[key]}"`)
|
|
26
|
-
.join(", ");
|
|
27
|
-
};
|
|
28
|
-
exports.generateAuthorization = generateAuthorization;
|
|
29
|
-
const uuid = () => node_crypto_1.default.randomUUID();
|
|
30
|
-
exports.uuid = uuid;
|
|
31
|
-
const branch = () => "z9hG4bK-" + (0, exports.uuid)();
|
|
32
|
-
exports.branch = branch;
|
|
33
|
-
const randomInt = () => Math.floor(Math.random() * (65535 - 1024 + 1)) + 1024;
|
|
34
|
-
exports.randomInt = randomInt;
|
|
35
|
-
const withoutTag = (s) => s.replace(/;tag=.*$/, "");
|
|
36
|
-
exports.withoutTag = withoutTag;
|
|
37
|
-
const extractAddress = (s) => s.match(/<(sip:.+?)>/)?.[1];
|
|
38
|
-
exports.extractAddress = extractAddress;
|
|
39
|
-
const keyAndSalt = node_crypto_1.default.randomBytes(30);
|
|
40
|
-
exports.localKey = keyAndSalt.toString("base64").replace(/=+$/, "");
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import CallSession from "./index.js";
|
|
2
|
-
import { type InboundMessage } from "../sip-message/index.js";
|
|
3
|
-
import type Softphone from "../index.js";
|
|
4
|
-
declare class InboundCallSession extends CallSession {
|
|
5
|
-
constructor(softphone: Softphone, inviteMessage: InboundMessage);
|
|
6
|
-
answer(): Promise<void>;
|
|
7
|
-
}
|
|
8
|
-
export default InboundCallSession;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import CallSession from "./index.js";
|
|
2
|
-
import { OutboundMessage } from "../sip-message/index.js";
|
|
3
|
-
import { localKey, randomInt } from "../utils.js";
|
|
4
|
-
class InboundCallSession extends CallSession {
|
|
5
|
-
constructor(softphone, inviteMessage) {
|
|
6
|
-
super(softphone, inviteMessage);
|
|
7
|
-
this.localPeer = inviteMessage.headers.To;
|
|
8
|
-
this.remotePeer = inviteMessage.headers.From;
|
|
9
|
-
// inbound call from call queue, invite message may not have body
|
|
10
|
-
if (inviteMessage.body.length > 0) {
|
|
11
|
-
this.remoteKey = inviteMessage.body.match(/AES_CM_128_HMAC_SHA1_80 inline:([\w+/]+)/)[1];
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
async answer() {
|
|
15
|
-
const answerSDP = `
|
|
16
|
-
v=0
|
|
17
|
-
o=- ${Date.now()} 0 IN IP4 ${this.softphone.client.localAddress}
|
|
18
|
-
s=rc-softphone-ts
|
|
19
|
-
c=IN IP4 ${this.softphone.client.localAddress}
|
|
20
|
-
t=0 0
|
|
21
|
-
m=audio ${randomInt()} RTP/SAVP ${this.softphone.codec.id} 101
|
|
22
|
-
a=rtpmap:${this.softphone.codec.id} ${this.softphone.codec.name}
|
|
23
|
-
a=rtpmap:101 telephone-event/8000
|
|
24
|
-
a=fmtp:101 0-15
|
|
25
|
-
a=sendrecv
|
|
26
|
-
a=crypto:1 AES_CM_128_HMAC_SHA1_80 inline:${localKey}
|
|
27
|
-
`.trim();
|
|
28
|
-
this.sdp = answerSDP;
|
|
29
|
-
const newMessage = new OutboundMessage("SIP/2.0 200 OK", {
|
|
30
|
-
Via: this.sipMessage.headers.Via,
|
|
31
|
-
"Call-ID": this.sipMessage.getHeader("Call-ID"),
|
|
32
|
-
From: this.sipMessage.headers.From,
|
|
33
|
-
To: this.sipMessage.headers.To,
|
|
34
|
-
CSeq: this.sipMessage.headers.CSeq,
|
|
35
|
-
Contact: `<sip:${this.softphone.sipInfo.username}@${this.softphone.client.localAddress}:${this.softphone.client.localPort};transport=TLS;ob>`,
|
|
36
|
-
Allow: "PRACK, INVITE, ACK, BYE, CANCEL, UPDATE, INFO, SUBSCRIBE, NOTIFY, REFER, MESSAGE, OPTIONS",
|
|
37
|
-
Supported: "replaces, 100rel, timer, norefersub",
|
|
38
|
-
"Session-Expires": "14400;refresher=uac",
|
|
39
|
-
Require: "timer",
|
|
40
|
-
"Content-Type": "application/sdp",
|
|
41
|
-
}, answerSDP);
|
|
42
|
-
const ackMessage = await this.softphone.send(newMessage, true);
|
|
43
|
-
// for inbound call from call queue, ack message may HAVE body (while invite message has no body)
|
|
44
|
-
if (ackMessage.body.length > 0) {
|
|
45
|
-
this.remoteIP = ackMessage.body.match(/c=IN IP4 ([\d.]+)/)[1];
|
|
46
|
-
this.remotePort = parseInt(ackMessage.body.match(/m=audio (\d+) /)[1], 10);
|
|
47
|
-
this.remoteKey = ackMessage.body.match(/AES_CM_128_HMAC_SHA1_80 inline:([\w+/]+)/)[1];
|
|
48
|
-
}
|
|
49
|
-
this.startLocalServices();
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
export default InboundCallSession;
|
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
import dgram from "node:dgram";
|
|
2
|
-
import EventEmitter from "node:events";
|
|
3
|
-
import { Buffer } from "node:buffer";
|
|
4
|
-
import { RtpHeader, RtpPacket, SrtpSession } from "werift-rtp";
|
|
5
|
-
import DTMF from "../dtmf.js";
|
|
6
|
-
import { RequestMessage, ResponseMessage, } from "../sip-message/index.js";
|
|
7
|
-
import { branch, extractAddress, localKey, randomInt } from "../utils.js";
|
|
8
|
-
import Streamer from "./streamer.js";
|
|
9
|
-
import waitFor from "wait-for-async";
|
|
10
|
-
class CallSession extends EventEmitter {
|
|
11
|
-
softphone;
|
|
12
|
-
sipMessage;
|
|
13
|
-
socket;
|
|
14
|
-
localPeer;
|
|
15
|
-
remotePeer;
|
|
16
|
-
remoteIP;
|
|
17
|
-
remotePort;
|
|
18
|
-
disposed = false;
|
|
19
|
-
srtpSession;
|
|
20
|
-
encoder;
|
|
21
|
-
decoder;
|
|
22
|
-
sdp;
|
|
23
|
-
// for audio streaming
|
|
24
|
-
ssrc = randomInt();
|
|
25
|
-
sequenceNumber = randomInt();
|
|
26
|
-
timestamp = randomInt();
|
|
27
|
-
constructor(softphone, sipMessage) {
|
|
28
|
-
super();
|
|
29
|
-
this.softphone = softphone;
|
|
30
|
-
this.encoder = softphone.codec.createEncoder();
|
|
31
|
-
this.decoder = softphone.codec.createDecoder();
|
|
32
|
-
this.sipMessage = sipMessage;
|
|
33
|
-
// inbound call from call queue, invite message may not have body
|
|
34
|
-
if (this.sipMessage.body.length > 0) {
|
|
35
|
-
this.remoteIP = this.sipMessage.body.match(/c=IN IP4 ([\d.]+)/)[1];
|
|
36
|
-
this.remotePort = parseInt(this.sipMessage.body.match(/m=audio (\d+) /)[1], 10);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
set remoteKey(key) {
|
|
40
|
-
const localKeyBuffer = Buffer.from(localKey, "base64");
|
|
41
|
-
const remoteKeyBuffer = Buffer.from(key, "base64");
|
|
42
|
-
this.srtpSession = new SrtpSession({
|
|
43
|
-
profile: 0x0001,
|
|
44
|
-
keys: {
|
|
45
|
-
localMasterKey: localKeyBuffer.subarray(0, 16),
|
|
46
|
-
localMasterSalt: localKeyBuffer.subarray(16, 30),
|
|
47
|
-
remoteMasterKey: remoteKeyBuffer.subarray(0, 16),
|
|
48
|
-
remoteMasterSalt: remoteKeyBuffer.subarray(16, 30),
|
|
49
|
-
},
|
|
50
|
-
});
|
|
51
|
-
}
|
|
52
|
-
get callId() {
|
|
53
|
-
return this.sipMessage.getHeader("Call-ID");
|
|
54
|
-
}
|
|
55
|
-
send(data) {
|
|
56
|
-
this.socket.send(data, this.remotePort, this.remoteIP);
|
|
57
|
-
}
|
|
58
|
-
async hangup() {
|
|
59
|
-
const requestMessage = new RequestMessage(`BYE sip:${this.softphone.sipInfo.domain} SIP/2.0`, {
|
|
60
|
-
"Call-ID": this.callId,
|
|
61
|
-
From: this.localPeer,
|
|
62
|
-
To: this.remotePeer,
|
|
63
|
-
Via: `SIP/2.0/TLS ${this.softphone.fakeDomain};branch=${branch()}`,
|
|
64
|
-
});
|
|
65
|
-
await this.softphone.send(requestMessage);
|
|
66
|
-
}
|
|
67
|
-
sendDTMF(char) {
|
|
68
|
-
const payloads = DTMF.charToPayloads(char);
|
|
69
|
-
const timestamp = this.timestamp;
|
|
70
|
-
let first = true;
|
|
71
|
-
for (const payload of payloads) {
|
|
72
|
-
const rtpHeader = new RtpHeader({
|
|
73
|
-
version: 2,
|
|
74
|
-
padding: false,
|
|
75
|
-
paddingSize: 0,
|
|
76
|
-
extension: false,
|
|
77
|
-
marker: first,
|
|
78
|
-
payloadOffset: 12,
|
|
79
|
-
payloadType: 101,
|
|
80
|
-
sequenceNumber: this.sequenceNumber,
|
|
81
|
-
timestamp,
|
|
82
|
-
ssrc: this.ssrc,
|
|
83
|
-
csrcLength: 0,
|
|
84
|
-
csrc: [],
|
|
85
|
-
extensionProfile: 48862,
|
|
86
|
-
extensionLength: undefined,
|
|
87
|
-
extensions: [],
|
|
88
|
-
});
|
|
89
|
-
const rtpPacket = new RtpPacket(rtpHeader, payload);
|
|
90
|
-
this.send(this.srtpSession.encrypt(rtpPacket.payload, rtpPacket.header));
|
|
91
|
-
this.sequenceNumber = (this.sequenceNumber + 1) % 65536;
|
|
92
|
-
first = false;
|
|
93
|
-
}
|
|
94
|
-
this.timestamp += 800;
|
|
95
|
-
}
|
|
96
|
-
async sendDTMFs(s, delay = 500) {
|
|
97
|
-
for (const c of s) {
|
|
98
|
-
this.sendDTMF(c);
|
|
99
|
-
await waitFor({ interval: delay });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
// buffer is the content of a audio file, it is supposed to be uncompressed PCM data
|
|
103
|
-
// The audio should be playable by command: play -t raw -b 16 -r 16000 -e signed-integer test.wav
|
|
104
|
-
streamAudio(input) {
|
|
105
|
-
const streamer = new Streamer(this, input);
|
|
106
|
-
streamer.start();
|
|
107
|
-
return streamer;
|
|
108
|
-
}
|
|
109
|
-
// send a single rtp packet
|
|
110
|
-
sendPacket(rtpPacket) {
|
|
111
|
-
if (this.disposed) {
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
this.send(this.srtpSession.encrypt(rtpPacket.payload, rtpPacket.header));
|
|
115
|
-
}
|
|
116
|
-
startLocalServices() {
|
|
117
|
-
this.socket = dgram.createSocket("udp4");
|
|
118
|
-
this.socket.on("message", (message) => {
|
|
119
|
-
const rtpPacket = RtpPacket.deSerialize(this.srtpSession.decrypt(message));
|
|
120
|
-
this.emit("rtpPacket", rtpPacket);
|
|
121
|
-
if (rtpPacket.header.payloadType === 101) {
|
|
122
|
-
this.emit("dtmfPacket", rtpPacket);
|
|
123
|
-
const char = DTMF.payloadToChar(rtpPacket.payload);
|
|
124
|
-
if (char) {
|
|
125
|
-
this.emit("dtmf", char);
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
else if (rtpPacket.header.payloadType === this.softphone.codec.id) {
|
|
129
|
-
if (rtpPacket.payload.length === 4 &&
|
|
130
|
-
rtpPacket.payload[0] >= 0x00 &&
|
|
131
|
-
rtpPacket.payload[0] < 0x0c &&
|
|
132
|
-
rtpPacket.payload[1] === 0x8a &&
|
|
133
|
-
rtpPacket.payload[2] === 0x03 &&
|
|
134
|
-
rtpPacket.payload[3] === 0xc0) {
|
|
135
|
-
// special DTMF packet in audio format
|
|
136
|
-
// first byte 0x00 to 0x0c means DTMF 0 to 9, *, #
|
|
137
|
-
// we ignore it since DTMF is handled by `if (rtpPacket.header.payloadType === 101) {`
|
|
138
|
-
return; // ignore it
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
rtpPacket.payload = this.decoder.decode(rtpPacket.payload);
|
|
142
|
-
this.emit("audioPacket", rtpPacket);
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
console.error("Audio packet decode failed", rtpPacket);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
// as I tested, we can use a random port here and it still works
|
|
150
|
-
// but it seems that in SDP we need to tell remote our local IP Address, not 127.0.0.1
|
|
151
|
-
this.socket.bind(); // random port
|
|
152
|
-
// send a message to remote server so that it knows where to reply
|
|
153
|
-
this.send("hello");
|
|
154
|
-
const byeHandler = (inboundMessage) => {
|
|
155
|
-
if (inboundMessage.getHeader("Call-ID") !== this.callId) {
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
if (inboundMessage.headers.CSeq.endsWith(" BYE")) {
|
|
159
|
-
this.softphone.off("message", byeHandler);
|
|
160
|
-
this.dispose();
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
this.softphone.on("message", byeHandler);
|
|
164
|
-
}
|
|
165
|
-
dispose() {
|
|
166
|
-
this.disposed = true;
|
|
167
|
-
this.emit("disposed");
|
|
168
|
-
this.removeAllListeners();
|
|
169
|
-
this.socket?.removeAllListeners();
|
|
170
|
-
this.socket?.close();
|
|
171
|
-
}
|
|
172
|
-
async transfer(transferTo) {
|
|
173
|
-
const requestMessage = new RequestMessage(`REFER sip:${this.softphone.sipInfo.username}@${this.softphone.sipInfo.outboundProxy};transport=tls SIP/2.0`, {
|
|
174
|
-
Via: `SIP/2.0/TLS ${this.softphone.client.localAddress}:${this.softphone.client.localPort};rport;branch=${branch()};alias`,
|
|
175
|
-
"Max-Forwards": 70,
|
|
176
|
-
From: this.localPeer,
|
|
177
|
-
To: this.remotePeer,
|
|
178
|
-
Contact: `<sip:${this.softphone.sipInfo.username}@${this.softphone.client.localAddress}:${this.softphone.client.localPort};transport=TLS;ob>`,
|
|
179
|
-
"Call-ID": this.callId,
|
|
180
|
-
Event: "refer",
|
|
181
|
-
Expires: 600,
|
|
182
|
-
Supported: "replaces, 100rel, timer, norefersub",
|
|
183
|
-
Accept: "message/sipfrag;version=2.0",
|
|
184
|
-
"Allow-Events": "presence, message-summary, refer",
|
|
185
|
-
"Refer-To": `sip:${transferTo}@${this.softphone.sipInfo.domain}`,
|
|
186
|
-
"Referred-By": `<sip:${this.softphone.sipInfo.username}@${this.softphone.sipInfo.domain}>`,
|
|
187
|
-
});
|
|
188
|
-
await this.softphone.send(requestMessage);
|
|
189
|
-
return new Promise((resolve) => {
|
|
190
|
-
const notifyHandler = (inboundMessage) => {
|
|
191
|
-
if (!inboundMessage.subject.startsWith("NOTIFY ")) {
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
const responseMessage = new ResponseMessage(inboundMessage, 200);
|
|
195
|
-
this.softphone.send(responseMessage);
|
|
196
|
-
if (inboundMessage.body.trim() === "SIP/2.0 200 OK") {
|
|
197
|
-
this.softphone.off("message", notifyHandler);
|
|
198
|
-
resolve();
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
this.softphone.on("message", notifyHandler);
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
async toggleReceive(toReceive) {
|
|
205
|
-
let newSDP = this.sdp;
|
|
206
|
-
if (!toReceive) {
|
|
207
|
-
newSDP = newSDP.replace(/a=sendrecv/, "a=sendonly");
|
|
208
|
-
}
|
|
209
|
-
const requestMessage = new RequestMessage(`INVITE ${extractAddress(this.remotePeer)} SIP/2.0`, {
|
|
210
|
-
"Call-Id": this.callId,
|
|
211
|
-
From: this.localPeer,
|
|
212
|
-
To: this.remotePeer,
|
|
213
|
-
Via: `SIP/2.0/TLS ${this.softphone.client.localAddress}:${this.softphone.client.localPort};rport;branch=${branch()};alias`,
|
|
214
|
-
"Content-Type": "application/sdp",
|
|
215
|
-
Contact: ` <sip:${this.softphone.sipInfo.username}@${this.softphone.client.localAddress}:${this.softphone.client.localPort};transport=TLS;ob>`,
|
|
216
|
-
}, newSDP);
|
|
217
|
-
const replyMessage = await this.softphone.send(requestMessage, true);
|
|
218
|
-
const ackMessage = new RequestMessage(`ACK ${extractAddress(this.remotePeer)} SIP/2.0`, {
|
|
219
|
-
"Call-Id": this.callId,
|
|
220
|
-
From: this.localPeer,
|
|
221
|
-
To: this.remotePeer,
|
|
222
|
-
Via: replyMessage.headers.Via,
|
|
223
|
-
CSeq: replyMessage.headers.CSeq.replace(" INVITE", " ACK"),
|
|
224
|
-
});
|
|
225
|
-
await this.softphone.send(ackMessage);
|
|
226
|
-
}
|
|
227
|
-
async hold() {
|
|
228
|
-
return this.toggleReceive(false);
|
|
229
|
-
}
|
|
230
|
-
async unhold() {
|
|
231
|
-
return this.toggleReceive(true);
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
export default CallSession;
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import CallSession from "./index.js";
|
|
2
|
-
import { type InboundMessage } from "../sip-message/index.js";
|
|
3
|
-
import type Softphone from "../index.js";
|
|
4
|
-
declare class OutboundCallSession extends CallSession {
|
|
5
|
-
constructor(softphone: Softphone, answerMessage: InboundMessage);
|
|
6
|
-
init(): void;
|
|
7
|
-
cancel(): Promise<void>;
|
|
8
|
-
get sessionId(): string;
|
|
9
|
-
get partyId(): string;
|
|
10
|
-
}
|
|
11
|
-
export default OutboundCallSession;
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import CallSession from "./index.js";
|
|
2
|
-
import { RequestMessage } from "../sip-message/index.js";
|
|
3
|
-
import { extractAddress, withoutTag } from "../utils.js";
|
|
4
|
-
class OutboundCallSession extends CallSession {
|
|
5
|
-
constructor(softphone, answerMessage) {
|
|
6
|
-
super(softphone, answerMessage);
|
|
7
|
-
this.localPeer = answerMessage.headers.From;
|
|
8
|
-
this.remotePeer = answerMessage.headers.To;
|
|
9
|
-
this.remoteKey = answerMessage.body.match(/AES_CM_128_HMAC_SHA1_80 inline:([\w+/]+)/)[1];
|
|
10
|
-
this.init();
|
|
11
|
-
}
|
|
12
|
-
init() {
|
|
13
|
-
// wait for user to answer the call
|
|
14
|
-
const answerHandler = (message) => {
|
|
15
|
-
if (message.headers.CSeq !== this.sipMessage.headers.CSeq) {
|
|
16
|
-
return;
|
|
17
|
-
}
|
|
18
|
-
if (message.subject.startsWith("SIP/2.0 486")) {
|
|
19
|
-
this.softphone.off("message", answerHandler);
|
|
20
|
-
this.emit("busy");
|
|
21
|
-
this.dispose();
|
|
22
|
-
return;
|
|
23
|
-
}
|
|
24
|
-
if (message.subject.startsWith("SIP/2.0 200")) {
|
|
25
|
-
this.softphone.off("message", answerHandler);
|
|
26
|
-
this.emit("answered");
|
|
27
|
-
const ackMessage = new RequestMessage(`ACK ${extractAddress(this.remotePeer)} SIP/2.0`, {
|
|
28
|
-
"Call-ID": this.callId,
|
|
29
|
-
From: this.localPeer,
|
|
30
|
-
To: this.remotePeer,
|
|
31
|
-
Via: this.sipMessage.headers.Via,
|
|
32
|
-
CSeq: this.sipMessage.headers.CSeq.replace(" INVITE", " ACK"),
|
|
33
|
-
});
|
|
34
|
-
this.softphone.send(ackMessage);
|
|
35
|
-
}
|
|
36
|
-
};
|
|
37
|
-
this.softphone.on("message", answerHandler);
|
|
38
|
-
this.once("answered", () => this.startLocalServices());
|
|
39
|
-
}
|
|
40
|
-
async cancel() {
|
|
41
|
-
const requestMessage = new RequestMessage(`CANCEL ${extractAddress(this.remotePeer)} SIP/2.0`, {
|
|
42
|
-
"Call-ID": this.callId,
|
|
43
|
-
From: this.localPeer,
|
|
44
|
-
To: withoutTag(this.remotePeer),
|
|
45
|
-
Via: this.sipMessage.headers.Via,
|
|
46
|
-
CSeq: this.sipMessage.headers.CSeq.replace(" INVITE", " CANCEL"),
|
|
47
|
-
});
|
|
48
|
-
await this.softphone.send(requestMessage);
|
|
49
|
-
}
|
|
50
|
-
get sessionId() {
|
|
51
|
-
const header = this.sipMessage.headers["p-rc-api-ids"];
|
|
52
|
-
let match = header.match(/party-id=([^;]+);session-id=([^;]+)/);
|
|
53
|
-
return match[2];
|
|
54
|
-
}
|
|
55
|
-
get partyId() {
|
|
56
|
-
const header = this.sipMessage.headers["p-rc-api-ids"];
|
|
57
|
-
let match = header.match(/party-id=([^;]+);session-id=([^;]+)/);
|
|
58
|
-
return match[1];
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
export default OutboundCallSession;
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
import EventEmitter from "node:events";
|
|
2
|
-
import { Buffer } from "node:buffer";
|
|
3
|
-
import { RtpHeader, RtpPacket } from "werift-rtp";
|
|
4
|
-
class Streamer extends EventEmitter {
|
|
5
|
-
paused = false;
|
|
6
|
-
callSession;
|
|
7
|
-
buffer;
|
|
8
|
-
originalBuffer;
|
|
9
|
-
constructor(callSesstion, buffer) {
|
|
10
|
-
super();
|
|
11
|
-
this.callSession = callSesstion;
|
|
12
|
-
this.buffer = buffer;
|
|
13
|
-
this.originalBuffer = buffer;
|
|
14
|
-
}
|
|
15
|
-
start() {
|
|
16
|
-
this.buffer = this.originalBuffer;
|
|
17
|
-
this.paused = false;
|
|
18
|
-
this.sendPacket();
|
|
19
|
-
}
|
|
20
|
-
stop() {
|
|
21
|
-
this.buffer = Buffer.alloc(0);
|
|
22
|
-
}
|
|
23
|
-
pause() {
|
|
24
|
-
this.paused = true;
|
|
25
|
-
}
|
|
26
|
-
resume() {
|
|
27
|
-
this.paused = false;
|
|
28
|
-
this.sendPacket();
|
|
29
|
-
}
|
|
30
|
-
get finished() {
|
|
31
|
-
return this.callSession.disposed ||
|
|
32
|
-
this.buffer.length < this.callSession.softphone.codec.packetSize;
|
|
33
|
-
}
|
|
34
|
-
sendPacket() {
|
|
35
|
-
if (!this.paused && !this.finished) {
|
|
36
|
-
const temp = this.callSession.encoder.encode(this.buffer.subarray(0, this.callSession.softphone.codec.packetSize));
|
|
37
|
-
const rtpPacket = new RtpPacket(new RtpHeader({
|
|
38
|
-
version: 2,
|
|
39
|
-
padding: false,
|
|
40
|
-
paddingSize: 0,
|
|
41
|
-
extension: false,
|
|
42
|
-
marker: false,
|
|
43
|
-
payloadOffset: 12,
|
|
44
|
-
payloadType: this.callSession.softphone.codec.id,
|
|
45
|
-
sequenceNumber: this.callSession.sequenceNumber,
|
|
46
|
-
timestamp: this.callSession.timestamp,
|
|
47
|
-
ssrc: this.callSession.ssrc,
|
|
48
|
-
csrcLength: 0,
|
|
49
|
-
csrc: [],
|
|
50
|
-
extensionProfile: 48862,
|
|
51
|
-
extensionLength: undefined,
|
|
52
|
-
extensions: [],
|
|
53
|
-
}), temp);
|
|
54
|
-
this.callSession.send(this.callSession.srtpSession.encrypt(rtpPacket.payload, rtpPacket.header));
|
|
55
|
-
this.callSession.sequenceNumber += 1;
|
|
56
|
-
if (this.callSession.sequenceNumber > 65535) {
|
|
57
|
-
this.callSession.sequenceNumber = 0;
|
|
58
|
-
}
|
|
59
|
-
this.callSession.timestamp +=
|
|
60
|
-
this.callSession.softphone.codec.timestampInterval;
|
|
61
|
-
this.buffer = this.buffer.subarray(this.callSession.softphone.codec.packetSize);
|
|
62
|
-
if (this.finished) {
|
|
63
|
-
this.emit("finished");
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
setTimeout(() => this.sendPacket(), 20);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
export default Streamer;
|
package/dist/esm/codec.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
declare class Codec {
|
|
3
|
-
id: number;
|
|
4
|
-
name: "OPUS/16000" | "OPUS/48000/2" | "PCMU/8000";
|
|
5
|
-
packetSize: number;
|
|
6
|
-
timestampInterval: number;
|
|
7
|
-
createEncoder: () => {
|
|
8
|
-
encode: (pcm: Buffer) => Buffer;
|
|
9
|
-
};
|
|
10
|
-
createDecoder: () => {
|
|
11
|
-
decode: (audio: Buffer) => Buffer;
|
|
12
|
-
};
|
|
13
|
-
constructor(name: "OPUS/16000" | "OPUS/48000/2" | "PCMU/8000");
|
|
14
|
-
}
|
|
15
|
-
export default Codec;
|
package/dist/esm/codec.js
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
import { Decoder, Encoder } from "@evan/opus";
|
|
3
|
-
class Codec {
|
|
4
|
-
id;
|
|
5
|
-
name;
|
|
6
|
-
packetSize;
|
|
7
|
-
timestampInterval;
|
|
8
|
-
createEncoder;
|
|
9
|
-
createDecoder;
|
|
10
|
-
constructor(name) {
|
|
11
|
-
this.name = name;
|
|
12
|
-
switch (name) {
|
|
13
|
-
case "OPUS/16000": {
|
|
14
|
-
this.createEncoder = () => {
|
|
15
|
-
const encoder = new Encoder({ channels: 1, sample_rate: 16000 });
|
|
16
|
-
return { encode: (pcm) => Buffer.from(encoder.encode(pcm)) };
|
|
17
|
-
};
|
|
18
|
-
this.createDecoder = () => {
|
|
19
|
-
const decoder = new Decoder({ channels: 1, sample_rate: 16000 });
|
|
20
|
-
return {
|
|
21
|
-
decode: (opus) => Buffer.from(decoder.decode(opus)),
|
|
22
|
-
};
|
|
23
|
-
};
|
|
24
|
-
this.id = 109;
|
|
25
|
-
this.packetSize = 640;
|
|
26
|
-
this.timestampInterval = 320;
|
|
27
|
-
break;
|
|
28
|
-
}
|
|
29
|
-
case "OPUS/48000/2": {
|
|
30
|
-
this.createEncoder = () => {
|
|
31
|
-
const encoder = new Encoder({ channels: 2, sample_rate: 48000 });
|
|
32
|
-
return { encode: (pcm) => Buffer.from(encoder.encode(pcm)) };
|
|
33
|
-
};
|
|
34
|
-
this.createDecoder = () => {
|
|
35
|
-
const decoder = new Decoder({ channels: 2, sample_rate: 48000 });
|
|
36
|
-
return {
|
|
37
|
-
decode: (opus) => Buffer.from(decoder.decode(opus)),
|
|
38
|
-
};
|
|
39
|
-
};
|
|
40
|
-
this.id = 111;
|
|
41
|
-
this.packetSize = 3840;
|
|
42
|
-
this.timestampInterval = 960;
|
|
43
|
-
break;
|
|
44
|
-
}
|
|
45
|
-
case "PCMU/8000": {
|
|
46
|
-
this.createEncoder = () => {
|
|
47
|
-
return { encode: (pcm) => pcm };
|
|
48
|
-
};
|
|
49
|
-
this.createDecoder = () => {
|
|
50
|
-
return { decode: (audio) => audio };
|
|
51
|
-
};
|
|
52
|
-
this.id = 0;
|
|
53
|
-
this.packetSize = 160;
|
|
54
|
-
this.timestampInterval = 160;
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
default: {
|
|
58
|
-
throw new Error(`unsupported codec: ${name}`);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
export default Codec;
|
package/dist/esm/dtmf.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
declare class DTMF {
|
|
3
|
-
static readonly phoneChars: string[];
|
|
4
|
-
private static readonly payloads;
|
|
5
|
-
static charToPayloads: (char: string) => Buffer<ArrayBuffer>[];
|
|
6
|
-
static payloadToChar: (payload: Buffer) => string;
|
|
7
|
-
}
|
|
8
|
-
export default DTMF;
|
package/dist/esm/dtmf.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { Buffer } from "node:buffer";
|
|
2
|
-
class DTMF {
|
|
3
|
-
static phoneChars = [
|
|
4
|
-
"0",
|
|
5
|
-
"1",
|
|
6
|
-
"2",
|
|
7
|
-
"3",
|
|
8
|
-
"4",
|
|
9
|
-
"5",
|
|
10
|
-
"6",
|
|
11
|
-
"7",
|
|
12
|
-
"8",
|
|
13
|
-
"9",
|
|
14
|
-
"*",
|
|
15
|
-
"#",
|
|
16
|
-
];
|
|
17
|
-
static payloads = [
|
|
18
|
-
0x00060000,
|
|
19
|
-
0x000600a0,
|
|
20
|
-
0x00060140,
|
|
21
|
-
0x00860320,
|
|
22
|
-
0x00860320,
|
|
23
|
-
0x00860320,
|
|
24
|
-
];
|
|
25
|
-
static charToPayloads = (char) => {
|
|
26
|
-
const index = DTMF.phoneChars.indexOf(char[0]);
|
|
27
|
-
if (index === -1) {
|
|
28
|
-
throw new Error("invalid phone char");
|
|
29
|
-
}
|
|
30
|
-
return DTMF.payloads.map((payload) => {
|
|
31
|
-
const temp = payload + index * 0x01000000;
|
|
32
|
-
const buffer = Buffer.alloc(4);
|
|
33
|
-
buffer.writeIntBE(temp, 0, 4);
|
|
34
|
-
return buffer;
|
|
35
|
-
});
|
|
36
|
-
};
|
|
37
|
-
static payloadToChar = (payload) => {
|
|
38
|
-
const intBE = payload.readIntBE(0, 4);
|
|
39
|
-
const index = (intBE - 0x00060000) / 0x01000000;
|
|
40
|
-
return DTMF.phoneChars[index];
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
export default DTMF;
|
package/dist/esm/index.d.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import EventEmitter from "node:events";
|
|
2
|
-
import { TLSSocket } from "node:tls";
|
|
3
|
-
import InboundCallSession from "./call-session/inbound.js";
|
|
4
|
-
import OutboundCallSession from "./call-session/outbound.js";
|
|
5
|
-
import { InboundMessage, OutboundMessage } from "./sip-message/index.js";
|
|
6
|
-
import { SoftPhoneOptions } from "./types.js";
|
|
7
|
-
import Codec from "./codec.js";
|
|
8
|
-
declare class Softphone extends EventEmitter {
|
|
9
|
-
sipInfo: SoftPhoneOptions;
|
|
10
|
-
client: TLSSocket;
|
|
11
|
-
codec: Codec;
|
|
12
|
-
fakeDomain: string;
|
|
13
|
-
fakeEmail: string;
|
|
14
|
-
private intervalHandle?;
|
|
15
|
-
private connected;
|
|
16
|
-
constructor(sipInfo: SoftPhoneOptions);
|
|
17
|
-
private instanceId;
|
|
18
|
-
private registerCallId;
|
|
19
|
-
register(): Promise<void>;
|
|
20
|
-
enableDebugMode(): void;
|
|
21
|
-
revoke(): void;
|
|
22
|
-
send(message: OutboundMessage, waitForReply?: true): Promise<InboundMessage>;
|
|
23
|
-
send(message: OutboundMessage, waitForReply?: false): Promise<undefined>;
|
|
24
|
-
answer(inviteMessage: InboundMessage): Promise<InboundCallSession>;
|
|
25
|
-
decline(inviteMessage: InboundMessage): Promise<void>;
|
|
26
|
-
call(callee: string): Promise<OutboundCallSession>;
|
|
27
|
-
}
|
|
28
|
-
export default Softphone;
|