@areumtecnologia/wasockets 2.0.0 → 2.3.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.
- package/README.md +1987 -1649
- package/lib/Client/Builders/Buttons.js +127 -0
- package/lib/Client/Builders/EventBuilder.js +122 -0
- package/lib/Client/Builders/InteractiveMessage.js +150 -0
- package/lib/Client/Builders/Lists.js +78 -0
- package/lib/Client/Builders/PollBuilder.js +84 -0
- package/lib/Client/Builders/index.js +21 -0
- package/lib/Client/Constants.js +54 -0
- package/lib/Client/Context/CallContext.js +96 -0
- package/lib/Client/Context/MediaObject.js +155 -0
- package/lib/Client/Context/MessageContext.js +146 -0
- package/lib/Client/Modules/BusinessManager.js +155 -0
- package/lib/Client/Modules/ChannelManager.js +144 -0
- package/lib/Client/Modules/ChatManager.js +113 -0
- package/lib/Client/Modules/CommunityManager.js +136 -0
- package/lib/Client/Modules/GroupManager.js +194 -0
- package/lib/Client/Modules/LabelManager.js +123 -0
- package/lib/Client/Modules/MessageManager.js +198 -0
- package/lib/Client/Modules/PresenceManager.js +68 -0
- package/lib/Client/Modules/PrivacyManager.js +98 -0
- package/lib/Client/Modules/ProfileManager.js +92 -0
- package/lib/Client/Modules/UserManager.js +92 -0
- package/lib/Client/Modules/VoipManager.js +117 -0
- package/lib/Client/Modules/index.js +29 -0
- package/lib/Client/Normalizers/EventNormalizer.js +82 -0
- package/lib/Client/Normalizers/JidNormalizer.js +93 -0
- package/lib/Client/Normalizers/MessageNormalizer.js +134 -0
- package/lib/Client/WaClient.js +438 -0
- package/lib/Client/index.d.ts +464 -0
- package/lib/Client/index.js +76 -0
- 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 +51 -49
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
|
|
5
|
+
const crypto = require("crypto");
|
|
6
|
+
const {
|
|
7
|
+
getBinaryNodeChild,
|
|
8
|
+
getBinaryNodeChildren,
|
|
9
|
+
getBinaryNodeChildString,
|
|
10
|
+
getBinaryNodeChildBuffer,
|
|
11
|
+
S_WHATSAPP_NET,
|
|
12
|
+
jidEncode,
|
|
13
|
+
jidDecode
|
|
14
|
+
} = require("../WABinary");
|
|
15
|
+
const { generateMessageID } = require("../Utils");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WhatsApp VoIP Protocol Stanza Builder & Parser
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const VOIP_CAPABILITY = Buffer.from("0105f509e0fa53", "hex");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Builds an outgoing <call> offer stanza matching official WhatsApp Android wire spec:
|
|
25
|
+
* audio (16000) -> audio (8000) -> (video) -> capability -> enc / destination -> encopt -> net
|
|
26
|
+
*/
|
|
27
|
+
function buildOfferStanza({
|
|
28
|
+
toJid,
|
|
29
|
+
myJid,
|
|
30
|
+
callId,
|
|
31
|
+
destinations,
|
|
32
|
+
deviceIdentity,
|
|
33
|
+
isVideo = false,
|
|
34
|
+
audioRate = "16000",
|
|
35
|
+
keygen = "2",
|
|
36
|
+
callerPn = null,
|
|
37
|
+
callerCountryCode = "BR",
|
|
38
|
+
deviceClass = "2015",
|
|
39
|
+
joinable = "1",
|
|
40
|
+
tcTokenNode = null,
|
|
41
|
+
callerNotify = null,
|
|
42
|
+
callerPlatform = null,
|
|
43
|
+
callerVersion = null
|
|
44
|
+
}) {
|
|
45
|
+
const offerContent = [];
|
|
46
|
+
|
|
47
|
+
// 1. Audio codecs: rate="8000" then rate="16000"
|
|
48
|
+
offerContent.push({ tag: "audio", attrs: { rate: "8000", enc: "opus" }, content: undefined });
|
|
49
|
+
offerContent.push({ tag: "audio", attrs: { rate: "16000", enc: "opus" }, content: undefined });
|
|
50
|
+
|
|
51
|
+
// Video codec (if video call)
|
|
52
|
+
if (isVideo) {
|
|
53
|
+
offerContent.push({
|
|
54
|
+
tag: "video",
|
|
55
|
+
attrs: {
|
|
56
|
+
enc: "vp8",
|
|
57
|
+
dec: "vp8",
|
|
58
|
+
orientation: "0",
|
|
59
|
+
screen_width: "1920",
|
|
60
|
+
screen_height: "1080",
|
|
61
|
+
device_orientation: "0"
|
|
62
|
+
},
|
|
63
|
+
content: undefined
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. Capability node
|
|
68
|
+
offerContent.push({ tag: "capability", attrs: { ver: "1" }, content: VOIP_CAPABILITY });
|
|
69
|
+
|
|
70
|
+
// 3. Signal encrypted destination keys
|
|
71
|
+
if (destinations && destinations.length > 0) {
|
|
72
|
+
if (destinations[0].tag === "to") {
|
|
73
|
+
offerContent.push({ tag: "destination", attrs: {}, content: destinations });
|
|
74
|
+
} else if (destinations[0].tag === "enc") {
|
|
75
|
+
offerContent.push(...destinations);
|
|
76
|
+
} else {
|
|
77
|
+
offerContent.push({ tag: "destination", attrs: {}, content: destinations });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// 4. Encopt (keygen="2")
|
|
82
|
+
offerContent.push({ tag: "encopt", attrs: { keygen }, content: undefined });
|
|
83
|
+
|
|
84
|
+
// 5. Network medium (medium="3")
|
|
85
|
+
offerContent.push({ tag: "net", attrs: { medium: "3" }, content: undefined });
|
|
86
|
+
|
|
87
|
+
// 6. Signed device identity
|
|
88
|
+
if (deviceIdentity) {
|
|
89
|
+
offerContent.push({
|
|
90
|
+
tag: "device-identity",
|
|
91
|
+
attrs: {},
|
|
92
|
+
content: deviceIdentity
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const { jidNormalizedUser } = require("../WABinary");
|
|
97
|
+
const callCreator = jidNormalizedUser(myJid);
|
|
98
|
+
|
|
99
|
+
const offerAttrs = {
|
|
100
|
+
"call-creator": callCreator,
|
|
101
|
+
"call-id": callId,
|
|
102
|
+
"device_class": deviceClass || "2015",
|
|
103
|
+
"joinable": joinable || "1"
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
if (callerPn) {
|
|
107
|
+
offerAttrs["caller_pn"] = callerPn;
|
|
108
|
+
}
|
|
109
|
+
if (callerCountryCode) {
|
|
110
|
+
offerAttrs["caller_country_code"] = callerCountryCode;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
tag: "call",
|
|
115
|
+
attrs: {
|
|
116
|
+
id: generateMessageID(),
|
|
117
|
+
to: toJid
|
|
118
|
+
},
|
|
119
|
+
content: [{
|
|
120
|
+
tag: "offer",
|
|
121
|
+
attrs: offerAttrs,
|
|
122
|
+
content: offerContent
|
|
123
|
+
}]
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Builds an <accept> stanza to answer an incoming WhatsApp call
|
|
129
|
+
*/
|
|
130
|
+
function buildAcceptStanza({
|
|
131
|
+
callId,
|
|
132
|
+
callCreator,
|
|
133
|
+
myJid,
|
|
134
|
+
isVideo = false,
|
|
135
|
+
audioRate = "16000",
|
|
136
|
+
destinations = null,
|
|
137
|
+
deviceIdentity = null
|
|
138
|
+
}) {
|
|
139
|
+
const acceptContent = [];
|
|
140
|
+
|
|
141
|
+
acceptContent.push({ tag: "audio", attrs: { enc: "opus", rate: audioRate }, content: undefined });
|
|
142
|
+
if (isVideo) {
|
|
143
|
+
acceptContent.push({
|
|
144
|
+
tag: "video",
|
|
145
|
+
attrs: {
|
|
146
|
+
enc: "vp8",
|
|
147
|
+
dec: "vp8",
|
|
148
|
+
orientation: "0",
|
|
149
|
+
screen_width: "1920",
|
|
150
|
+
screen_height: "1080",
|
|
151
|
+
device_orientation: "0"
|
|
152
|
+
},
|
|
153
|
+
content: undefined
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
acceptContent.push({ tag: "net", attrs: { medium: "3" }, content: undefined });
|
|
158
|
+
acceptContent.push({ tag: "capability", attrs: { ver: "1" }, content: VOIP_CAPABILITY });
|
|
159
|
+
acceptContent.push({ tag: "encopt", attrs: { keygen: "2" }, content: undefined });
|
|
160
|
+
|
|
161
|
+
if (destinations && destinations.length > 0) {
|
|
162
|
+
acceptContent.push({ tag: "destination", attrs: {}, content: destinations });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (deviceIdentity) {
|
|
166
|
+
acceptContent.push({
|
|
167
|
+
tag: "device-identity",
|
|
168
|
+
attrs: {},
|
|
169
|
+
content: deviceIdentity
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
tag: "call",
|
|
175
|
+
attrs: {
|
|
176
|
+
id: generateMessageID(),
|
|
177
|
+
from: myJid,
|
|
178
|
+
to: callCreator
|
|
179
|
+
},
|
|
180
|
+
content: [{
|
|
181
|
+
tag: "accept",
|
|
182
|
+
attrs: {
|
|
183
|
+
"call-id": callId,
|
|
184
|
+
"call-creator": callCreator
|
|
185
|
+
},
|
|
186
|
+
content: acceptContent
|
|
187
|
+
}]
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Builds a <reject> stanza
|
|
193
|
+
*/
|
|
194
|
+
function buildRejectStanza({ callId, callCreator, myJid, reason = "0" }) {
|
|
195
|
+
return {
|
|
196
|
+
tag: "call",
|
|
197
|
+
attrs: {
|
|
198
|
+
id: generateMessageID(),
|
|
199
|
+
from: myJid,
|
|
200
|
+
to: callCreator
|
|
201
|
+
},
|
|
202
|
+
content: [{
|
|
203
|
+
tag: "reject",
|
|
204
|
+
attrs: {
|
|
205
|
+
"call-id": callId,
|
|
206
|
+
"call-creator": callCreator,
|
|
207
|
+
count: reason
|
|
208
|
+
},
|
|
209
|
+
content: undefined
|
|
210
|
+
}]
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Builds a <terminate> stanza (hangup / timeout / error)
|
|
216
|
+
*/
|
|
217
|
+
function buildTerminateStanza({ callId, callCreator, myJid, reason = "hangup" }) {
|
|
218
|
+
return {
|
|
219
|
+
tag: "call",
|
|
220
|
+
attrs: {
|
|
221
|
+
id: generateMessageID(),
|
|
222
|
+
from: myJid,
|
|
223
|
+
to: callCreator
|
|
224
|
+
},
|
|
225
|
+
content: [{
|
|
226
|
+
tag: "terminate",
|
|
227
|
+
attrs: {
|
|
228
|
+
"call-id": callId,
|
|
229
|
+
"call-creator": callCreator,
|
|
230
|
+
reason
|
|
231
|
+
},
|
|
232
|
+
content: undefined
|
|
233
|
+
}]
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Builds a relay query IQ stanza to retrieve Meta relay servers
|
|
239
|
+
*/
|
|
240
|
+
function buildRelayQueryIq() {
|
|
241
|
+
return {
|
|
242
|
+
tag: "iq",
|
|
243
|
+
attrs: {
|
|
244
|
+
id: generateMessageID(),
|
|
245
|
+
to: S_WHATSAPP_NET,
|
|
246
|
+
type: "get",
|
|
247
|
+
xmlns: "w:p"
|
|
248
|
+
},
|
|
249
|
+
content: [{
|
|
250
|
+
tag: "relay",
|
|
251
|
+
attrs: {
|
|
252
|
+
latency: "15"
|
|
253
|
+
},
|
|
254
|
+
content: undefined
|
|
255
|
+
}]
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Parses relay tokens and endpoints from a relay IQ result
|
|
261
|
+
*/
|
|
262
|
+
function parseRelayEndpoints(node) {
|
|
263
|
+
if (!node) return [];
|
|
264
|
+
|
|
265
|
+
const endpoints = [];
|
|
266
|
+
const findTeNodes = (curr) => {
|
|
267
|
+
if (!curr || typeof curr !== "object") return;
|
|
268
|
+
if (curr.tag === "te") {
|
|
269
|
+
const ip = curr.attrs?.ip;
|
|
270
|
+
const port = parseInt(curr.attrs?.port, 10);
|
|
271
|
+
const token = curr.content instanceof Uint8Array
|
|
272
|
+
? Buffer.from(curr.content)
|
|
273
|
+
: (curr.attrs?.token ? Buffer.from(curr.attrs.token, "base64") : null);
|
|
274
|
+
|
|
275
|
+
if (ip && port) {
|
|
276
|
+
endpoints.push({
|
|
277
|
+
ip,
|
|
278
|
+
port,
|
|
279
|
+
token,
|
|
280
|
+
type: curr.attrs?.type || "udp"
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (Array.isArray(curr.content)) {
|
|
285
|
+
for (const child of curr.content) {
|
|
286
|
+
findTeNodes(child);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
findTeNodes(node);
|
|
292
|
+
return endpoints;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Parses an incoming <call> node and extracts metadata, offer/accept parameters,
|
|
297
|
+
* destination encrypted payloads, and video attributes.
|
|
298
|
+
*/
|
|
299
|
+
function parseCallNode(node) {
|
|
300
|
+
const { attrs } = node;
|
|
301
|
+
const children = getBinaryNodeChildren(node);
|
|
302
|
+
if (!children.length) return null;
|
|
303
|
+
|
|
304
|
+
const actionNode = children[0];
|
|
305
|
+
const action = actionNode.tag; // offer, accept, reject, terminate, relaylatency
|
|
306
|
+
|
|
307
|
+
const callId = actionNode.attrs["call-id"];
|
|
308
|
+
const callCreator = actionNode.attrs["call-creator"] || attrs.from;
|
|
309
|
+
const from = attrs.from || callCreator;
|
|
310
|
+
|
|
311
|
+
const audioChild = getBinaryNodeChild(actionNode, "audio");
|
|
312
|
+
const videoChild = getBinaryNodeChild(actionNode, "video");
|
|
313
|
+
const netChild = getBinaryNodeChild(actionNode, "net");
|
|
314
|
+
const destinationChild = getBinaryNodeChild(actionNode, "destination");
|
|
315
|
+
|
|
316
|
+
// Extract destinations (encrypted callKey for each participant device)
|
|
317
|
+
let destinationNodes = destinationChild ? getBinaryNodeChildren(destinationChild, "to") : [];
|
|
318
|
+
if (!destinationNodes.length) {
|
|
319
|
+
destinationNodes = getBinaryNodeChildren(actionNode, "to");
|
|
320
|
+
}
|
|
321
|
+
if (!destinationNodes.length) {
|
|
322
|
+
const encChild = getBinaryNodeChild(actionNode, "enc");
|
|
323
|
+
if (encChild) {
|
|
324
|
+
destinationNodes = [{ tag: "to", attrs: { jid: attrs.to }, content: [encChild] }];
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
action,
|
|
330
|
+
callId,
|
|
331
|
+
callCreator,
|
|
332
|
+
from,
|
|
333
|
+
chatId: attrs.from,
|
|
334
|
+
timestamp: attrs.t ? new Date(+attrs.t * 1000) : new Date(),
|
|
335
|
+
offline: !!attrs.offline,
|
|
336
|
+
isVideo: !!videoChild,
|
|
337
|
+
audioRate: audioChild?.attrs?.rate || "16000",
|
|
338
|
+
audioCodec: audioChild?.attrs?.enc || "opus",
|
|
339
|
+
netMedium: netChild?.attrs?.medium,
|
|
340
|
+
destinations: destinationNodes,
|
|
341
|
+
rawNode: node
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
module.exports = {
|
|
346
|
+
VOIP_CAPABILITY,
|
|
347
|
+
buildOfferStanza,
|
|
348
|
+
buildAcceptStanza,
|
|
349
|
+
buildRejectStanza,
|
|
350
|
+
buildTerminateStanza,
|
|
351
|
+
buildRelayQueryIq,
|
|
352
|
+
parseRelayEndpoints,
|
|
353
|
+
parseCallNode,
|
|
354
|
+
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
|
|
5
|
+
const dgram = require("dgram");
|
|
6
|
+
const { EventEmitter } = require("events");
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Meta VoIP Relay UDP Transport Client
|
|
10
|
+
* Manages UDP socket communication, relay authentication tokens,
|
|
11
|
+
* NAT keep-alives, and packet dispatching.
|
|
12
|
+
*/
|
|
13
|
+
class VoIPRelayTransport extends EventEmitter {
|
|
14
|
+
constructor({ ip, port, token, logger = console }) {
|
|
15
|
+
super();
|
|
16
|
+
this.ip = ip;
|
|
17
|
+
this.port = port;
|
|
18
|
+
this.token = Buffer.isBuffer(token) ? token : (token ? Buffer.from(token) : Buffer.alloc(0));
|
|
19
|
+
this.logger = logger;
|
|
20
|
+
this.socket = null;
|
|
21
|
+
this.keepAliveTimer = null;
|
|
22
|
+
this.isConnected = false;
|
|
23
|
+
this.bytesSent = 0;
|
|
24
|
+
this.bytesReceived = 0;
|
|
25
|
+
this.packetsSent = 0;
|
|
26
|
+
this.packetsReceived = 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Connects to the Meta Relay Server via UDP
|
|
31
|
+
*/
|
|
32
|
+
async connect() {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
try {
|
|
35
|
+
this.socket = dgram.createSocket("udp4");
|
|
36
|
+
|
|
37
|
+
this.socket.on("error", (err) => {
|
|
38
|
+
this.logger.error?.({ err, relay: `${this.ip}:${this.port}` }, "VoIP Relay socket error");
|
|
39
|
+
this.emit("error", err);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
this.socket.on("message", (msg, rinfo) => {
|
|
43
|
+
this.bytesReceived += msg.length;
|
|
44
|
+
this.packetsReceived++;
|
|
45
|
+
this.handleIncomingPacket(msg, rinfo);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.socket.on("close", () => {
|
|
49
|
+
this.isConnected = false;
|
|
50
|
+
this.stopKeepAlive();
|
|
51
|
+
this.emit("close");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Bind to random available local UDP port
|
|
55
|
+
this.socket.bind(0, () => {
|
|
56
|
+
this.isConnected = true;
|
|
57
|
+
// Send initial relay allocation / binding handshake
|
|
58
|
+
this.sendRelayBinding();
|
|
59
|
+
this.startKeepAlive();
|
|
60
|
+
resolve();
|
|
61
|
+
});
|
|
62
|
+
} catch (err) {
|
|
63
|
+
reject(err);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sends the Meta VoIP Relay Binding Packet
|
|
70
|
+
* Format: [0x01 (Bind Command)] [Token Length (2 bytes)] [Token Bytes]
|
|
71
|
+
*/
|
|
72
|
+
sendRelayBinding() {
|
|
73
|
+
if (!this.isConnected || !this.socket) return;
|
|
74
|
+
|
|
75
|
+
const packet = Buffer.alloc(3 + this.token.length);
|
|
76
|
+
packet.writeUInt8(0x01, 0); // Binding request
|
|
77
|
+
packet.writeUInt16BE(this.token.length, 1);
|
|
78
|
+
if (this.token.length > 0) {
|
|
79
|
+
this.token.copy(packet, 3);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
this.sendRaw(packet);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Sends an RTP / SRTP packet through the relay connection
|
|
87
|
+
* @param {Buffer} rtpPacket The encrypted SRTP packet
|
|
88
|
+
*/
|
|
89
|
+
sendRtp(rtpPacket) {
|
|
90
|
+
if (!this.isConnected || !this.socket) return;
|
|
91
|
+
// In Meta relay transport, standard RTP packets can be prefixed with data tag (0x02) or sent directly
|
|
92
|
+
this.sendRaw(rtpPacket);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Sends a raw UDP packet to the relay endpoint
|
|
97
|
+
*/
|
|
98
|
+
sendRaw(buffer) {
|
|
99
|
+
if (!this.isConnected || !this.socket) return;
|
|
100
|
+
|
|
101
|
+
this.socket.send(buffer, 0, buffer.length, this.port, this.ip, (err) => {
|
|
102
|
+
if (err) {
|
|
103
|
+
this.logger.warn?.({ err }, "Error sending VoIP relay packet");
|
|
104
|
+
} else {
|
|
105
|
+
this.bytesSent += buffer.length;
|
|
106
|
+
this.packetsSent++;
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Parses incoming UDP packet
|
|
113
|
+
*/
|
|
114
|
+
handleIncomingPacket(msg, rinfo) {
|
|
115
|
+
if (msg.length < 2) return;
|
|
116
|
+
|
|
117
|
+
const header = msg[0];
|
|
118
|
+
|
|
119
|
+
// 0x01 = Relay Ack / Pong
|
|
120
|
+
if (header === 0x01) {
|
|
121
|
+
this.emit("relay.bound", { rinfo });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 0x80+ = Standard RTP/SRTP packet (Version 2 RFC 3550)
|
|
126
|
+
if ((header & 0xc0) === 0x80) {
|
|
127
|
+
this.emit("rtp", msg);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Emit general packet
|
|
132
|
+
this.emit("packet", msg);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Sends periodic keep-alives to preserve NAT binding and session affinity
|
|
137
|
+
*/
|
|
138
|
+
startKeepAlive(intervalMs = 2000) {
|
|
139
|
+
this.stopKeepAlive();
|
|
140
|
+
this.keepAliveTimer = setInterval(() => {
|
|
141
|
+
if (this.isConnected) {
|
|
142
|
+
// Relay ping: 0x03 [Ping]
|
|
143
|
+
const ping = Buffer.from([0x03, 0x00]);
|
|
144
|
+
this.sendRaw(ping);
|
|
145
|
+
}
|
|
146
|
+
}, intervalMs);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
stopKeepAlive() {
|
|
150
|
+
if (this.keepAliveTimer) {
|
|
151
|
+
clearInterval(this.keepAliveTimer);
|
|
152
|
+
this.keepAliveTimer = null;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Gracefully closes the relay socket
|
|
158
|
+
*/
|
|
159
|
+
close() {
|
|
160
|
+
this.stopKeepAlive();
|
|
161
|
+
this.isConnected = false;
|
|
162
|
+
if (this.socket) {
|
|
163
|
+
try {
|
|
164
|
+
this.socket.close();
|
|
165
|
+
} catch (_) {}
|
|
166
|
+
this.socket = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = {
|
|
172
|
+
VoIPRelayTransport,
|
|
173
|
+
};
|