@nerxa/socketon 0.0.0-stage → 1.21.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.
Files changed (289) hide show
  1. package/EXPERIMENTAL.md +350 -0
  2. package/LICENSE +24 -0
  3. package/README.md +6317 -2
  4. package/WAProto/index.d.ts +17301 -0
  5. package/WAProto/index.js +125088 -0
  6. package/engine-requirements.js +14 -0
  7. package/package.json +100 -4
  8. package/src/Defaults/index.d.ts +152 -0
  9. package/src/Defaults/index.js +164 -0
  10. package/src/MessageBuilder/bot-signature.d.ts +22 -0
  11. package/src/MessageBuilder/bot-signature.js +122 -0
  12. package/src/MessageBuilder/extras.d.ts +449 -0
  13. package/src/MessageBuilder/extras.js +1356 -0
  14. package/src/MessageBuilder/index.d.ts +210 -0
  15. package/src/MessageBuilder/index.js +3594 -0
  16. package/src/MessageBuilder/metaai.d.ts +438 -0
  17. package/src/MessageBuilder/metaai.js +890 -0
  18. package/src/Signal/Group/ciphertext-message.d.ts +9 -0
  19. package/src/Signal/Group/ciphertext-message.js +11 -0
  20. package/src/Signal/Group/group-session-builder.d.ts +7 -0
  21. package/src/Signal/Group/group-session-builder.js +27 -0
  22. package/src/Signal/Group/group_cipher.d.ts +11 -0
  23. package/src/Signal/Group/group_cipher.js +78 -0
  24. package/src/Signal/Group/index.d.ts +11 -0
  25. package/src/Signal/Group/index.js +11 -0
  26. package/src/Signal/Group/keyhelper.d.ts +6 -0
  27. package/src/Signal/Group/keyhelper.js +17 -0
  28. package/src/Signal/Group/sender-chain-key.d.ts +13 -0
  29. package/src/Signal/Group/sender-chain-key.js +25 -0
  30. package/src/Signal/Group/sender-key-distribution-message.d.ts +16 -0
  31. package/src/Signal/Group/sender-key-distribution-message.js +62 -0
  32. package/src/Signal/Group/sender-key-message.d.ts +18 -0
  33. package/src/Signal/Group/sender-key-message.js +65 -0
  34. package/src/Signal/Group/sender-key-name.d.ts +11 -0
  35. package/src/Signal/Group/sender-key-name.js +47 -0
  36. package/src/Signal/Group/sender-key-record.d.ts +12 -0
  37. package/src/Signal/Group/sender-key-record.js +40 -0
  38. package/src/Signal/Group/sender-key-state.d.ts +16 -0
  39. package/src/Signal/Group/sender-key-state.js +83 -0
  40. package/src/Signal/Group/sender-message-key.d.ts +11 -0
  41. package/src/Signal/Group/sender-message-key.js +25 -0
  42. package/src/Signal/libsignal.d.ts +54 -0
  43. package/src/Signal/libsignal.js +427 -0
  44. package/src/Signal/lid-mapping.d.ts +20 -0
  45. package/src/Signal/lid-mapping.js +274 -0
  46. package/src/Socket/Client/index.d.ts +2 -0
  47. package/src/Socket/Client/index.js +3 -0
  48. package/src/Socket/Client/tcp.js +177 -0
  49. package/src/Socket/Client/types.d.ts +5 -0
  50. package/src/Socket/Client/types.js +9 -0
  51. package/src/Socket/Client/websocket.d.ts +12 -0
  52. package/src/Socket/Client/websocket.js +53 -0
  53. package/src/Socket/business.d.ts +431 -0
  54. package/src/Socket/business.js +379 -0
  55. package/src/Socket/chats.d.ts +247 -0
  56. package/src/Socket/chats.js +1484 -0
  57. package/src/Socket/communities.d.ts +598 -0
  58. package/src/Socket/communities.js +421 -0
  59. package/src/Socket/email.js +145 -0
  60. package/src/Socket/groups.d.ts +298 -0
  61. package/src/Socket/groups.js +339 -0
  62. package/src/Socket/index.d.ts +581 -0
  63. package/src/Socket/index.js +37 -0
  64. package/src/Socket/messages-recv.d.ts +370 -0
  65. package/src/Socket/messages-recv.js +1864 -0
  66. package/src/Socket/messages-send.d.ts +390 -0
  67. package/src/Socket/messages-send.js +1574 -0
  68. package/src/Socket/mex.d.ts +1 -0
  69. package/src/Socket/mex.js +44 -0
  70. package/src/Socket/mobile-builders.js +72 -0
  71. package/src/Socket/mobile.js +424 -0
  72. package/src/Socket/newsletter.d.ts +468 -0
  73. package/src/Socket/newsletter.js +671 -0
  74. package/src/Socket/socket.d.ts +77 -0
  75. package/src/Socket/socket.js +1106 -0
  76. package/src/Store/index.d.ts +3 -0
  77. package/src/Store/index.js +3 -0
  78. package/src/Store/make-in-memory-store.d.ts +62 -0
  79. package/src/Store/make-in-memory-store.js +396 -0
  80. package/src/Store/make-ordered-dictionary.d.ts +12 -0
  81. package/src/Store/make-ordered-dictionary.js +78 -0
  82. package/src/Store/object-repository.d.ts +10 -0
  83. package/src/Store/object-repository.js +23 -0
  84. package/src/Types/Auth.d.ts +1 -0
  85. package/src/Types/Auth.js +1 -0
  86. package/src/Types/Bussines.d.ts +1 -0
  87. package/src/Types/Bussines.js +1 -0
  88. package/src/Types/Call.d.ts +1 -0
  89. package/src/Types/Call.js +1 -0
  90. package/src/Types/Chat.d.ts +2 -0
  91. package/src/Types/Chat.js +24 -0
  92. package/src/Types/Contact.d.ts +1 -0
  93. package/src/Types/Contact.js +1 -0
  94. package/src/Types/Events.d.ts +1 -0
  95. package/src/Types/Events.js +0 -0
  96. package/src/Types/GroupMetadata.d.ts +5 -0
  97. package/src/Types/GroupMetadata.js +6 -0
  98. package/src/Types/Label.d.ts +2 -0
  99. package/src/Types/Label.js +24 -0
  100. package/src/Types/LabelAssociation.d.ts +2 -0
  101. package/src/Types/LabelAssociation.js +6 -0
  102. package/src/Types/Message.d.ts +13 -0
  103. package/src/Types/Message.js +23 -0
  104. package/src/Types/Mex.d.ts +2 -0
  105. package/src/Types/Mex.js +95 -0
  106. package/src/Types/Product.d.ts +1 -0
  107. package/src/Types/Product.js +1 -0
  108. package/src/Types/Signal.d.ts +1 -0
  109. package/src/Types/Signal.js +0 -0
  110. package/src/Types/Socket.d.ts +1 -0
  111. package/src/Types/Socket.js +0 -0
  112. package/src/Types/State.d.ts +5 -0
  113. package/src/Types/State.js +54 -0
  114. package/src/Types/USync.d.ts +1 -0
  115. package/src/Types/USync.js +0 -0
  116. package/src/Types/index.d.ts +13 -0
  117. package/src/Types/index.js +25 -0
  118. package/src/Utils/auth-store.d.ts +29 -0
  119. package/src/Utils/auth-store.js +76 -0
  120. package/src/Utils/auth-utils.d.ts +58 -0
  121. package/src/Utils/auth-utils.js +273 -0
  122. package/src/Utils/browser-utils.d.ts +9 -0
  123. package/src/Utils/browser-utils.js +28 -0
  124. package/src/Utils/business.d.ts +49 -0
  125. package/src/Utils/business.js +226 -0
  126. package/src/Utils/chat-utils.d.ts +408 -0
  127. package/src/Utils/chat-utils.js +846 -0
  128. package/src/Utils/companion-reg-client-utils.d.ts +4 -0
  129. package/src/Utils/companion-reg-client-utils.js +39 -0
  130. package/src/Utils/crypto.d.ts +39 -0
  131. package/src/Utils/crypto.js +110 -0
  132. package/src/Utils/decode-wa-message.d.ts +99 -0
  133. package/src/Utils/decode-wa-message.js +373 -0
  134. package/src/Utils/event-buffer.d.ts +22 -0
  135. package/src/Utils/event-buffer.js +624 -0
  136. package/src/Utils/generics.d.ts +63 -0
  137. package/src/Utils/generics.js +375 -0
  138. package/src/Utils/group-metadata-cache.d.ts +4 -0
  139. package/src/Utils/group-metadata-cache.js +25 -0
  140. package/src/Utils/history.d.ts +75 -0
  141. package/src/Utils/history.js +131 -0
  142. package/src/Utils/html-app.d.ts +19 -0
  143. package/src/Utils/html-app.js +96 -0
  144. package/src/Utils/html-multiplayer.d.ts +10 -0
  145. package/src/Utils/html-multiplayer.js +94 -0
  146. package/src/Utils/identity-change-handler.d.ts +13 -0
  147. package/src/Utils/identity-change-handler.js +48 -0
  148. package/src/Utils/index.d.ts +44 -0
  149. package/src/Utils/index.js +44 -0
  150. package/src/Utils/link-preview-metadata.d.ts +47 -0
  151. package/src/Utils/link-preview-metadata.js +88 -0
  152. package/src/Utils/link-preview.d.ts +12 -0
  153. package/src/Utils/link-preview.js +79 -0
  154. package/src/Utils/logger.d.ts +2 -0
  155. package/src/Utils/logger.js +2 -0
  156. package/src/Utils/lt-hash.d.ts +6 -0
  157. package/src/Utils/lt-hash.js +3 -0
  158. package/src/Utils/make-mutex.d.ts +6 -0
  159. package/src/Utils/make-mutex.js +32 -0
  160. package/src/Utils/message-retry-manager.d.ts +79 -0
  161. package/src/Utils/message-retry-manager.js +255 -0
  162. package/src/Utils/messages-media.d.ts +136 -0
  163. package/src/Utils/messages-media.js +814 -0
  164. package/src/Utils/messages.d.ts +62 -0
  165. package/src/Utils/messages.js +2223 -0
  166. package/src/Utils/mobile-companion-crypto.js +191 -0
  167. package/src/Utils/modern-messages.d.ts +150 -0
  168. package/src/Utils/modern-messages.js +496 -0
  169. package/src/Utils/native-flow.d.ts +11 -0
  170. package/src/Utils/native-flow.js +115 -0
  171. package/src/Utils/newsletter-status.d.ts +158 -0
  172. package/src/Utils/newsletter-status.js +651 -0
  173. package/src/Utils/noise-handler.d.ts +20 -0
  174. package/src/Utils/noise-handler.js +200 -0
  175. package/src/Utils/offline-node-processor.d.ts +9 -0
  176. package/src/Utils/offline-node-processor.js +33 -0
  177. package/src/Utils/optional-media.d.ts +3 -0
  178. package/src/Utils/optional-media.js +45 -0
  179. package/src/Utils/payment-messages.d.ts +66 -0
  180. package/src/Utils/payment-messages.js +160 -0
  181. package/src/Utils/pre-key-manager.d.ts +25 -0
  182. package/src/Utils/pre-key-manager.js +95 -0
  183. package/src/Utils/process-message.d.ts +60 -0
  184. package/src/Utils/process-message.js +688 -0
  185. package/src/Utils/reporting-utils.d.ts +12 -0
  186. package/src/Utils/reporting-utils.js +256 -0
  187. package/src/Utils/scheduled-message.d.ts +51 -0
  188. package/src/Utils/scheduled-message.js +112 -0
  189. package/src/Utils/sender-key-memory.d.ts +19 -0
  190. package/src/Utils/sender-key-memory.js +51 -0
  191. package/src/Utils/signal.d.ts +82 -0
  192. package/src/Utils/signal.js +193 -0
  193. package/src/Utils/stanza-ack.d.ts +16 -0
  194. package/src/Utils/stanza-ack.js +30 -0
  195. package/src/Utils/status-stickers.d.ts +127 -0
  196. package/src/Utils/status-stickers.js +260 -0
  197. package/src/Utils/sync-action-utils.d.ts +2 -0
  198. package/src/Utils/sync-action-utils.js +44 -0
  199. package/src/Utils/tc-token-utils.d.ts +29 -0
  200. package/src/Utils/tc-token-utils.js +155 -0
  201. package/src/Utils/ttl-cache.d.ts +28 -0
  202. package/src/Utils/ttl-cache.js +120 -0
  203. package/src/Utils/use-mongo-auth-state.d.ts +21 -0
  204. package/src/Utils/use-mongo-auth-state.js +67 -0
  205. package/src/Utils/use-multi-file-auth-state.d.ts +11 -0
  206. package/src/Utils/use-multi-file-auth-state.js +106 -0
  207. package/src/Utils/use-mysql-auth-state.d.ts +19 -0
  208. package/src/Utils/use-mysql-auth-state.js +79 -0
  209. package/src/Utils/use-postgres-auth-state.d.ts +19 -0
  210. package/src/Utils/use-postgres-auth-state.js +79 -0
  211. package/src/Utils/use-redis-auth-state.d.ts +19 -0
  212. package/src/Utils/use-redis-auth-state.js +89 -0
  213. package/src/Utils/use-single-file-auth-state.d.ts +10 -0
  214. package/src/Utils/use-single-file-auth-state.js +108 -0
  215. package/src/Utils/use-sqlite-auth-state.d.ts +12 -0
  216. package/src/Utils/use-sqlite-auth-state.js +120 -0
  217. package/src/Utils/useNekoDBAuth.d.ts +19 -0
  218. package/src/Utils/useNekoDBAuth.js +111 -0
  219. package/src/Utils/username.d.ts +12 -0
  220. package/src/Utils/username.js +58 -0
  221. package/src/Utils/validate-connection.d.ts +43 -0
  222. package/src/Utils/validate-connection.js +277 -0
  223. package/src/Utils/voice-recognition.d.ts +53 -0
  224. package/src/Utils/voice-recognition.js +219 -0
  225. package/src/Voip/audio-feeder.d.ts +24 -0
  226. package/src/Voip/audio-feeder.js +268 -0
  227. package/src/Voip/index.d.ts +88 -0
  228. package/src/Voip/index.js +936 -0
  229. package/src/Voip/relay-transport.d.ts +5 -0
  230. package/src/Voip/relay-transport.js +824 -0
  231. package/src/Voip/signaling.d.ts +11 -0
  232. package/src/Voip/signaling.js +1099 -0
  233. package/src/Voip/types.d.ts +2 -0
  234. package/src/Voip/types.js +12 -0
  235. package/src/Voip/video-feeder.d.ts +24 -0
  236. package/src/Voip/video-feeder.js +268 -0
  237. package/src/Voip/wasm-engine.d.ts +26 -0
  238. package/src/Voip/wasm-engine.js +1307 -0
  239. package/src/Voip/worker-bootstrap.js +1023 -0
  240. package/src/WABinary/constants.d.ts +22 -0
  241. package/src/WABinary/constants.js +1300 -0
  242. package/src/WABinary/decode.d.ts +9 -0
  243. package/src/WABinary/decode.js +261 -0
  244. package/src/WABinary/encode.d.ts +2 -0
  245. package/src/WABinary/encode.js +219 -0
  246. package/src/WABinary/generic-utils.d.ts +76 -0
  247. package/src/WABinary/generic-utils.js +214 -0
  248. package/src/WABinary/index.d.ts +5 -0
  249. package/src/WABinary/index.js +5 -0
  250. package/src/WABinary/jid-utils.d.ts +30 -0
  251. package/src/WABinary/jid-utils.js +97 -0
  252. package/src/WABinary/types.d.ts +1 -0
  253. package/src/WABinary/types.js +0 -0
  254. package/src/WAM/BinaryInfo.d.ts +7 -0
  255. package/src/WAM/BinaryInfo.js +9 -0
  256. package/src/WAM/constants.d.ts +34926 -0
  257. package/src/WAM/constants.js +22852 -0
  258. package/src/WAM/encode.d.ts +1 -0
  259. package/src/WAM/encode.js +148 -0
  260. package/src/WAM/index.d.ts +3 -0
  261. package/src/WAM/index.js +3 -0
  262. package/src/WAUSync/Protocols/USyncContactProtocol.d.ts +36 -0
  263. package/src/WAUSync/Protocols/USyncContactProtocol.js +50 -0
  264. package/src/WAUSync/Protocols/USyncDeviceProtocol.d.ts +22 -0
  265. package/src/WAUSync/Protocols/USyncDeviceProtocol.js +51 -0
  266. package/src/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +12 -0
  267. package/src/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -0
  268. package/src/WAUSync/Protocols/USyncStatusProtocol.d.ts +12 -0
  269. package/src/WAUSync/Protocols/USyncStatusProtocol.js +37 -0
  270. package/src/WAUSync/Protocols/USyncUsernameProtocol.d.ts +9 -0
  271. package/src/WAUSync/Protocols/USyncUsernameProtocol.js +23 -0
  272. package/src/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +38 -0
  273. package/src/WAUSync/Protocols/UsyncBotProfileProtocol.js +49 -0
  274. package/src/WAUSync/Protocols/UsyncLIDProtocol.d.ts +14 -0
  275. package/src/WAUSync/Protocols/UsyncLIDProtocol.js +28 -0
  276. package/src/WAUSync/Protocols/index.d.ts +5 -0
  277. package/src/WAUSync/Protocols/index.js +5 -0
  278. package/src/WAUSync/USyncQuery.d.ts +20 -0
  279. package/src/WAUSync/USyncQuery.js +93 -0
  280. package/src/WAUSync/USyncUser.d.ts +16 -0
  281. package/src/WAUSync/USyncUser.js +30 -0
  282. package/src/WAUSync/index.d.ts +3 -0
  283. package/src/WAUSync/index.js +3 -0
  284. package/src/assets/wasm/README.md +76 -0
  285. package/src/assets/wasm/loader.js +5 -0
  286. package/src/assets/wasm/whatsapp.wasm +0 -0
  287. package/src/assets/wasm/worker-modules.js +273 -0
  288. package/src/index.d.ts +43 -0
  289. package/src/index.js +48 -0
