@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,15 @@
1
+ export declare class AudioFeeder {
2
+ #private;
3
+ private readonly sampleRate;
4
+ private readonly channels;
5
+ private readonly framesPerChunk;
6
+ private readonly onChunk;
7
+ private readonly source;
8
+ droppedChunks: number;
9
+ underflowChunks: number;
10
+ bytesProduced: number;
11
+ chunksEmitted: number;
12
+ constructor(sampleRate: number, channels: number, framesPerChunk: number, onChunk: (chunk: Float32Array) => void, source?: string);
13
+ start: () => void;
14
+ stop: () => void;
15
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Audio feeder.
3
+ *
4
+ * Spawns ffmpeg to decode `source` into f32le PCM at the requested rate, then
5
+ * meters frames out at chunk-cadence to the WASM uplink.
6
+ *
7
+ * @author ShellTear
8
+ */
9
+ import { spawn } from "node:child_process";
10
+ const LOW_WATERMARK_CHUNKS = 16;
11
+ const MAX_QUEUED_CHUNKS = 1024;
12
+ const DEFAULT_WARMUP_MS = 500;
13
+
14
+ export class AudioFeeder {
15
+ sampleRate;
16
+ channels;
17
+ framesPerChunk;
18
+ onChunk;
19
+ source;
20
+ #proc = null;
21
+ #pending = Buffer.alloc(0);
22
+ #queue = [];
23
+ #emitTimer = null;
24
+ #nextEmitAtMs = 0;
25
+ #warmupUntilMs = 0;
26
+ #paused = false;
27
+ #volume = 1.0;
28
+
29
+ droppedChunks = 0;
30
+ underflowChunks = 0;
31
+ bytesProduced = 0;
32
+ chunksEmitted = 0;
33
+
34
+ constructor(sampleRate, channels, framesPerChunk, onChunk, source = "silence") {
35
+ this.sampleRate = sampleRate;
36
+ this.channels = channels;
37
+ this.framesPerChunk = framesPerChunk;
38
+ this.onChunk = onChunk;
39
+ this.source = source;
40
+ }
41
+
42
+ start = () => {
43
+ if (this.#proc || this.#emitTimer)
44
+ return;
45
+ const chunkSamples = this.framesPerChunk * this.channels;
46
+ const chunkBytes = chunkSamples * Float32Array.BYTES_PER_ELEMENT;
47
+ const chunkIntervalMs = (this.framesPerChunk / this.sampleRate) * 1000;
48
+
49
+ if (!this.source || this.source === "silence" || this.source === "live") {
50
+ const silentChunk = new Float32Array(chunkSamples);
51
+ for (let i = 0; i < chunkSamples; i++) { silentChunk[i] = (Math.random() - 0.5) * 1e-4; }
52
+ this.#emitTimer = setInterval(() => {
53
+ this.chunksEmitted += 1;
54
+ const nextChunk = (!this.#paused ? this.#queue.shift() : null) || silentChunk;
55
+ this.onChunk(this.#applyVolume(nextChunk));
56
+ }, chunkIntervalMs);
57
+ return;
58
+ }
59
+
60
+ this.#spawnFfmpeg(chunkSamples, chunkBytes, chunkIntervalMs);
61
+ };
62
+
63
+ stop = () => {
64
+ if (this.#emitTimer) {
65
+ clearInterval(this.#emitTimer);
66
+ clearTimeout(this.#emitTimer);
67
+ this.#emitTimer = null;
68
+ }
69
+ this.#proc?.kill("SIGTERM");
70
+ this.#proc = null;
71
+ this.#pending = Buffer.alloc(0);
72
+ this.#queue = [];
73
+ this.#warmupUntilMs = 0;
74
+ };
75
+
76
+ pause = () => {
77
+ this.#paused = true;
78
+ };
79
+
80
+ resume = () => {
81
+ this.#paused = false;
82
+ };
83
+
84
+ setVolume = (gain) => {
85
+ this.#volume = Math.max(0, Number(gain) || 1.0);
86
+ };
87
+
88
+ clearQueue = () => {
89
+ this.#queue = [];
90
+ this.#pending = Buffer.alloc(0);
91
+ };
92
+
93
+ pushChunk = (chunk) => {
94
+ if (chunk && chunk.length && this.#queue.length < 50) {
95
+ this.#queue.push(chunk);
96
+ }
97
+ };
98
+
99
+ playFile = (filePath) => {
100
+ if (this.#proc) {
101
+ this.#proc.kill("SIGTERM");
102
+ this.#proc = null;
103
+ }
104
+ this.clearQueue();
105
+ this.source = filePath;
106
+
107
+ const chunkSamples = this.framesPerChunk * this.channels;
108
+ const chunkBytes = chunkSamples * Float32Array.BYTES_PER_ELEMENT;
109
+ const chunkIntervalMs = (this.framesPerChunk / this.sampleRate) * 1000;
110
+
111
+ // If interval is already active, spawn ffmpeg to feed into #queue
112
+ if (this.#emitTimer) {
113
+ const inputArgs = this.#resolveInputArgs();
114
+ this.#proc = spawn("ffmpeg", [
115
+ "-hide_banner",
116
+ "-loglevel", "error",
117
+ "-thread_queue_size", "512",
118
+ ...inputArgs,
119
+ "-f", "f32le",
120
+ "-ac", String(this.channels),
121
+ "-ar", String(this.sampleRate),
122
+ "pipe:1",
123
+ ]);
124
+ this.#setupProcHandlers(chunkSamples, chunkBytes);
125
+ } else {
126
+ this.#spawnFfmpeg(chunkSamples, chunkBytes, chunkIntervalMs);
127
+ }
128
+ };
129
+
130
+ getStats = () => ({
131
+ droppedChunks: this.droppedChunks,
132
+ underflowChunks: this.underflowChunks,
133
+ bytesProduced: this.bytesProduced,
134
+ chunksEmitted: this.chunksEmitted,
135
+ queueLength: this.#queue.length,
136
+ isPaused: this.#paused,
137
+ volume: this.#volume
138
+ });
139
+
140
+ #applyVolume = (chunk) => {
141
+ if (this.#volume === 1.0 || !chunk) return chunk;
142
+ const out = new Float32Array(chunk.length);
143
+ for (let i = 0; i < chunk.length; i++) {
144
+ out[i] = Math.max(-1, Math.min(1, chunk[i] * this.#volume));
145
+ }
146
+ return out;
147
+ };
148
+
149
+ #spawnFfmpeg = (chunkSamples, chunkBytes, chunkIntervalMs) => {
150
+ const inputArgs = this.#resolveInputArgs();
151
+ this.#proc = spawn("ffmpeg", [
152
+ "-hide_banner",
153
+ "-loglevel", "error",
154
+ "-thread_queue_size", "512",
155
+ ...inputArgs,
156
+ "-f", "f32le",
157
+ "-ac", String(this.channels),
158
+ "-ar", String(this.sampleRate),
159
+ "pipe:1",
160
+ ]);
161
+ this.#setupProcHandlers(chunkSamples, chunkBytes);
162
+ this.#nextEmitAtMs = 0;
163
+ this.#warmupUntilMs = Date.now() + DEFAULT_WARMUP_MS;
164
+ this.#scheduleNext(chunkSamples, chunkIntervalMs);
165
+ };
166
+
167
+ #setupProcHandlers = (chunkSamples, chunkBytes) => {
168
+ this.#proc.stdout.on("data", (chunk) => {
169
+ this.#pending = Buffer.concat([this.#pending, chunk]);
170
+ while (this.#pending.length >= chunkBytes) {
171
+ if (this.#queue.length >= MAX_QUEUED_CHUNKS) {
172
+ this.#proc?.stdout.pause();
173
+ break;
174
+ }
175
+ const frame = this.#pending.subarray(0, chunkBytes);
176
+ this.#pending = this.#pending.subarray(chunkBytes);
177
+ const out = new Float32Array(chunkSamples);
178
+ out.set(new Float32Array(frame.buffer, frame.byteOffset, chunkSamples));
179
+ this.bytesProduced += chunkBytes;
180
+ this.#queue.push(out);
181
+ }
182
+ });
183
+ this.#proc.stderr.on("data", (chunk) => {
184
+ process.stderr.write(`[AudioFeeder] ${chunk.toString().trim()}\n`);
185
+ });
186
+ this.#proc.on("exit", (code) => {
187
+ if (code !== 0 && code !== null) {
188
+ process.stderr.write(`[AudioFeeder] ffmpeg exited with code=${code}\n`);
189
+ }
190
+ this.#proc = null;
191
+ });
192
+ };
193
+
194
+ #resolveInputArgs = () => {
195
+ if (!this.source || this.source === "silence") {
196
+ return ["-f", "lavfi", "-i", `aevalsrc=0:d=3600:s=${this.sampleRate}`];
197
+ }
198
+ if (this.source.startsWith("lavfi:")) {
199
+ return ["-f", "lavfi", "-i", this.source.slice("lavfi:".length)];
200
+ }
201
+ return ["-i", this.source];
202
+ };
203
+
204
+ #scheduleNext = (chunkSamples, chunkIntervalMs) => {
205
+ if (!this.#proc && this.#queue.length === 0) {
206
+ // Keep timer running with silence keepalives if proc exited
207
+ this.#emitTimer = setTimeout(() => {
208
+ this.#flushOne(chunkSamples);
209
+ this.#scheduleNext(chunkSamples, chunkIntervalMs);
210
+ }, chunkIntervalMs);
211
+ return;
212
+ }
213
+ const now = Date.now();
214
+ if (this.#nextEmitAtMs === 0)
215
+ this.#nextEmitAtMs = now;
216
+ const delayMs = Math.max(0, this.#nextEmitAtMs - now);
217
+ this.#emitTimer = setTimeout(() => {
218
+ this.#emitTimer = null;
219
+ if (this.#queue.length < LOW_WATERMARK_CHUNKS && Date.now() < this.#warmupUntilMs) {
220
+ this.#nextEmitAtMs = Date.now() + 10;
221
+ this.#scheduleNext(chunkSamples, chunkIntervalMs);
222
+ return;
223
+ }
224
+ this.#flushOne(chunkSamples);
225
+ this.#nextEmitAtMs += chunkIntervalMs;
226
+ this.#scheduleNext(chunkSamples, chunkIntervalMs);
227
+ }, delayMs);
228
+ };
229
+
230
+ #flushOne = (chunkSamples) => {
231
+ let nextChunk = !this.#paused ? this.#queue.shift() : null;
232
+ if (!nextChunk) {
233
+ nextChunk = new Float32Array(chunkSamples);
234
+ for (let i = 0; i < chunkSamples; i++) { nextChunk[i] = (Math.random() - 0.5) * 1e-4; }
235
+ this.underflowChunks += 1;
236
+ }
237
+ this.chunksEmitted += 1;
238
+ this.onChunk(this.#applyVolume(nextChunk));
239
+ if (this.#proc?.stdout.isPaused() && this.#queue.length <= MAX_QUEUED_CHUNKS / 4) {
240
+ this.#proc.stdout.resume();
241
+ }
242
+ };
243
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * baileys-caller — WhatsApp voice calling for Node.js.
3
+ *
4
+ * Wraps WhatsApp Web's official VoIP WASM stack and routes signaling through
5
+ * Baileys.
6
+ *
7
+ * @author ShellTear
8
+ */
9
+ import { EventEmitter } from "node:events";
10
+ import type { Readable } from "node:stream";
11
+ import { WasmEngine } from "./wasm-engine.mjs";
12
+ import { CallState, type VoipSdkConfig, type CallRealtimeStats, type CallTelemetry, type VadOptions, type StreamOptions, type RecordOptions } from "./types.mjs";
13
+ export type { VoipSdkConfig, CallOptions, CallEvents, AudioConfig, CallRealtimeStats, CallTelemetry, VadOptions, StreamOptions, RecordOptions } from "./types.mjs";
14
+ export { CallState } from "./types.mjs";
15
+
16
+ /** A live or recently-ended call. */
17
+ export declare class ActiveCall extends EventEmitter {
18
+ #private;
19
+ readonly callId: string;
20
+ private readonly engine: WasmEngine;
21
+ readonly isVideo: boolean;
22
+ peer: { phoneNumber: string; pnJid: string; lidJid: string } | null;
23
+ caller: { pnJid: string | null; lidJid: string | null; displayName: string } | null;
24
+ readonly direction: string;
25
+ telemetry: CallTelemetry | null;
26
+ /** @internal mirrors the source path for the audio feeder */
27
+ _audioSource: string;
28
+
29
+ constructor(callId: string, engine: WasmEngine, durationMs: number, isVideo?: boolean);
30
+ get state(): CallState;
31
+ get isVideoPaused(): boolean;
32
+
33
+ end: () => void;
34
+ mute: (muted: boolean) => void;
35
+ waitForEnd: () => Promise<string>;
36
+
37
+ pushAudio: (audioData: Float32Array | Int16Array | Buffer, options?: { sampleRate?: number }) => void;
38
+ pushVideo: (frameBuffer: Buffer, width: number, height: number) => void;
39
+ pushVideoRgba: (rgbaBuffer: Buffer, width: number, height: number) => void;
40
+ get isSharingScreen(): boolean;
41
+ startScreenShare: (options?: { fps?: number; width?: number; height?: number; screen?: string }) => any;
42
+ stopScreenShare: () => void;
43
+
44
+ getRxStream: (options?: StreamOptions) => Readable;
45
+ getTxStream: (options?: StreamOptions) => Readable;
46
+ recordToFile: (filePath: string, options?: RecordOptions) => any;
47
+ stopRecording: () => void;
48
+
49
+ setInputVolume: (gain: number) => void;
50
+ setOutputVolume: (gain: number) => void;
51
+ pauseAudio: () => void;
52
+ resumeAudio: () => void;
53
+ playFile: (filePath: string) => void;
54
+
55
+ pauseVideo: () => void;
56
+ resumeVideo: () => void;
57
+ setVideoFps: (fps: number) => void;
58
+
59
+ setVadOptions: (options?: VadOptions) => void;
60
+ getStats: () => CallRealtimeStats;
61
+ getTelemetry: () => CallTelemetry;
62
+
63
+ /** @internal */
64
+ _updateState: (state: number) => void;
65
+ /** @internal */
66
+ _emitAudio: (pcm: Float32Array) => void;
67
+ /** @internal */
68
+ _emitAudioOut: (pcm: Float32Array) => void;
69
+ /** @internal */
70
+ _emitVideo: (data: any) => void;
71
+ /** @internal */
72
+ _emitVideoOut: (data: any) => void;
73
+ /** @internal */
74
+ _forceEnd: (reason: string) => void;
75
+ }
76
+
77
+ /** Top-level client. Connects to WhatsApp and lets you place calls. */
78
+ export declare class VoipClient {
79
+ #private;
80
+ constructor(config: VoipSdkConfig);
81
+ /** Connect to WhatsApp and bring up the WASM VoIP stack. */
82
+ connect: () => Promise<void>;
83
+ /** Place an outbound voice or video call. */
84
+ call: (phoneNumber: string, opts?: {
85
+ audioSource?: string;
86
+ durationMs?: number;
87
+ isVideo?: boolean;
88
+ }) => Promise<ActiveCall>;
89
+ /** Tear down the WhatsApp socket and release resources. */
90
+ disconnect: () => void;
91
+ }