@areumtecnologia/wasockets 2.0.0 → 2.2.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 +1929 -1649
- package/lib/Defaults/baileys-version.json +1 -1
- package/lib/Defaults/connection.js +14 -1
- package/lib/Socket/index.js +51 -48
- package/lib/Socket/messages-recv.js +74 -13
- package/lib/Socket/messages-send.js +50 -25
- package/lib/Socket/socket.js +13 -0
- package/lib/Socket/voip.js +123 -0
- package/lib/Store/make-cache-manager-store.js +14 -5
- package/lib/Types/index.d.ts +4 -0
- package/lib/Utils/auth-utils.js +16 -1
- package/lib/Utils/browser-utils.js +6 -2
- package/lib/Utils/generics.js +47 -8
- package/lib/Utils/index.d.ts +1 -0
- package/lib/Utils/messages-media.js +44 -21
- package/lib/Utils/pre-key-manager.js +16 -1
- package/lib/Utils/validate-connection.js +10 -11
- package/lib/VoIP/VoIPCrypto.js +150 -0
- package/lib/VoIP/VoIPManager.js +469 -0
- package/lib/VoIP/VoIPMediaEngine.js +224 -0
- package/lib/VoIP/VoIPProtocol.js +354 -0
- package/lib/VoIP/VoIPRelayTransport.js +173 -0
- package/lib/VoIP/VoIPSession.js +314 -0
- package/lib/VoIP/assets/wasm/loader.js +5 -0
- package/lib/VoIP/assets/wasm/whatsapp.wasm +0 -0
- package/lib/VoIP/assets/wasm/worker-modules.js +305 -0
- package/lib/VoIP/engine/audio-feeder.d.mts +15 -0
- package/lib/VoIP/engine/audio-feeder.mjs +243 -0
- package/lib/VoIP/engine/index.d.mts +91 -0
- package/lib/VoIP/engine/index.mjs +1683 -0
- package/lib/VoIP/engine/relay-transport.d.mts +43 -0
- package/lib/VoIP/engine/relay-transport.mjs +607 -0
- package/lib/VoIP/engine/signaling.d.mts +44 -0
- package/lib/VoIP/engine/signaling.mjs +893 -0
- package/lib/VoIP/engine/types.d.mts +183 -0
- package/lib/VoIP/engine/types.mjs +17 -0
- package/lib/VoIP/engine/wasm-engine.d.mts +103 -0
- package/lib/VoIP/engine/wasm-engine.mjs +1397 -0
- package/lib/VoIP/engine/worker-bootstrap.mjs +1049 -0
- package/lib/VoIP/index.js +51 -0
- package/lib/VoIP/utils/audio.d.ts +28 -0
- package/lib/VoIP/utils/audio.js +148 -0
- package/lib/WABinary/generic-utils.js +22 -6
- package/lib/index.d.ts +382 -274
- package/lib/index.js +5 -0
- package/package.json +28 -32
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
|
|
5
|
+
const { EventEmitter } = require("events");
|
|
6
|
+
const { deriveVoIPKeys } = require("./VoIPCrypto");
|
|
7
|
+
const { VoIPRelayTransport } = require("./VoIPRelayTransport");
|
|
8
|
+
const { VoIPMediaEngine } = require("./VoIPMediaEngine");
|
|
9
|
+
const { buildAcceptStanza, buildRejectStanza, buildTerminateStanza } = require("./VoIPProtocol");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Call States
|
|
13
|
+
*/
|
|
14
|
+
const CallState = {
|
|
15
|
+
OFFERING: "offering",
|
|
16
|
+
RINGING: "ringing",
|
|
17
|
+
ACCEPTED: "accepted",
|
|
18
|
+
CONNECTED: "connected",
|
|
19
|
+
REJECTED: "rejected",
|
|
20
|
+
TERMINATED: "terminated"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* WhatsApp VoIP Active Call Session
|
|
25
|
+
*/
|
|
26
|
+
class VoIPSession extends EventEmitter {
|
|
27
|
+
constructor({
|
|
28
|
+
callId,
|
|
29
|
+
callCreator,
|
|
30
|
+
peerJid,
|
|
31
|
+
direction = "inbound", // "inbound" | "outbound"
|
|
32
|
+
isVideo = false,
|
|
33
|
+
callKey = null,
|
|
34
|
+
relayEndpoints = [],
|
|
35
|
+
socketClient,
|
|
36
|
+
logger = console
|
|
37
|
+
}) {
|
|
38
|
+
super();
|
|
39
|
+
this.callId = callId;
|
|
40
|
+
this.callCreator = callCreator;
|
|
41
|
+
this.peerJid = peerJid;
|
|
42
|
+
this.direction = direction;
|
|
43
|
+
this.isVideo = isVideo;
|
|
44
|
+
this.callKey = callKey;
|
|
45
|
+
this.relayEndpoints = relayEndpoints;
|
|
46
|
+
this.socketClient = socketClient;
|
|
47
|
+
this.logger = logger;
|
|
48
|
+
|
|
49
|
+
this.state = direction === "inbound" ? CallState.RINGING : CallState.OFFERING;
|
|
50
|
+
this.startTime = null;
|
|
51
|
+
this.endTime = null;
|
|
52
|
+
this.relayTransport = null;
|
|
53
|
+
this.mediaEngine = null;
|
|
54
|
+
this.derivedKeys = null;
|
|
55
|
+
this.mediaStreamTimer = null;
|
|
56
|
+
|
|
57
|
+
if (this.callKey) {
|
|
58
|
+
this.initCrypto(this.callKey);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Dynamically adds or updates relay endpoints received from network stanzas
|
|
64
|
+
*/
|
|
65
|
+
addRelayEndpoints(endpoints) {
|
|
66
|
+
if (!endpoints || !endpoints.length) return;
|
|
67
|
+
let added = 0;
|
|
68
|
+
for (const ep of endpoints) {
|
|
69
|
+
if (!this.relayEndpoints.some(e => e.ip === ep.ip && e.port === ep.port)) {
|
|
70
|
+
this.relayEndpoints.push(ep);
|
|
71
|
+
added++;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (added > 0) {
|
|
75
|
+
this.logger.info?.({ count: this.relayEndpoints.length }, "Updated VoIP relay endpoints");
|
|
76
|
+
if ((this.state === CallState.ACCEPTED || this.state === CallState.CONNECTED) && (!this.relayTransport || !this.relayTransport.isConnected)) {
|
|
77
|
+
this.connectRelay().catch(err => {
|
|
78
|
+
this.logger.warn?.({ err: err?.message }, "Failed to connect relay on update");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Initializes SRTP keys derived from the 32-byte callKey
|
|
86
|
+
*/
|
|
87
|
+
initCrypto(callKey) {
|
|
88
|
+
this.callKey = Buffer.isBuffer(callKey) ? callKey : Buffer.from(callKey);
|
|
89
|
+
this.derivedKeys = deriveVoIPKeys(this.callKey);
|
|
90
|
+
|
|
91
|
+
// If we are the caller (outbound): TX = callerAudio, RX = calleeAudio
|
|
92
|
+
// If we are the callee (inbound): TX = calleeAudio, RX = callerAudio
|
|
93
|
+
const isCaller = this.direction === "outbound";
|
|
94
|
+
const txKeys = isCaller ? this.derivedKeys.callerAudio : this.derivedKeys.calleeAudio;
|
|
95
|
+
const rxKeys = isCaller ? this.derivedKeys.calleeAudio : this.derivedKeys.callerAudio;
|
|
96
|
+
|
|
97
|
+
this.mediaEngine = new VoIPMediaEngine({
|
|
98
|
+
txKey: txKeys.masterKey,
|
|
99
|
+
txSalt: txKeys.masterSalt,
|
|
100
|
+
rxKey: rxKeys.masterKey,
|
|
101
|
+
rxSalt: rxKeys.masterSalt,
|
|
102
|
+
logger: this.logger
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Forward media events
|
|
106
|
+
this.mediaEngine.on("audio", (audioData) => this.emit("audio", audioData));
|
|
107
|
+
this.mediaEngine.on("video", (videoData) => this.emit("video", videoData));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Connects to the primary Meta relay server
|
|
112
|
+
*/
|
|
113
|
+
async connectRelay() {
|
|
114
|
+
if (!this.relayEndpoints || this.relayEndpoints.length === 0) {
|
|
115
|
+
this.logger.warn?.({ callId: this.callId }, "No relay endpoints available, waiting for relay stanza");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const endpoint = this.relayEndpoints[0];
|
|
120
|
+
this.logger.info?.({ endpoint }, "Connecting to WhatsApp VoIP Relay server");
|
|
121
|
+
|
|
122
|
+
this.relayTransport = new VoIPRelayTransport({
|
|
123
|
+
ip: endpoint.ip,
|
|
124
|
+
port: endpoint.port,
|
|
125
|
+
token: endpoint.token,
|
|
126
|
+
logger: this.logger
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// When receiving UDP packets from relay, route to media engine
|
|
130
|
+
this.relayTransport.on("rtp", (rtpBuffer) => {
|
|
131
|
+
if (this.mediaEngine) {
|
|
132
|
+
this.mediaEngine.unpackPacket(rtpBuffer);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
this.relayTransport.on("relay.bound", () => {
|
|
137
|
+
this.logger.info?.({ callId: this.callId }, "Relay session successfully bound and ready");
|
|
138
|
+
this.setState(CallState.CONNECTED);
|
|
139
|
+
this.startActiveMedia();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await this.relayTransport.connect();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Starts continuous RTP comfort-noise transmission to open two-way relay media flow
|
|
147
|
+
*/
|
|
148
|
+
startActiveMedia() {
|
|
149
|
+
if (this.mediaStreamTimer) return;
|
|
150
|
+
const silence = VoIPMediaEngine.createSilenceFrame();
|
|
151
|
+
this.mediaStreamTimer = setInterval(() => {
|
|
152
|
+
if (this.state !== CallState.CONNECTED || !this.relayTransport?.isConnected) {
|
|
153
|
+
this.stopActiveMedia();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (this.mediaEngine && this.relayTransport) {
|
|
157
|
+
const srtpPacket = this.mediaEngine.packAudio(silence);
|
|
158
|
+
this.relayTransport.sendRtp(srtpPacket);
|
|
159
|
+
}
|
|
160
|
+
}, 20);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Stops continuous media stream
|
|
165
|
+
*/
|
|
166
|
+
stopActiveMedia() {
|
|
167
|
+
if (this.mediaStreamTimer) {
|
|
168
|
+
clearInterval(this.mediaStreamTimer);
|
|
169
|
+
this.mediaStreamTimer = null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Answers an incoming WhatsApp call
|
|
175
|
+
*/
|
|
176
|
+
async accept(options = {}) {
|
|
177
|
+
if (this.state !== CallState.RINGING && this.state !== CallState.OFFERING) {
|
|
178
|
+
throw new Error(`Cannot accept call in state: ${this.state}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.setState(CallState.ACCEPTED);
|
|
182
|
+
this.startTime = new Date();
|
|
183
|
+
|
|
184
|
+
const meId = this.socketClient.authState?.creds?.me?.id;
|
|
185
|
+
const meLid = this.socketClient.authState?.creds?.me?.lid;
|
|
186
|
+
const isLid = (this.callCreator && this.callCreator.endsWith("@lid")) || (this.peerJid && this.peerJid.endsWith("@lid"));
|
|
187
|
+
const myJid = (isLid && meLid) ? meLid : meId;
|
|
188
|
+
|
|
189
|
+
// Build and send <call><accept> stanza
|
|
190
|
+
const acceptStanza = buildAcceptStanza({
|
|
191
|
+
callId: this.callId,
|
|
192
|
+
callCreator: this.callCreator,
|
|
193
|
+
myJid,
|
|
194
|
+
isVideo: this.isVideo,
|
|
195
|
+
audioRate: options.audioRate || "16000"
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (this.socketClient.sendNode) {
|
|
199
|
+
await this.socketClient.sendNode(acceptStanza);
|
|
200
|
+
} else {
|
|
201
|
+
await this.socketClient.query(acceptStanza);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Connect relay transport if endpoints exist
|
|
205
|
+
if (this.relayEndpoints.length > 0) {
|
|
206
|
+
await this.connectRelay();
|
|
207
|
+
} else {
|
|
208
|
+
this.setState(CallState.CONNECTED);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return this;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Rejects an incoming WhatsApp call
|
|
216
|
+
*/
|
|
217
|
+
async reject(reason = "0") {
|
|
218
|
+
this.setState(CallState.REJECTED);
|
|
219
|
+
this.cleanup();
|
|
220
|
+
|
|
221
|
+
const myJid = this.socketClient.authState?.creds?.me?.id;
|
|
222
|
+
const rejectStanza = buildRejectStanza({
|
|
223
|
+
callId: this.callId,
|
|
224
|
+
callCreator: this.callCreator,
|
|
225
|
+
myJid,
|
|
226
|
+
reason
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (this.socketClient.sendNode) {
|
|
230
|
+
await this.socketClient.sendNode(rejectStanza);
|
|
231
|
+
} else {
|
|
232
|
+
await this.socketClient.query(rejectStanza);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Terminates (hangs up) an active WhatsApp call
|
|
238
|
+
*/
|
|
239
|
+
async terminate(reason = "hangup") {
|
|
240
|
+
this.setState(CallState.TERMINATED);
|
|
241
|
+
this.cleanup();
|
|
242
|
+
|
|
243
|
+
const myJid = this.socketClient.authState?.creds?.me?.id;
|
|
244
|
+
const terminateStanza = buildTerminateStanza({
|
|
245
|
+
callId: this.callId,
|
|
246
|
+
callCreator: this.callCreator,
|
|
247
|
+
myJid,
|
|
248
|
+
reason
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (this.socketClient.sendNode) {
|
|
252
|
+
await this.socketClient.sendNode(terminateStanza);
|
|
253
|
+
} else {
|
|
254
|
+
await this.socketClient.query(terminateStanza);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Sends a single Opus audio frame over the call
|
|
260
|
+
* @param {Buffer} opusBuffer Opus compressed frame (20ms, 16kHz)
|
|
261
|
+
* @param {boolean} isMarker True if start of talkspurt
|
|
262
|
+
*/
|
|
263
|
+
sendAudio(opusBuffer, isMarker = false) {
|
|
264
|
+
if (!this.mediaEngine || !this.relayTransport) return;
|
|
265
|
+
const srtpPacket = this.mediaEngine.packAudio(opusBuffer, isMarker);
|
|
266
|
+
this.relayTransport.sendRtp(srtpPacket);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Streams an array of audio frames (TTS or recording)
|
|
271
|
+
*/
|
|
272
|
+
streamAudio(frames) {
|
|
273
|
+
if (!this.mediaEngine || !this.relayTransport) return;
|
|
274
|
+
this.mediaEngine.startStreaming(frames, (packet) => {
|
|
275
|
+
this.relayTransport?.sendRtp(packet);
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Updates internal call state and emits state event
|
|
281
|
+
*/
|
|
282
|
+
setState(newState) {
|
|
283
|
+
const oldState = this.state;
|
|
284
|
+
this.state = newState;
|
|
285
|
+
this.emit("state", { from: oldState, to: newState });
|
|
286
|
+
|
|
287
|
+
if (newState === CallState.TERMINATED || newState === CallState.REJECTED) {
|
|
288
|
+
this.endTime = new Date();
|
|
289
|
+
this.emit("ended", {
|
|
290
|
+
durationSec: this.startTime ? Math.round((this.endTime - this.startTime) / 1000) : 0,
|
|
291
|
+
reason: newState
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Cleans up media engines and network sockets
|
|
298
|
+
*/
|
|
299
|
+
cleanup() {
|
|
300
|
+
this.stopActiveMedia();
|
|
301
|
+
if (this.mediaEngine) {
|
|
302
|
+
this.mediaEngine.stopStreaming();
|
|
303
|
+
}
|
|
304
|
+
if (this.relayTransport) {
|
|
305
|
+
this.relayTransport.close();
|
|
306
|
+
this.relayTransport = null;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
module.exports = {
|
|
312
|
+
CallState,
|
|
313
|
+
VoIPSession,
|
|
314
|
+
};
|