@@ -0,0 +1,936 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { createHmac, randomBytes } from 'node:crypto';
3
+ import { WasmEngine } from './wasm-engine.js';
4
+ import { RelayRtcTransport } from './relay-transport.js';
5
+ import { SignalingBridge } from './signaling.js';
6
+ import { AudioFeeder } from './audio-feeder.js';
7
+ import { VideoFeeder, VIDEO_FORMAT_I420 } from './video-feeder.js';
8
+ import { CallState } from './types.js';
9
+ export { CallState };
10
+
11
+ const SHA256_LEN = 32;
12
+
13
+
14
+ const toBareJid = jid => {
15
+ if (!jid) return jid;
16
+ const value = String(jid);
17
+ const at = value.indexOf("@");
18
+ if (at < 0) return value;
19
+ const user = value.slice(0, at).split(":")[0];
20
+ return `${user}@${value.slice(at + 1)}`;
21
+ };
22
+
23
+
24
+ const computeHkdf = (key, salt, info, length) => {
25
+ const effectiveSalt = salt && salt.length > 0 ? Buffer.from(salt) : Buffer.alloc(SHA256_LEN, 0);
26
+ const prk = createHmac("sha256", effectiveSalt).update(key).digest();
27
+ const blocks = Math.ceil(length / SHA256_LEN);
28
+ const okm = Buffer.alloc(blocks * SHA256_LEN);
29
+ let prev = Buffer.alloc(0);
30
+
31
+ for (let i = 1; i <= blocks; i += 1) {
32
+ prev = createHmac("sha256", prk)
33
+ .update(prev)
34
+ .update(info)
35
+ .update(Buffer.from([i]))
36
+ .digest();
37
+ prev.copy(okm, (i - 1) * SHA256_LEN);
38
+ }
39
+
40
+ return new Uint8Array(okm.buffer, okm.byteOffset, length);
41
+ };
42
+
43
+ const computeHmacSha256 = (data, key) => {
44
+ const result = createHmac("sha256", Buffer.from(key)).update(data).digest();
45
+ return new Uint8Array(result.buffer, result.byteOffset, result.byteLength);
46
+ };
47
+
48
+ const isCallReceiptNode = node => {
49
+ if (node?.tag !== "receipt") return false;
50
+ const child = Array.isArray(node.content) ? node.content[0] : null;
51
+ return !!(child?.attrs?.["call-id"] || child?.attrs?.call_id);
52
+ };
53
+
54
+ export class ActiveCall extends EventEmitter {
55
+ constructor(callId, engine, durationMs) {
56
+ super();
57
+ this.callId = callId;
58
+ this.engine = engine;
59
+ this._state = CallState.Idle;
60
+ this._endResolver = undefined;
61
+ this._endPromise = undefined;
62
+ this._endTimer = null;
63
+ this._ended = false;
64
+ this._audioSource = "silence";
65
+
66
+ this.end = () => {
67
+ if (this._ended) return;
68
+ try { this.engine.endCall(0, true); } catch {}
69
+ this._forceEnd("local_end");
70
+ };
71
+
72
+ this.mute = muted => {
73
+ try { this.engine.setMute(muted); } catch {}
74
+ };
75
+
76
+ this.waitForEnd = () => this._endPromise;
77
+
78
+ this._playlist = [];
79
+ this._feeder = null;
80
+ this._idleTimer = null;
81
+ this._endWhenQueueEmpty = true;
82
+ this._idleGraceMs = 0;
83
+
84
+ this._attachFeeder = feeder => {
85
+ this._feeder = feeder;
86
+ };
87
+
88
+ this.play = source => {
89
+ this._playlist = [];
90
+ if (this._feeder) {
91
+ this._feeder.clearPlaylist();
92
+ this._feeder.enqueue(source);
93
+ this._feeder.skip();
94
+ return true;
95
+ }
96
+ this._playlist.push(source);
97
+ return false;
98
+ };
99
+
100
+ this.enqueue = source => {
101
+ this._clearIdleTimer();
102
+ if (this._feeder) {
103
+ return this._feeder.enqueue(source);
104
+ }
105
+ const tracks = Array.isArray(source) ? source : [source];
106
+ for (const track of tracks) {
107
+ if (track) {
108
+ this._playlist.push(track);
109
+ }
110
+ }
111
+ return this._playlist.length;
112
+ };
113
+
114
+ this.skip = () => this._feeder?.skip() ?? false;
115
+
116
+ this.queued = () => this._feeder?.playlistLength ?? this._playlist.length;
117
+
118
+ this.nowPlaying = () => this._feeder?.currentTrack ?? null;
119
+
120
+ this._video = false;
121
+ this._videoSource = "black";
122
+ this._videoPlaylist = [];
123
+ this._videoFeeder = null;
124
+
125
+ this._attachVideoFeeder = feeder => {
126
+ this._videoFeeder = feeder;
127
+ };
128
+
129
+ this.playVideo = source => {
130
+ this._videoPlaylist = [];
131
+ if (this._videoFeeder) {
132
+ this._videoFeeder.clearPlaylist();
133
+ this._videoFeeder.enqueue(source);
134
+ this._videoFeeder.skip();
135
+ return true;
136
+ }
137
+ this._videoPlaylist.push(source);
138
+ return false;
139
+ };
140
+
141
+ this.enqueueVideo = source => {
142
+ if (this._videoFeeder) {
143
+ return this._videoFeeder.enqueue(source);
144
+ }
145
+ const tracks = Array.isArray(source) ? source : [source];
146
+ for (const track of tracks) {
147
+ if (track) {
148
+ this._videoPlaylist.push(track);
149
+ }
150
+ }
151
+ return this._videoPlaylist.length;
152
+ };
153
+
154
+ this.skipVideo = () => this._videoFeeder?.skip() ?? false;
155
+
156
+ this.queuedVideo = () => this._videoFeeder?.playlistLength ?? this._videoPlaylist.length;
157
+
158
+ this.nowPlayingVideo = () => this._videoFeeder?.currentTrack ?? null;
159
+
160
+ this.isVideo = () => this._video;
161
+
162
+ this._screenShare = false;
163
+
164
+ this.startScreenShare = () => {
165
+ if (!this._video) {
166
+ throw new Error("screen share needs a video call: pass video: true, or screenShare: true which implies it");
167
+ }
168
+ this._screenShare = true;
169
+ try {
170
+ return this.engine.startScreenShare();
171
+ }
172
+ catch {
173
+ return undefined;
174
+ }
175
+ };
176
+
177
+ this.stopScreenShare = () => {
178
+ this._screenShare = false;
179
+ try {
180
+ return this.engine.stopScreenShare();
181
+ }
182
+ catch {
183
+ return undefined;
184
+ }
185
+ };
186
+
187
+ this.isScreenShare = () => this._screenShare;
188
+
189
+ this._onVideoTrackStart = track => {
190
+ this.emit("videotrack", track);
191
+ };
192
+
193
+ this._onVideoTrackEnd = track => {
194
+ this.emit("videotrackend", track);
195
+ };
196
+
197
+ this._clearIdleTimer = () => {
198
+ if (this._idleTimer) {
199
+ clearTimeout(this._idleTimer);
200
+ this._idleTimer = null;
201
+ }
202
+ };
203
+
204
+ this._onTrackStart = track => {
205
+ this._clearIdleTimer();
206
+ this.emit("track", track);
207
+ };
208
+
209
+ this._onTrackEnd = track => {
210
+ this.emit("trackend", track);
211
+ };
212
+
213
+ this._onPlaylistIdle = () => {
214
+ this.emit("idle");
215
+ if (!this._endWhenQueueEmpty) {
216
+ return;
217
+ }
218
+ this._clearIdleTimer();
219
+ this._idleTimer = setTimeout(() => {
220
+ this._idleTimer = null;
221
+ if (this._feeder?.playlistLength || this._feeder?.currentTrack) {
222
+ return;
223
+ }
224
+ this.end();
225
+ }, this._idleGraceMs);
226
+ };
227
+
228
+ this._updateState = state => {
229
+ this._state = state;
230
+ if (state === CallState.PreacceptReceived) {
231
+ this.emit("ringing");
232
+ } else if (state === CallState.Active) {
233
+ this.emit("connected");
234
+ if (this._screenShare) {
235
+ try { this.engine.startScreenShare(); } catch {}
236
+ }
237
+ } else if (state === CallState.Idle || state === CallState.Ending) {
238
+ this._forceEnd("ended");
239
+ }
240
+ };
241
+
242
+ this._emitAudio = pcm => {
243
+ this.emit("audio", pcm);
244
+ };
245
+
246
+ this._forceEnd = reason => {
247
+ if (this._ended) return;
248
+ this._ended = true;
249
+ this._clearIdleTimer();
250
+ if (this._endTimer) {
251
+ clearTimeout(this._endTimer);
252
+ this._endTimer = null;
253
+ }
254
+ this.emit("ended", reason);
255
+ this._endResolver(reason);
256
+ };
257
+
258
+ this._endPromise = new Promise(resolve => {
259
+ this._endResolver = resolve;
260
+ });
261
+
262
+ if (durationMs > 0) {
263
+ this._endTimer = setTimeout(() => this.end(), durationMs);
264
+ }
265
+ }
266
+
267
+ get state() {
268
+ return this._state;
269
+ }
270
+ }
271
+
272
+
273
+ export class VoipClient {
274
+ constructor(config = {}) {
275
+ this._config = config;
276
+ this._log = typeof config.logger === 'function'
277
+ ? config.logger
278
+ : config.debug ? (...args) => console.log(...args) : () => {};
279
+ this._engine = null;
280
+ this._relay = null;
281
+ this._signaling = null;
282
+ this._sock = config.socket || config.sock || config.conn || null;
283
+ this._activeCall = null;
284
+ this._externalSocket = !!this._sock;
285
+ this._capturePtr = 0;
286
+ this._captureChunkBytes = 0;
287
+ this._captureSampleRate = 16000;
288
+ this._captureChannels = 1;
289
+ this._captureFramesPerChunk = 320;
290
+ this._feeder = null;
291
+ this._videoFeeder = null;
292
+ this._wsCallHandler = null;
293
+ this._wsReceiptHandler = null;
294
+ this._captureStartRequested = false;
295
+ this._audioStartTimer = null;
296
+
297
+ this.connect = async () => {
298
+ if (!this._sock) {
299
+ throw new Error('VoipClient needs the socket you are already logged in with: new VoipClient({ socket: sock }). It never opens a session of its own, so there is no second pairing or QR scan.');
300
+ }
301
+ if (!this._sock.ws) {
302
+ throw new Error('the socket has no websocket yet; call connect() once connection.update reports "open"');
303
+ }
304
+ await this._initVoipStack();
305
+ };
306
+
307
+ this.attach = async socket => {
308
+ if (!socket) {
309
+ throw new Error('attach needs the reconnected socket');
310
+ }
311
+ this._detachSocketHandlers();
312
+ this._sock = socket;
313
+ this._externalSocket = true;
314
+ this._signaling?.setSocket?.(socket);
315
+ this._bindSocketHandlers();
316
+ };
317
+
318
+ this.call = async (phoneNumber, opts = {}) => {
319
+ if (!this._engine || !this._signaling) {
320
+ throw new Error("Not connected. Call connect() first.");
321
+ }
322
+
323
+ if (this._activeCall) {
324
+ try { this._activeCall._forceEnd("replaced"); } catch {}
325
+ try { this._engine?.endCall?.(0, true); } catch {}
326
+ try { this._feeder?.stop(); } catch {}
327
+ this._feeder = null;
328
+ try { this._videoFeeder?.stop(); } catch {}
329
+ this._videoFeeder = null;
330
+ this._activeCall = null;
331
+ }
332
+
333
+ const targetNumber = phoneNumber.replace(/\D/g, "");
334
+ if (!targetNumber) {
335
+ throw new Error("Nomor target tidak valid.");
336
+ }
337
+
338
+ const targetPnJid = `${targetNumber}@s.whatsapp.net`;
339
+ const durationMs = opts.durationMs ?? 120000;
340
+ const audioSource = opts.audioSource ?? "silence";
341
+
342
+ const peerLid = await this._signaling.resolveLid(targetPnJid);
343
+ if (!peerLid) {
344
+ throw new Error(`Tidak bisa resolve LID untuk ${targetPnJid}. Pastikan target valid dan sudah tersimpan mapping LID/PN atau USync LID bisa diakses.`);
345
+ }
346
+
347
+ const peerJid = peerLid;
348
+ const peerPn = targetPnJid;
349
+ const isLidCall = true;
350
+
351
+ for (const jid of [targetPnJid, peerLid]) {
352
+ try { await this._sock.presenceSubscribe(jid); } catch {}
353
+ }
354
+ await new Promise(resolve => setTimeout(resolve, 150));
355
+
356
+ let peerDeviceJids = [];
357
+ try {
358
+ peerDeviceJids = await this._signaling.discoverPeerDevices(peerLid);
359
+ } catch (err) {
360
+ this._log("[RTC DISCOVER DEVICES ERROR]", err?.message || err);
361
+ }
362
+
363
+ const deviceList = peerDeviceJids.length ? peerDeviceJids : [toBareJid(peerLid)];
364
+
365
+ try {
366
+ await this._signaling.ensureSessionsForPeers(deviceList);
367
+ } catch (err) {
368
+ this._log("[RTC ENSURE SESSIONS WARNING]", err?.message || err);
369
+ }
370
+
371
+ await new Promise(resolve => setTimeout(resolve, 100));
372
+
373
+ let tcToken = null;
374
+ try {
375
+ this._signaling.issueTcToken(peerLid).catch(err => {
376
+ this._log("[RTC ISSUE TC TOKEN ASYNC ERROR]", err?.message || err);
377
+ });
378
+
379
+ tcToken = await Promise.race([
380
+ this._signaling.ensureTcToken(peerLid, targetPnJid).catch(err => {
381
+ this._log("[RTC ENSURE TC TOKEN ERROR]", err?.message || err);
382
+ return null;
383
+ }),
384
+ new Promise(resolve => setTimeout(() => resolve(null), 1500))
385
+ ]);
386
+ } catch (err) {
387
+ this._log("[RTC TC TOKEN FAST PATH ERROR]", err?.message || err);
388
+ }
389
+
390
+ if (!tcToken || !tcToken.length) {
391
+ this._log("[RTC TC TOKEN WARNING] TC token kosong, lanjut startCall dengan token kosong.");
392
+ tcToken = undefined;
393
+ }
394
+
395
+ const callId = ("00" + randomBytes(16).toString("hex").slice(2)).toUpperCase();
396
+ const call = new ActiveCall(callId, this._engine, durationMs);
397
+
398
+ call._audioSource = audioSource;
399
+ call._audioStartDelayMs = Number(opts.audioStartDelayMs ?? opts.audioDelayMs ?? 1800) || 0;
400
+ call._playlist = Array.isArray(opts.playlist) ? opts.playlist.filter(Boolean) : [];
401
+ call._endWhenQueueEmpty = opts.endWhenQueueEmpty ?? true;
402
+ call._idleGraceMs = Number(opts.idleGraceMs ?? 0) || 0;
403
+ call._screenShare = !!opts.screenShare;
404
+ call._video = !!(opts.video ?? opts.isVideo) || call._screenShare;
405
+ call._videoSource = opts.videoSource ?? "black";
406
+ call._videoPlaylist = Array.isArray(opts.videoPlaylist) ? opts.videoPlaylist.filter(Boolean) : [];
407
+ this._activeCall = call;
408
+
409
+ call.once("ended", () => {
410
+ if (this._activeCall === call) this._activeCall = null;
411
+ });
412
+
413
+ call.once("error", () => {
414
+ if (this._activeCall === call) this._activeCall = null;
415
+ });
416
+
417
+ this._log("[RTC START CALL PARAMS]", {
418
+ peerJid,
419
+ peerPn,
420
+ peerList: deviceList,
421
+ callId,
422
+ isLidCall,
423
+ targetPnJid,
424
+ peerLid,
425
+ hasTcToken: !!tcToken,
426
+ tcTokenLength: tcToken?.length || 0
427
+ });
428
+
429
+ try {
430
+ const startResult = this._engine.startCall({
431
+ peerJid,
432
+ peerPn,
433
+ peerList: deviceList,
434
+ callId,
435
+ isVideo: call._video,
436
+ isLidCall,
437
+ isFromDialer: false,
438
+ extraData: tcToken
439
+ });
440
+ this._log("[RTC START CALL RESULT]", startResult);
441
+ } catch (err) {
442
+ if (this._activeCall === call) this._activeCall = null;
443
+ try { call._forceEnd("start_call_failed"); } catch {}
444
+ throw err;
445
+ }
446
+
447
+ return call;
448
+ };
449
+
450
+ this.callGroup = async (groupJid, opts = {}) => {
451
+ if (!this._engine || !this._signaling) {
452
+ throw new Error("Not connected. Call connect() first.");
453
+ }
454
+ if (typeof groupJid !== "string" || !groupJid.endsWith("@g.us")) {
455
+ throw new Error(`${JSON.stringify(groupJid)} is not a group jid`);
456
+ }
457
+
458
+ const metadata = opts.metadata ?? await this._sock.groupMetadata(groupJid);
459
+ const selfPn = toBareJid(this._sock.authState?.creds?.me?.id ?? this._sock.user?.id ?? "");
460
+ const selfLid = toBareJid(this._sock.authState?.creds?.me?.lid ?? "");
461
+ const members = (metadata?.participants ?? [])
462
+ .map(participant => toBareJid(participant.id ?? participant.jid ?? ""))
463
+ .filter(jid => jid && jid !== selfPn && jid !== selfLid);
464
+ const wanted = Array.isArray(opts.participants) && opts.participants.length
465
+ ? opts.participants.map(toBareJid)
466
+ : members;
467
+
468
+ if (!wanted.length) {
469
+ throw new Error(`no one to call in ${groupJid}`);
470
+ }
471
+
472
+ const pnUserJids = [];
473
+ const lidUserJids = [];
474
+ const deviceJidsCsv = [];
475
+
476
+ for (const member of wanted) {
477
+ const isLid = member.endsWith("@lid");
478
+ const lid = isLid ? member : await this._signaling.resolveLid(member).catch(() => null);
479
+ const pn = isLid ? "" : member;
480
+ if (pn) {
481
+ pnUserJids.push(pn);
482
+ }
483
+ if (lid) {
484
+ lidUserJids.push(lid);
485
+ let devices = [];
486
+ try {
487
+ devices = await this._signaling.discoverPeerDevices(lid);
488
+ } catch (err) {
489
+ this._log("[RTC GROUP DISCOVER DEVICES ERROR]", member, err?.message || err);
490
+ }
491
+ deviceJidsCsv.push((devices.length ? devices : [lid]).join(","));
492
+ }
493
+ }
494
+
495
+ if (!lidUserJids.length) {
496
+ throw new Error(`could not resolve a LID for anyone in ${groupJid}`);
497
+ }
498
+
499
+ const flatDevices = deviceJidsCsv.join(",").split(",").filter(Boolean);
500
+ try {
501
+ await this._signaling.ensureSessionsForPeers(flatDevices);
502
+ } catch (err) {
503
+ this._log("[RTC GROUP ENSURE SESSIONS WARNING]", err?.message || err);
504
+ }
505
+
506
+ const callId = ("00" + randomBytes(16).toString("hex").slice(2)).toUpperCase();
507
+ const call = new ActiveCall(callId, this._engine, opts.durationMs ?? 120000);
508
+
509
+ call._audioSource = opts.audioSource ?? "silence";
510
+ call._audioStartDelayMs = Number(opts.audioStartDelayMs ?? opts.audioDelayMs ?? 1800) || 0;
511
+ call._playlist = Array.isArray(opts.playlist) ? opts.playlist.filter(Boolean) : [];
512
+ call._endWhenQueueEmpty = opts.endWhenQueueEmpty ?? true;
513
+ call._idleGraceMs = Number(opts.idleGraceMs ?? 0) || 0;
514
+ call._screenShare = !!opts.screenShare;
515
+ call._video = !!(opts.video ?? opts.isVideo) || call._screenShare;
516
+ call._videoSource = opts.videoSource ?? "black";
517
+ call._videoPlaylist = Array.isArray(opts.videoPlaylist) ? opts.videoPlaylist.filter(Boolean) : [];
518
+ call.groupJid = groupJid;
519
+ this._activeCall = call;
520
+
521
+ call.once("ended", () => {
522
+ if (this._activeCall === call) this._activeCall = null;
523
+ });
524
+
525
+ this._log("[RTC START GROUP CALL PARAMS]", { groupJid, callId, pnUserJids, lidUserJids, deviceJidsCsv });
526
+
527
+ try {
528
+ const startResult = this._engine.startGroupCall({
529
+ pnUserJids,
530
+ lidUserJids,
531
+ deviceJidsCsv,
532
+ callId,
533
+ isVideo: call._video,
534
+ groupJid,
535
+ chatName: metadata?.subject ?? ""
536
+ });
537
+ this._log("[RTC START GROUP CALL RESULT]", startResult);
538
+ } catch (err) {
539
+ if (this._activeCall === call) this._activeCall = null;
540
+ try { call._forceEnd("start_group_call_failed"); } catch {}
541
+ throw err;
542
+ }
543
+
544
+ return call;
545
+ };
546
+
547
+ this.joinGroupCall = async (invite, opts = {}) => {
548
+ if (!this._engine || !this._signaling) {
549
+ throw new Error("Not connected. Call connect() first.");
550
+ }
551
+ const callId = invite?.callId ?? invite?.id;
552
+ const groupJid = invite?.groupJid ?? invite?.from;
553
+ const callCreatorJid = invite?.callCreatorJid ?? invite?.creator ?? invite?.from;
554
+ if (!callId || !callCreatorJid) {
555
+ throw new Error("joinGroupCall needs at least { callId, callCreatorJid } from the incoming offer");
556
+ }
557
+
558
+ const call = new ActiveCall(callId, this._engine, opts.durationMs ?? 0);
559
+ call._audioSource = opts.audioSource ?? "silence";
560
+ call._audioStartDelayMs = Number(opts.audioStartDelayMs ?? opts.audioDelayMs ?? 1800) || 0;
561
+ call._playlist = Array.isArray(opts.playlist) ? opts.playlist.filter(Boolean) : [];
562
+ call._endWhenQueueEmpty = opts.endWhenQueueEmpty ?? true;
563
+ call._idleGraceMs = Number(opts.idleGraceMs ?? 0) || 0;
564
+ call._screenShare = !!opts.screenShare;
565
+ call._video = !!(opts.video ?? opts.isVideo) || call._screenShare;
566
+ call._videoSource = opts.videoSource ?? "black";
567
+ call._videoPlaylist = Array.isArray(opts.videoPlaylist) ? opts.videoPlaylist.filter(Boolean) : [];
568
+ call.groupJid = groupJid;
569
+ this._activeCall = call;
570
+
571
+ call.once("ended", () => {
572
+ if (this._activeCall === call) this._activeCall = null;
573
+ });
574
+
575
+ try {
576
+ const result = this._engine.joinOngoingCall({
577
+ callId,
578
+ callCreatorJid,
579
+ initialPeerJid: invite?.initialPeerJid ?? callCreatorJid,
580
+ pnUserJids: invite?.pnUserJids ?? [],
581
+ lidUserJids: invite?.lidUserJids ?? [],
582
+ deviceJidsCsv: invite?.deviceJidsCsv ?? [],
583
+ groupJid: groupJid ?? "",
584
+ isVideo: call._video,
585
+ joinAndAccept: opts.joinAndAccept ?? true,
586
+ chatName: invite?.chatName ?? ""
587
+ });
588
+ this._log("[RTC JOIN CALL RESULT]", result);
589
+ } catch (err) {
590
+ if (this._activeCall === call) this._activeCall = null;
591
+ try { call._forceEnd("join_call_failed"); } catch {}
592
+ throw err;
593
+ }
594
+
595
+ return call;
596
+ };
597
+
598
+ this.acceptCall = (isMicEnabled = true) => this._engine?.acceptCall(isMicEnabled, false);
599
+
600
+ this.disconnect = () => {
601
+ this._activeCall?._forceEnd("disconnect");
602
+ this._activeCall = null;
603
+ this._captureStartRequested = false;
604
+ try { this._feeder?.stop(); } catch {}
605
+ this._feeder = null;
606
+ try { this._videoFeeder?.stop(); } catch {}
607
+ this._videoFeeder = null;
608
+ if (this._audioStartTimer) {
609
+ clearTimeout(this._audioStartTimer);
610
+ this._audioStartTimer = null;
611
+ }
612
+ try { this._signaling?.attachEngine?.(null); } catch {}
613
+ this._detachSocketHandlers();
614
+ this._relay?.closeAll();
615
+ this._engine?.destroy();
616
+ if (!this._externalSocket) {
617
+ this._sock?.end?.();
618
+ }
619
+ this._engine = null;
620
+ this._relay = null;
621
+ this._signaling = null;
622
+ if (!this._externalSocket) {
623
+ this._sock = null;
624
+ }
625
+ };
626
+
627
+ this.resetCallState = () => {
628
+ try { this._activeCall?._forceEnd("reset"); } catch {}
629
+ try { this._engine?.endCall?.(0, true); } catch {}
630
+ try { this._feeder?.stop(); } catch {}
631
+ this._feeder = null;
632
+ try { this._videoFeeder?.stop(); } catch {}
633
+ this._videoFeeder = null;
634
+ if (this._audioStartTimer) {
635
+ clearTimeout(this._audioStartTimer);
636
+ this._audioStartTimer = null;
637
+ }
638
+ this._captureStartRequested = false;
639
+ this._activeCall = null;
640
+ };
641
+
642
+ this._initVoipStack = async () => {
643
+ if (!this._sock) {
644
+ throw new Error("Socket Baileys belum tersedia.");
645
+ }
646
+
647
+ this._signaling = new SignalingBridge({
648
+ sock: this._sock,
649
+ logger: this._config.logger,
650
+ debug: this._config.debug
651
+ });
652
+ await this._signaling.init();
653
+
654
+ this._relay = new RelayRtcTransport({
655
+ onTransportMessage: (data, ip, port) => this._engine?.handleOnTransportMessage(data, ip, port),
656
+ onIceRtt: (rttMs, ip, port) => this._engine?.updateIceRtt(rttMs, ip, port)
657
+ });
658
+
659
+ this._engine = new WasmEngine({
660
+ callbacks: {
661
+ onSignalingXmpp: (peerJid, callId, xmlPayload) => {
662
+ this._log("[RTC WASM SIGNALING XMPP]", {
663
+ peerJid,
664
+ callId,
665
+ payloadLength: xmlPayload?.length || 0
666
+ });
667
+ return this._signaling.sendSignaling(peerJid, callId, xmlPayload);
668
+ },
669
+ onCallEvent: (eventType, eventData) => this._handleCallEvent(eventType, eventData),
670
+ sendDataToRelay: (data, ip, port) => this._relay.send(data, ip, port),
671
+ onAudioCaptureInit: config => this._handleAudioCaptureInit(config),
672
+ onAudioCaptureStart: () => this._handleAudioCaptureStart(),
673
+ onAudioCaptureStop: () => this._handleAudioCaptureStop(),
674
+ onAudioPlaybackData: audioData => this._activeCall?._emitAudio(audioData),
675
+ onVideoCaptureStart: config => this._handleVideoCaptureStart(config),
676
+ onVideoCaptureStop: () => this._handleVideoCaptureStop(),
677
+ onDesktopCaptureStart: config => this._handleDesktopCaptureStart(config),
678
+ onDesktopCaptureStop: () => this._handleDesktopCaptureStop(),
679
+ cryptoHkdf: computeHkdf,
680
+ hmacSha256: computeHmacSha256
681
+ }
682
+ });
683
+
684
+ await this._engine.initialize();
685
+ this._signaling.attachEngine(this._engine);
686
+
687
+ const selfPnJidRaw =
688
+ this._sock.authState?.creds?.me?.id ||
689
+ this._sock.authState?.creds?.me?.jid ||
690
+ this._sock.user?.id ||
691
+ this._sock.user?.jid;
692
+
693
+ const selfPnJid = toBareJid(selfPnJidRaw);
694
+ const selfLidJid =
695
+ this._sock.authState?.creds?.me?.lid ||
696
+ this._sock.authState?.creds?.me?.lidJid ||
697
+ "";
698
+
699
+ if (!selfPnJid) {
700
+ throw new Error("Tidak bisa membaca JID bot dari socket utama.");
701
+ }
702
+
703
+ this._log("[RTC SELF JID]", { selfPnJid, selfLidJid });
704
+
705
+ this._engine.initVoipStack(selfPnJid, selfPnJid, selfLidJid);
706
+ await this._engine.waitForVoipStackReady();
707
+
708
+ try { this._engine.updateNetworkMedium(2, 0); } catch {}
709
+
710
+ this._bindSocketHandlers();
711
+ };
712
+
713
+ this._detachSocketHandlers = () => {
714
+ const ws = this._sock?.ws;
715
+ if (ws?.off) {
716
+ try { if (this._wsCallHandler) ws.off("CB:call", this._wsCallHandler); } catch {}
717
+ try { if (this._wsReceiptHandler) ws.off("CB:receipt", this._wsReceiptHandler); } catch {}
718
+ }
719
+ this._wsCallHandler = null;
720
+ this._wsReceiptHandler = null;
721
+ };
722
+
723
+ this._bindSocketHandlers = () => {
724
+ if (!this._sock?.ws?.on) {
725
+ return;
726
+ }
727
+ this._detachSocketHandlers();
728
+
729
+ this._wsCallHandler = node => {
730
+ const signaling = this._signaling;
731
+ const engine = this._engine;
732
+ if (!signaling || !engine) return;
733
+ if (typeof engine.isInitialized === "function" && !engine.isInitialized()) return;
734
+ signaling.processIncomingCall(node, engine, this._activeCall?.callId ?? "");
735
+ };
736
+
737
+ this._wsReceiptHandler = node => {
738
+ if (!isCallReceiptNode(node)) return;
739
+ const signaling = this._signaling;
740
+ const engine = this._engine;
741
+ if (!signaling || !engine) return;
742
+ if (typeof engine.isInitialized === "function" && !engine.isInitialized()) return;
743
+ signaling.processIncomingReceipt(node, engine, this._activeCall?.callId ?? "");
744
+ };
745
+
746
+ this._sock.ws.on("CB:call", this._wsCallHandler);
747
+ this._sock.ws.on("CB:receipt", this._wsReceiptHandler);
748
+ };
749
+
750
+ this._handleCallEvent = (eventType, eventData) => {
751
+ this._log("[RTC CALL EVENT]", { eventType, hasData: !!eventData, data: eventData });
752
+
753
+ if (eventType === 16 && eventData) {
754
+ try {
755
+ const parsed = JSON.parse(eventData);
756
+ const info = parsed.call_info ?? parsed.callInfo ?? {};
757
+ const callState = Number(info.call_state ?? info.callState ?? 0);
758
+ this._log("[RTC CALL STATE]", callState);
759
+ this._activeCall?._updateState(callState);
760
+ if (callState === CallState.Active) {
761
+ this._captureStartRequested = true;
762
+ setTimeout(() => {
763
+ try { this._startAudioFeeder?.(); } catch (err) {
764
+ this._log("[RTC AUDIO START ON ACTIVE ERROR]", err?.message || err);
765
+ }
766
+ }, 300);
767
+ }
768
+ } catch {}
769
+ } else if (eventType === 156 && eventData) {
770
+ try {
771
+ const update = JSON.parse(eventData);
772
+ this._relay?.updateRelayList(update);
773
+ } catch {}
774
+ } else if (eventType === 2) {
775
+ this._activeCall?._forceEnd("remote_end");
776
+ }
777
+ };
778
+
779
+ this._handleAudioCaptureInit = config => {
780
+ if (!this._engine) return;
781
+ this._captureSampleRate = config.sampleRate || 16000;
782
+ this._captureChannels = config.channels || 1;
783
+ this._captureFramesPerChunk = config.framesPerChunk || 320;
784
+ const chunkSamples = this._captureFramesPerChunk * this._captureChannels;
785
+ this._captureChunkBytes = chunkSamples * Float32Array.BYTES_PER_ELEMENT;
786
+ this._capturePtr = this._engine.malloc(this._captureChunkBytes);
787
+ if (this._captureStartRequested && this._activeCall?.state === CallState.Active) {
788
+ setTimeout(() => {
789
+ try { this._startAudioFeeder?.(); } catch {}
790
+ }, 100);
791
+ }
792
+ };
793
+
794
+ this._startAudioFeeder = () => {
795
+ this._captureStartRequested = true;
796
+
797
+ if (this._activeCall?.state !== CallState.Active) {
798
+ this._log("[RTC AUDIO] capture siap, tunggu call active sebelum audio start");
799
+ return;
800
+ }
801
+
802
+ if (!this._engine || !this._capturePtr) {
803
+ this._log("[RTC AUDIO] capturePtr belum siap, audio feeder batal start");
804
+ return;
805
+ }
806
+
807
+ if (this._feeder) return;
808
+
809
+ const audioDelayMs = Number(this._activeCall?._audioStartDelayMs ?? 0) || 0;
810
+ if (audioDelayMs > 0 && !this._audioStartTimer) {
811
+ this._log("[RTC AUDIO] call active, audio ditunda:", audioDelayMs);
812
+ this._audioStartTimer = setTimeout(() => {
813
+ this._audioStartTimer = null;
814
+ if (this._activeCall?.state === CallState.Active) {
815
+ this._activeCall._audioStartDelayMs = 0;
816
+ this._startAudioFeeder();
817
+ }
818
+ }, audioDelayMs);
819
+ return;
820
+ }
821
+
822
+ if (this._audioStartTimer) return;
823
+
824
+ const call = this._activeCall;
825
+ const playlist = call?._playlist?.length ? [...call._playlist] : [call?._audioSource ?? "silence"];
826
+
827
+ this._log("[RTC AUDIO] start feeder:", {
828
+ playlist,
829
+ sampleRate: this._captureSampleRate,
830
+ channels: this._captureChannels,
831
+ framesPerChunk: this._captureFramesPerChunk
832
+ });
833
+
834
+ this._feeder = new AudioFeeder(
835
+ this._captureSampleRate,
836
+ this._captureChannels,
837
+ this._captureFramesPerChunk,
838
+ chunk => {
839
+ if (this._engine && this._capturePtr && (!this._engine.isInitialized || this._engine.isInitialized())) {
840
+ this._engine.sendAudioData(chunk, this._capturePtr);
841
+ }
842
+ },
843
+ undefined,
844
+ {
845
+ onTrackStart: track => call?._onTrackStart(track),
846
+ onTrackEnd: track => call?._onTrackEnd(track),
847
+ onIdle: () => call?._onPlaylistIdle(),
848
+ ffmpegPath: this._config.ffmpegPath
849
+ }
850
+ );
851
+
852
+ call?._attachFeeder(this._feeder);
853
+ this._feeder.start(playlist);
854
+ };
855
+
856
+ this._startVideoFeeder = (config, useDesktopCapture) => {
857
+ const call = this._activeCall;
858
+ const label = useDesktopCapture ? "SCREEN" : "VIDEO";
859
+ if (!call?._video) {
860
+ this._log(`[RTC ${label}] capture diminta tapi call ini bukan video, diabaikan`);
861
+ return;
862
+ }
863
+ if (this._videoFeeder) {
864
+ return;
865
+ }
866
+ const width = config?.width ?? 640;
867
+ const height = config?.height ?? 480;
868
+ const fps = config?.fps ?? 24;
869
+ const playlist = call._videoPlaylist?.length ? [...call._videoPlaylist] : [call._videoSource ?? "black"];
870
+
871
+ this._log(`[RTC ${label}] start feeder:`, { width, height, fps, playlist });
872
+
873
+ this._videoFeeder = new VideoFeeder(
874
+ width,
875
+ height,
876
+ fps,
877
+ (frame, w, h, rate) => {
878
+ if (this._engine && (!this._engine.isInitialized || this._engine.isInitialized())) {
879
+ this._engine.sendVideoFrame(frame, w, h, rate, VIDEO_FORMAT_I420, 0, useDesktopCapture);
880
+ }
881
+ },
882
+ undefined,
883
+ {
884
+ onTrackStart: track => call._onVideoTrackStart(track),
885
+ onTrackEnd: track => call._onVideoTrackEnd(track),
886
+ ffmpegPath: this._config.ffmpegPath
887
+ }
888
+ );
889
+
890
+ call._attachVideoFeeder(this._videoFeeder);
891
+ this._videoFeeder.start(playlist);
892
+ };
893
+
894
+ this._stopVideoFeeder = label => {
895
+ this._log(`[RTC ${label}] stop feeder`);
896
+ try { this._videoFeeder?.stop(); } catch {}
897
+ this._videoFeeder = null;
898
+ try { this._engine?.releaseVideoFrameBuffer?.(); } catch {}
899
+ };
900
+
901
+ this._handleVideoCaptureStart = config => this._startVideoFeeder(config, false);
902
+ this._handleVideoCaptureStop = () => this._stopVideoFeeder("VIDEO");
903
+ this._handleDesktopCaptureStart = config => this._startVideoFeeder(config, true);
904
+ this._handleDesktopCaptureStop = () => this._stopVideoFeeder("SCREEN");
905
+
906
+ this._handleAudioCaptureStart = () => {
907
+ this._captureStartRequested = true;
908
+ if (this._activeCall?.state === CallState.Active) {
909
+ this._startAudioFeeder();
910
+ } else {
911
+ this._log("[RTC AUDIO] startCaptureJS diterima sebelum active, audio ditahan dulu");
912
+ }
913
+ };
914
+
915
+ this._handleAudioCaptureStop = () => {
916
+ this._captureStartRequested = false;
917
+ this._feeder?.stop();
918
+ this._feeder = null;
919
+ try { this._videoFeeder?.stop(); } catch {}
920
+ this._videoFeeder = null;
921
+ if (this._engine && this._capturePtr) {
922
+ try { this._engine.free(this._capturePtr); } catch {}
923
+ this._capturePtr = 0;
924
+ }
925
+ };
926
+ }
927
+ }
928
+
929
+ export { VideoFeeder, VIDEO_FORMAT_I420 } from './video-feeder.js';
930
+ export { AudioFeeder } from './audio-feeder.js';
931
+ export const makeVoipClient = async (socket, config = {}) => {
932
+ const client = new VoipClient({ ...config, socket });
933
+ await client.connect();
934
+ return client;
935
+ };
936
+ export default VoipClient;