@areumtecnologia/wasockets 2.0.0-alpha.1 → 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.
Files changed (46) hide show
  1. package/README.md +1929 -1649
  2. package/lib/Defaults/baileys-version.json +1 -1
  3. package/lib/Defaults/connection.js +14 -1
  4. package/lib/Socket/index.js +51 -48
  5. package/lib/Socket/messages-recv.js +74 -13
  6. package/lib/Socket/messages-send.js +50 -25
  7. package/lib/Socket/socket.js +13 -0
  8. package/lib/Socket/voip.js +123 -0
  9. package/lib/Store/make-cache-manager-store.js +14 -5
  10. package/lib/Types/index.d.ts +4 -0
  11. package/lib/Utils/auth-utils.js +16 -1
  12. package/lib/Utils/browser-utils.js +6 -2
  13. package/lib/Utils/generics.js +47 -8
  14. package/lib/Utils/index.d.ts +1 -0
  15. package/lib/Utils/messages-media.js +44 -21
  16. package/lib/Utils/pre-key-manager.js +16 -1
  17. package/lib/Utils/validate-connection.js +10 -11
  18. package/lib/VoIP/VoIPCrypto.js +150 -0
  19. package/lib/VoIP/VoIPManager.js +469 -0
  20. package/lib/VoIP/VoIPMediaEngine.js +224 -0
  21. package/lib/VoIP/VoIPProtocol.js +354 -0
  22. package/lib/VoIP/VoIPRelayTransport.js +173 -0
  23. package/lib/VoIP/VoIPSession.js +314 -0
  24. package/lib/VoIP/assets/wasm/loader.js +5 -0
  25. package/lib/VoIP/assets/wasm/whatsapp.wasm +0 -0
  26. package/lib/VoIP/assets/wasm/worker-modules.js +305 -0
  27. package/lib/VoIP/engine/audio-feeder.d.mts +15 -0
  28. package/lib/VoIP/engine/audio-feeder.mjs +243 -0
  29. package/lib/VoIP/engine/index.d.mts +91 -0
  30. package/lib/VoIP/engine/index.mjs +1683 -0
  31. package/lib/VoIP/engine/relay-transport.d.mts +43 -0
  32. package/lib/VoIP/engine/relay-transport.mjs +607 -0
  33. package/lib/VoIP/engine/signaling.d.mts +44 -0
  34. package/lib/VoIP/engine/signaling.mjs +893 -0
  35. package/lib/VoIP/engine/types.d.mts +183 -0
  36. package/lib/VoIP/engine/types.mjs +17 -0
  37. package/lib/VoIP/engine/wasm-engine.d.mts +103 -0
  38. package/lib/VoIP/engine/wasm-engine.mjs +1397 -0
  39. package/lib/VoIP/engine/worker-bootstrap.mjs +1049 -0
  40. package/lib/VoIP/index.js +51 -0
  41. package/lib/VoIP/utils/audio.d.ts +28 -0
  42. package/lib/VoIP/utils/audio.js +148 -0
  43. package/lib/WABinary/generic-utils.js +22 -6
  44. package/lib/index.d.ts +382 -274
  45. package/lib/index.js +5 -0
  46. 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
+ };