@harmonia-audio/voice 0.1.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/dist/index.cjs +936 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +638 -0
- package/dist/index.d.ts +638 -0
- package/dist/index.js +919 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,936 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var events = require('events');
|
|
4
|
+
var dgram = require('dgram');
|
|
5
|
+
var WebSocket = require('ws');
|
|
6
|
+
var native = require('@harmonia-audio/native');
|
|
7
|
+
var codec = require('@harmonia-audio/codec');
|
|
8
|
+
var stream = require('stream');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
var dgram__default = /*#__PURE__*/_interopDefault(dgram);
|
|
13
|
+
var WebSocket__default = /*#__PURE__*/_interopDefault(WebSocket);
|
|
14
|
+
|
|
15
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
16
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
17
|
+
}) : x)(function(x) {
|
|
18
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
19
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
20
|
+
});
|
|
21
|
+
var NONCE_SIZE_XSALSA20 = 24;
|
|
22
|
+
var NONCE_SIZE_LITE = 4;
|
|
23
|
+
var EncryptionMode = /* @__PURE__ */ ((EncryptionMode2) => {
|
|
24
|
+
EncryptionMode2["XSalsa20Poly1305"] = "xsalsa20_poly1305";
|
|
25
|
+
EncryptionMode2["XSalsa20Poly1305Suffix"] = "xsalsa20_poly1305_suffix";
|
|
26
|
+
EncryptionMode2["XSalsa20Poly1305Lite"] = "xsalsa20_poly1305_lite";
|
|
27
|
+
EncryptionMode2["AeadAes256Gcm"] = "aead_aes256_gcm";
|
|
28
|
+
EncryptionMode2["AeadXChaCha20Poly1305"] = "aead_xchacha20_poly1305_rtpsize";
|
|
29
|
+
return EncryptionMode2;
|
|
30
|
+
})(EncryptionMode || {});
|
|
31
|
+
var PREFERRED_MODES = [
|
|
32
|
+
"aead_xchacha20_poly1305_rtpsize" /* AeadXChaCha20Poly1305 */,
|
|
33
|
+
"aead_aes256_gcm" /* AeadAes256Gcm */,
|
|
34
|
+
"xsalsa20_poly1305_lite" /* XSalsa20Poly1305Lite */,
|
|
35
|
+
"xsalsa20_poly1305_suffix" /* XSalsa20Poly1305Suffix */,
|
|
36
|
+
"xsalsa20_poly1305" /* XSalsa20Poly1305 */
|
|
37
|
+
];
|
|
38
|
+
function selectEncryptionMode(available) {
|
|
39
|
+
for (const preferred of PREFERRED_MODES) {
|
|
40
|
+
if (available.includes(preferred)) {
|
|
41
|
+
return preferred;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const first = available[0];
|
|
45
|
+
if (first !== void 0) {
|
|
46
|
+
return first;
|
|
47
|
+
}
|
|
48
|
+
return "xsalsa20_poly1305" /* XSalsa20Poly1305 */;
|
|
49
|
+
}
|
|
50
|
+
function encryptOpusPacket(rtpHeader, payload, secretKey, mode, nonceCounter) {
|
|
51
|
+
switch (mode) {
|
|
52
|
+
case "xsalsa20_poly1305" /* XSalsa20Poly1305 */: {
|
|
53
|
+
const nonce = Buffer.alloc(NONCE_SIZE_XSALSA20);
|
|
54
|
+
rtpHeader.copy(nonce, 0, 0, 12);
|
|
55
|
+
const encrypted = native.secretboxEncrypt(payload, nonce, secretKey);
|
|
56
|
+
return Buffer.concat([rtpHeader, encrypted]);
|
|
57
|
+
}
|
|
58
|
+
case "xsalsa20_poly1305_suffix" /* XSalsa20Poly1305Suffix */: {
|
|
59
|
+
const nonce = native.randomBytes(NONCE_SIZE_XSALSA20);
|
|
60
|
+
const encrypted = native.secretboxEncrypt(payload, nonce, secretKey);
|
|
61
|
+
return Buffer.concat([rtpHeader, encrypted, nonce]);
|
|
62
|
+
}
|
|
63
|
+
case "xsalsa20_poly1305_lite" /* XSalsa20Poly1305Lite */: {
|
|
64
|
+
const nonce = Buffer.alloc(NONCE_SIZE_XSALSA20);
|
|
65
|
+
nonce.writeUInt32BE(nonceCounter, 0);
|
|
66
|
+
const encrypted = native.secretboxEncrypt(payload, nonce, secretKey);
|
|
67
|
+
const nonceAppend = Buffer.alloc(NONCE_SIZE_LITE);
|
|
68
|
+
nonceAppend.writeUInt32BE(nonceCounter, 0);
|
|
69
|
+
return Buffer.concat([rtpHeader, encrypted, nonceAppend]);
|
|
70
|
+
}
|
|
71
|
+
case "aead_xchacha20_poly1305_rtpsize" /* AeadXChaCha20Poly1305 */: {
|
|
72
|
+
const nonce = Buffer.alloc(24);
|
|
73
|
+
nonce.writeUInt32BE(nonceCounter, 0);
|
|
74
|
+
const encrypted = native.xchacha20Encrypt(payload, nonce, secretKey, rtpHeader);
|
|
75
|
+
const nonceAppend = Buffer.alloc(4);
|
|
76
|
+
nonceAppend.writeUInt32BE(nonceCounter, 0);
|
|
77
|
+
return Buffer.concat([rtpHeader, encrypted, nonceAppend]);
|
|
78
|
+
}
|
|
79
|
+
default:
|
|
80
|
+
return Buffer.concat([rtpHeader, payload]);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function decryptOpusPacket(packet, secretKey, mode) {
|
|
84
|
+
const rtpHeader = packet.subarray(0, 12);
|
|
85
|
+
switch (mode) {
|
|
86
|
+
case "xsalsa20_poly1305" /* XSalsa20Poly1305 */: {
|
|
87
|
+
const nonce = Buffer.alloc(NONCE_SIZE_XSALSA20);
|
|
88
|
+
rtpHeader.copy(nonce, 0, 0, 12);
|
|
89
|
+
const ciphertext = packet.subarray(12);
|
|
90
|
+
return native.secretboxDecrypt(ciphertext, nonce, secretKey);
|
|
91
|
+
}
|
|
92
|
+
case "xsalsa20_poly1305_suffix" /* XSalsa20Poly1305Suffix */: {
|
|
93
|
+
const nonce = packet.subarray(packet.length - NONCE_SIZE_XSALSA20);
|
|
94
|
+
const ciphertext = packet.subarray(12, packet.length - NONCE_SIZE_XSALSA20);
|
|
95
|
+
return native.secretboxDecrypt(ciphertext, nonce, secretKey);
|
|
96
|
+
}
|
|
97
|
+
case "xsalsa20_poly1305_lite" /* XSalsa20Poly1305Lite */: {
|
|
98
|
+
const nonce = Buffer.alloc(NONCE_SIZE_XSALSA20);
|
|
99
|
+
const nonceFragment = packet.subarray(packet.length - NONCE_SIZE_LITE);
|
|
100
|
+
nonceFragment.copy(nonce, 0, 0, NONCE_SIZE_LITE);
|
|
101
|
+
const ciphertext = packet.subarray(12, packet.length - NONCE_SIZE_LITE);
|
|
102
|
+
return native.secretboxDecrypt(ciphertext, nonce, secretKey);
|
|
103
|
+
}
|
|
104
|
+
case "aead_xchacha20_poly1305_rtpsize" /* AeadXChaCha20Poly1305 */: {
|
|
105
|
+
const nonce = Buffer.alloc(24);
|
|
106
|
+
const nonceFragment = packet.subarray(packet.length - 4);
|
|
107
|
+
nonceFragment.copy(nonce, 0, 0, 4);
|
|
108
|
+
const ciphertext = packet.subarray(12, packet.length - 4);
|
|
109
|
+
return native.xchacha20Decrypt(ciphertext, nonce, secretKey, rtpHeader);
|
|
110
|
+
}
|
|
111
|
+
default: {
|
|
112
|
+
return packet.subarray(12);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/errors.ts
|
|
118
|
+
var HarmoniaVoiceError = class extends Error {
|
|
119
|
+
code;
|
|
120
|
+
context;
|
|
121
|
+
constructor(code, message, context = {}) {
|
|
122
|
+
super(message);
|
|
123
|
+
this.name = "HarmoniaVoiceError";
|
|
124
|
+
this.code = code;
|
|
125
|
+
this.context = context;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/store.ts
|
|
130
|
+
var connections = /* @__PURE__ */ new Map();
|
|
131
|
+
function getVoiceConnection(guildId) {
|
|
132
|
+
return connections.get(guildId);
|
|
133
|
+
}
|
|
134
|
+
function getVoiceConnections() {
|
|
135
|
+
return connections;
|
|
136
|
+
}
|
|
137
|
+
function setVoiceConnection(guildId, connection) {
|
|
138
|
+
connections.set(guildId, connection);
|
|
139
|
+
}
|
|
140
|
+
function removeVoiceConnection(guildId) {
|
|
141
|
+
connections.delete(guildId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// src/types.ts
|
|
145
|
+
var VoiceConnectionState = /* @__PURE__ */ ((VoiceConnectionState2) => {
|
|
146
|
+
VoiceConnectionState2["Idle"] = "idle";
|
|
147
|
+
VoiceConnectionState2["Signalling"] = "signalling";
|
|
148
|
+
VoiceConnectionState2["Connecting"] = "connecting";
|
|
149
|
+
VoiceConnectionState2["Ready"] = "ready";
|
|
150
|
+
VoiceConnectionState2["Resuming"] = "resuming";
|
|
151
|
+
VoiceConnectionState2["Disconnected"] = "disconnected";
|
|
152
|
+
VoiceConnectionState2["Destroyed"] = "destroyed";
|
|
153
|
+
return VoiceConnectionState2;
|
|
154
|
+
})(VoiceConnectionState || {});
|
|
155
|
+
var AudioPlayerState = /* @__PURE__ */ ((AudioPlayerState2) => {
|
|
156
|
+
AudioPlayerState2["Idle"] = "idle";
|
|
157
|
+
AudioPlayerState2["Buffering"] = "buffering";
|
|
158
|
+
AudioPlayerState2["Playing"] = "playing";
|
|
159
|
+
AudioPlayerState2["Paused"] = "paused";
|
|
160
|
+
AudioPlayerState2["AutoPaused"] = "autopaused";
|
|
161
|
+
return AudioPlayerState2;
|
|
162
|
+
})(AudioPlayerState || {});
|
|
163
|
+
var UserAudioStream = class extends stream.Readable {
|
|
164
|
+
userId;
|
|
165
|
+
constructor(userId) {
|
|
166
|
+
super({ objectMode: false });
|
|
167
|
+
this.userId = userId;
|
|
168
|
+
}
|
|
169
|
+
_read() {
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
var AudioReceiver = class extends events.EventEmitter {
|
|
173
|
+
connection;
|
|
174
|
+
ssrcMap = /* @__PURE__ */ new Map();
|
|
175
|
+
userSsrcMap = /* @__PURE__ */ new Map();
|
|
176
|
+
subscriptions = /* @__PURE__ */ new Map();
|
|
177
|
+
constructor(connection) {
|
|
178
|
+
super();
|
|
179
|
+
this.connection = connection;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Subscribe to audio from a specific user.
|
|
183
|
+
*
|
|
184
|
+
* @param userId - The Discord user ID to receive audio from
|
|
185
|
+
* @returns A readable stream of PCM audio from that user
|
|
186
|
+
*
|
|
187
|
+
* @example
|
|
188
|
+
* ```ts
|
|
189
|
+
* const stream = receiver.subscribe('123456789');
|
|
190
|
+
* stream.pipe(fileWriteStream);
|
|
191
|
+
* ```
|
|
192
|
+
*/
|
|
193
|
+
subscribe(userId) {
|
|
194
|
+
const existing = this.subscriptions.get(userId);
|
|
195
|
+
if (existing) {
|
|
196
|
+
return existing;
|
|
197
|
+
}
|
|
198
|
+
const stream = new UserAudioStream(userId);
|
|
199
|
+
this.subscriptions.set(userId, stream);
|
|
200
|
+
stream.on("close", () => {
|
|
201
|
+
this.subscriptions.delete(userId);
|
|
202
|
+
});
|
|
203
|
+
return stream;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Unsubscribe from a user's audio.
|
|
207
|
+
*
|
|
208
|
+
* @example
|
|
209
|
+
* ```ts
|
|
210
|
+
* receiver.unsubscribe('123456789');
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
unsubscribe(userId) {
|
|
214
|
+
const stream = this.subscriptions.get(userId);
|
|
215
|
+
if (stream) {
|
|
216
|
+
stream.destroy();
|
|
217
|
+
this.subscriptions.delete(userId);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** @internal */
|
|
221
|
+
mapSsrcToUser(ssrc, userId) {
|
|
222
|
+
let state = this.ssrcMap.get(ssrc);
|
|
223
|
+
if (!state) {
|
|
224
|
+
state = {
|
|
225
|
+
userId,
|
|
226
|
+
decoder: new codec.OpusDecoder({ sampleRate: 48e3, channels: 2 }),
|
|
227
|
+
stream: new UserAudioStream(userId)
|
|
228
|
+
};
|
|
229
|
+
this.ssrcMap.set(ssrc, state);
|
|
230
|
+
} else {
|
|
231
|
+
state.userId = userId;
|
|
232
|
+
}
|
|
233
|
+
this.userSsrcMap.set(userId, ssrc);
|
|
234
|
+
this.emit("userConnected", userId, ssrc);
|
|
235
|
+
}
|
|
236
|
+
/** @internal */
|
|
237
|
+
removeUser(userId) {
|
|
238
|
+
const ssrc = this.userSsrcMap.get(userId);
|
|
239
|
+
if (ssrc !== void 0) {
|
|
240
|
+
const state = this.ssrcMap.get(ssrc);
|
|
241
|
+
if (state) {
|
|
242
|
+
state.stream.destroy();
|
|
243
|
+
this.ssrcMap.delete(ssrc);
|
|
244
|
+
}
|
|
245
|
+
this.userSsrcMap.delete(userId);
|
|
246
|
+
}
|
|
247
|
+
this.subscriptions.get(userId)?.destroy();
|
|
248
|
+
this.subscriptions.delete(userId);
|
|
249
|
+
this.emit("userDisconnected", userId);
|
|
250
|
+
}
|
|
251
|
+
/** @internal */
|
|
252
|
+
handlePacket(rawPacket, decryptedPayload) {
|
|
253
|
+
try {
|
|
254
|
+
const header = native.parseRtpHeader(rawPacket);
|
|
255
|
+
const state = this.ssrcMap.get(header.ssrc);
|
|
256
|
+
if (!state?.userId) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const pcm = state.decoder.decode(decryptedPayload);
|
|
260
|
+
const subscription = this.subscriptions.get(state.userId);
|
|
261
|
+
if (subscription && !subscription.destroyed) {
|
|
262
|
+
subscription.push(pcm);
|
|
263
|
+
}
|
|
264
|
+
this.emit("audio", state.userId, pcm, header);
|
|
265
|
+
} catch {
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Destroy all streams and clean up.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* receiver.destroy();
|
|
274
|
+
* ```
|
|
275
|
+
*/
|
|
276
|
+
destroy() {
|
|
277
|
+
for (const [, stream] of this.subscriptions) {
|
|
278
|
+
stream.destroy();
|
|
279
|
+
}
|
|
280
|
+
this.subscriptions.clear();
|
|
281
|
+
this.ssrcMap.clear();
|
|
282
|
+
this.userSsrcMap.clear();
|
|
283
|
+
return this;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
// src/connection.ts
|
|
288
|
+
var VOICE_GATEWAY_VERSION = 8;
|
|
289
|
+
var HEARTBEAT_MAX_MISSED = 3;
|
|
290
|
+
var IP_DISCOVERY_PACKET_SIZE = 74;
|
|
291
|
+
var VoiceConnection = class extends events.EventEmitter {
|
|
292
|
+
_state = "idle" /* Idle */;
|
|
293
|
+
guildId;
|
|
294
|
+
channelId;
|
|
295
|
+
selfDeaf;
|
|
296
|
+
selfMute;
|
|
297
|
+
adapter;
|
|
298
|
+
sessionId;
|
|
299
|
+
voiceToken;
|
|
300
|
+
endpoint;
|
|
301
|
+
ws;
|
|
302
|
+
udpSocket;
|
|
303
|
+
ssrc = 0;
|
|
304
|
+
remoteIp = "";
|
|
305
|
+
remotePort = 0;
|
|
306
|
+
localIp = "";
|
|
307
|
+
localPort = 0;
|
|
308
|
+
secretKey;
|
|
309
|
+
encryptionMode = "xsalsa20_poly1305" /* XSalsa20Poly1305 */;
|
|
310
|
+
sequence = 0;
|
|
311
|
+
timestamp = 0;
|
|
312
|
+
nonceCounter = 0;
|
|
313
|
+
heartbeatInterval;
|
|
314
|
+
heartbeatNonce = 0;
|
|
315
|
+
missedHeartbeats = 0;
|
|
316
|
+
lastHeartbeatAck = 0;
|
|
317
|
+
subscribedPlayer;
|
|
318
|
+
audioThread;
|
|
319
|
+
ringBuffer;
|
|
320
|
+
receiver;
|
|
321
|
+
voiceStateResolve;
|
|
322
|
+
voiceServerResolve;
|
|
323
|
+
constructor(options) {
|
|
324
|
+
super();
|
|
325
|
+
this.guildId = options.guildId;
|
|
326
|
+
this.channelId = options.channelId;
|
|
327
|
+
this.selfDeaf = options.selfDeaf ?? false;
|
|
328
|
+
this.selfMute = options.selfMute ?? false;
|
|
329
|
+
this.receiver = new AudioReceiver(this);
|
|
330
|
+
this.adapter = options.adapterCreator({
|
|
331
|
+
onVoiceStateUpdate: (data) => this.handleVoiceStateUpdate(data),
|
|
332
|
+
onVoiceServerUpdate: (data) => this.handleVoiceServerUpdate(data),
|
|
333
|
+
destroy: () => this.destroy()
|
|
334
|
+
});
|
|
335
|
+
setVoiceConnection(this.guildId, this);
|
|
336
|
+
this.sendVoiceStateUpdate();
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Current connection state.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* ```ts
|
|
343
|
+
* if (connection.state === VoiceConnectionState.Ready) { ... }
|
|
344
|
+
* ```
|
|
345
|
+
*/
|
|
346
|
+
get state() {
|
|
347
|
+
return this._state;
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Get the audio receiver for this connection.
|
|
351
|
+
*
|
|
352
|
+
* @example
|
|
353
|
+
* ```ts
|
|
354
|
+
* const receiver = connection.getReceiver();
|
|
355
|
+
* ```
|
|
356
|
+
*/
|
|
357
|
+
getReceiver() {
|
|
358
|
+
return this.receiver;
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Subscribe an audio player to this connection.
|
|
362
|
+
*
|
|
363
|
+
* @example
|
|
364
|
+
* ```ts
|
|
365
|
+
* connection.subscribe(player);
|
|
366
|
+
* ```
|
|
367
|
+
*/
|
|
368
|
+
subscribe(player) {
|
|
369
|
+
this.subscribedPlayer = player;
|
|
370
|
+
}
|
|
371
|
+
/**
|
|
372
|
+
* Unsubscribe the current audio player.
|
|
373
|
+
*
|
|
374
|
+
* @example
|
|
375
|
+
* ```ts
|
|
376
|
+
* connection.unsubscribe();
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
unsubscribe() {
|
|
380
|
+
this.subscribedPlayer = void 0;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Destroy the connection and clean up all resources.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* ```ts
|
|
387
|
+
* connection.destroy();
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
destroy() {
|
|
391
|
+
if (this._state === "destroyed" /* Destroyed */) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
this.transitionTo("destroyed" /* Destroyed */);
|
|
395
|
+
this.cleanup();
|
|
396
|
+
removeVoiceConnection(this.guildId);
|
|
397
|
+
if (this.adapter) {
|
|
398
|
+
this.adapter.sendPayload({
|
|
399
|
+
op: 4,
|
|
400
|
+
d: {
|
|
401
|
+
guild_id: this.guildId,
|
|
402
|
+
channel_id: null,
|
|
403
|
+
self_mute: false,
|
|
404
|
+
self_deaf: false
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
this.adapter.destroy();
|
|
408
|
+
this.adapter = void 0;
|
|
409
|
+
}
|
|
410
|
+
this.emit("destroyed");
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Send an Opus packet through the voice connection.
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```ts
|
|
417
|
+
* connection.sendOpusPacket(opusBuffer);
|
|
418
|
+
* ```
|
|
419
|
+
*/
|
|
420
|
+
sendOpusPacket(opusPacket) {
|
|
421
|
+
if (this._state !== "ready" /* Ready */) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
if (!this.secretKey || !this.udpSocket) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
const header = native.buildRtpHeader(this.sequence, this.timestamp, this.ssrc, 120);
|
|
428
|
+
const encrypted = encryptOpusPacket(
|
|
429
|
+
header,
|
|
430
|
+
opusPacket,
|
|
431
|
+
this.secretKey,
|
|
432
|
+
this.encryptionMode,
|
|
433
|
+
this.nonceCounter
|
|
434
|
+
);
|
|
435
|
+
this.udpSocket.send(encrypted, this.remotePort, this.remoteIp);
|
|
436
|
+
this.sequence = this.sequence + 1 & 65535;
|
|
437
|
+
this.timestamp = this.timestamp + 960 >>> 0;
|
|
438
|
+
this.nonceCounter = this.nonceCounter + 1 >>> 0;
|
|
439
|
+
}
|
|
440
|
+
sendVoiceStateUpdate() {
|
|
441
|
+
this.transitionTo("signalling" /* Signalling */);
|
|
442
|
+
this.adapter?.sendPayload({
|
|
443
|
+
op: 4,
|
|
444
|
+
d: {
|
|
445
|
+
guild_id: this.guildId,
|
|
446
|
+
channel_id: this.channelId,
|
|
447
|
+
self_mute: this.selfMute,
|
|
448
|
+
self_deaf: this.selfDeaf
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
handleVoiceStateUpdate(data) {
|
|
453
|
+
this.sessionId = data.session_id;
|
|
454
|
+
if (this.voiceStateResolve) {
|
|
455
|
+
this.voiceStateResolve();
|
|
456
|
+
this.voiceStateResolve = void 0;
|
|
457
|
+
}
|
|
458
|
+
this.tryConnect();
|
|
459
|
+
}
|
|
460
|
+
handleVoiceServerUpdate(data) {
|
|
461
|
+
this.voiceToken = data.token;
|
|
462
|
+
this.endpoint = data.endpoint ?? void 0;
|
|
463
|
+
if (this.voiceServerResolve) {
|
|
464
|
+
this.voiceServerResolve();
|
|
465
|
+
this.voiceServerResolve = void 0;
|
|
466
|
+
}
|
|
467
|
+
this.tryConnect();
|
|
468
|
+
}
|
|
469
|
+
tryConnect() {
|
|
470
|
+
if (!this.sessionId || !this.voiceToken || !this.endpoint) {
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
this.transitionTo("connecting" /* Connecting */);
|
|
474
|
+
this.connectWebSocket();
|
|
475
|
+
}
|
|
476
|
+
connectWebSocket() {
|
|
477
|
+
const wsUrl = `wss://${this.endpoint}/?v=${VOICE_GATEWAY_VERSION}`;
|
|
478
|
+
this.ws?.close();
|
|
479
|
+
this.ws = new WebSocket__default.default(wsUrl);
|
|
480
|
+
this.ws.on("open", () => {
|
|
481
|
+
this.sendIdentify();
|
|
482
|
+
});
|
|
483
|
+
this.ws.on("message", (raw) => {
|
|
484
|
+
const data = JSON.parse(raw.toString());
|
|
485
|
+
this.handleGatewayMessage(data.op, data.d);
|
|
486
|
+
});
|
|
487
|
+
this.ws.on("close", (code) => {
|
|
488
|
+
this.stopHeartbeat();
|
|
489
|
+
if (this._state !== "destroyed" /* Destroyed */) {
|
|
490
|
+
if (code === 4014 || code === 4006) {
|
|
491
|
+
this.transitionTo("disconnected" /* Disconnected */);
|
|
492
|
+
this.emit("disconnected");
|
|
493
|
+
} else if (this._state !== "disconnected" /* Disconnected */) {
|
|
494
|
+
this.transitionTo("resuming" /* Resuming */);
|
|
495
|
+
setTimeout(() => this.connectWebSocket(), 1e3);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
this.ws.on("error", (error) => {
|
|
500
|
+
this.emit("error", error);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
sendIdentify() {
|
|
504
|
+
this.wsSend(0, {
|
|
505
|
+
server_id: this.guildId,
|
|
506
|
+
user_id: "",
|
|
507
|
+
// filled by adapter
|
|
508
|
+
session_id: this.sessionId,
|
|
509
|
+
token: this.voiceToken
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
handleGatewayMessage(op, d) {
|
|
513
|
+
switch (op) {
|
|
514
|
+
case 2:
|
|
515
|
+
this.handleReady(d);
|
|
516
|
+
break;
|
|
517
|
+
case 4:
|
|
518
|
+
this.handleSessionDescription(d);
|
|
519
|
+
break;
|
|
520
|
+
case 5:
|
|
521
|
+
this.handleSpeaking(d);
|
|
522
|
+
break;
|
|
523
|
+
case 6:
|
|
524
|
+
this.handleHeartbeatAck(d);
|
|
525
|
+
break;
|
|
526
|
+
case 8:
|
|
527
|
+
this.handleHello(d);
|
|
528
|
+
break;
|
|
529
|
+
case 9:
|
|
530
|
+
this.transitionTo("ready" /* Ready */);
|
|
531
|
+
break;
|
|
532
|
+
case 12:
|
|
533
|
+
this.handleClientConnect(d);
|
|
534
|
+
break;
|
|
535
|
+
case 13:
|
|
536
|
+
this.handleClientDisconnect(d);
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
handleReady(data) {
|
|
541
|
+
this.ssrc = data.ssrc;
|
|
542
|
+
this.remoteIp = data.ip;
|
|
543
|
+
this.remotePort = data.port;
|
|
544
|
+
this.encryptionMode = selectEncryptionMode(data.modes);
|
|
545
|
+
this.connectUdp();
|
|
546
|
+
}
|
|
547
|
+
connectUdp() {
|
|
548
|
+
this.udpSocket?.close();
|
|
549
|
+
this.udpSocket = dgram__default.default.createSocket("udp4");
|
|
550
|
+
this.udpSocket.on("message", (msg) => {
|
|
551
|
+
this.handleUdpMessage(msg);
|
|
552
|
+
});
|
|
553
|
+
this.udpSocket.on("error", (error) => {
|
|
554
|
+
this.emit("error", error);
|
|
555
|
+
});
|
|
556
|
+
this.udpSocket.bind(0, () => {
|
|
557
|
+
const address = this.udpSocket?.address();
|
|
558
|
+
if (address) {
|
|
559
|
+
this.localPort = address.port;
|
|
560
|
+
const fd = this.udpSocket?._handle?.fd;
|
|
561
|
+
if (typeof fd === "number" && fd >= 0) {
|
|
562
|
+
try {
|
|
563
|
+
native.configureSocketForAudio(fd);
|
|
564
|
+
} catch {
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
this.performIpDiscovery();
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
performIpDiscovery() {
|
|
572
|
+
const discoveryPacket = Buffer.alloc(IP_DISCOVERY_PACKET_SIZE);
|
|
573
|
+
discoveryPacket.writeUInt16BE(1, 0);
|
|
574
|
+
discoveryPacket.writeUInt16BE(70, 2);
|
|
575
|
+
discoveryPacket.writeUInt32BE(this.ssrc, 4);
|
|
576
|
+
this.udpSocket?.send(
|
|
577
|
+
discoveryPacket,
|
|
578
|
+
this.remotePort,
|
|
579
|
+
this.remoteIp
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
handleUdpMessage(msg) {
|
|
583
|
+
if (msg.length === IP_DISCOVERY_PACKET_SIZE) {
|
|
584
|
+
this.handleIpDiscoveryResponse(msg);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (msg.length > 12 && this.secretKey) {
|
|
588
|
+
try {
|
|
589
|
+
const decrypted = decryptOpusPacket(msg, this.secretKey, this.encryptionMode);
|
|
590
|
+
this.receiver.handlePacket(msg, decrypted);
|
|
591
|
+
} catch {
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
handleIpDiscoveryResponse(response) {
|
|
596
|
+
const ipEnd = response.indexOf(0, 8);
|
|
597
|
+
this.localIp = response.subarray(8, ipEnd > 8 ? ipEnd : 72).toString("utf8").replace(/\0/g, "");
|
|
598
|
+
this.localPort = response.readUInt16BE(72);
|
|
599
|
+
this.wsSend(1, {
|
|
600
|
+
protocol: "udp",
|
|
601
|
+
data: {
|
|
602
|
+
address: this.localIp,
|
|
603
|
+
port: this.localPort,
|
|
604
|
+
mode: this.encryptionMode
|
|
605
|
+
}
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
handleSessionDescription(d) {
|
|
609
|
+
const keyArray = d["secret_key"];
|
|
610
|
+
this.secretKey = Buffer.from(keyArray);
|
|
611
|
+
this.encryptionMode = d["mode"];
|
|
612
|
+
this.sequence = 0;
|
|
613
|
+
this.timestamp = 0;
|
|
614
|
+
this.nonceCounter = 0;
|
|
615
|
+
this.transitionTo("ready" /* Ready */);
|
|
616
|
+
this.emit("ready");
|
|
617
|
+
this.setSpeaking(1);
|
|
618
|
+
}
|
|
619
|
+
handleSpeaking(d) {
|
|
620
|
+
const userId = d["user_id"];
|
|
621
|
+
const ssrc = d["ssrc"];
|
|
622
|
+
this.receiver.mapSsrcToUser(ssrc, userId);
|
|
623
|
+
}
|
|
624
|
+
handleClientConnect(d) {
|
|
625
|
+
const userId = d["user_id"];
|
|
626
|
+
const audioSsrc = d["audio_ssrc"];
|
|
627
|
+
if (audioSsrc) {
|
|
628
|
+
this.receiver.mapSsrcToUser(audioSsrc, userId);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
handleClientDisconnect(d) {
|
|
632
|
+
const userId = d["user_id"];
|
|
633
|
+
this.receiver.removeUser(userId);
|
|
634
|
+
}
|
|
635
|
+
handleHello(d) {
|
|
636
|
+
const interval = d["heartbeat_interval"];
|
|
637
|
+
this.startHeartbeat(interval);
|
|
638
|
+
}
|
|
639
|
+
handleHeartbeatAck(_d) {
|
|
640
|
+
this.missedHeartbeats = 0;
|
|
641
|
+
this.lastHeartbeatAck = Date.now();
|
|
642
|
+
}
|
|
643
|
+
startHeartbeat(intervalMs) {
|
|
644
|
+
this.stopHeartbeat();
|
|
645
|
+
this.missedHeartbeats = 0;
|
|
646
|
+
this.heartbeatInterval = setInterval(() => {
|
|
647
|
+
if (this.missedHeartbeats >= HEARTBEAT_MAX_MISSED) {
|
|
648
|
+
this.ws?.close(4009);
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
this.heartbeatNonce = Date.now();
|
|
652
|
+
this.wsSend(3, this.heartbeatNonce);
|
|
653
|
+
this.missedHeartbeats++;
|
|
654
|
+
}, intervalMs);
|
|
655
|
+
}
|
|
656
|
+
stopHeartbeat() {
|
|
657
|
+
if (this.heartbeatInterval) {
|
|
658
|
+
clearInterval(this.heartbeatInterval);
|
|
659
|
+
this.heartbeatInterval = void 0;
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Set the speaking flags.
|
|
664
|
+
*
|
|
665
|
+
* @example
|
|
666
|
+
* ```ts
|
|
667
|
+
* connection.setSpeaking(1); // speaking
|
|
668
|
+
* ```
|
|
669
|
+
*/
|
|
670
|
+
setSpeaking(flags) {
|
|
671
|
+
this.wsSend(5, {
|
|
672
|
+
speaking: flags,
|
|
673
|
+
delay: 0,
|
|
674
|
+
ssrc: this.ssrc
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
wsSend(op, d) {
|
|
678
|
+
if (this.ws?.readyState === WebSocket__default.default.OPEN) {
|
|
679
|
+
this.ws.send(JSON.stringify({ op, d }));
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
transitionTo(newState) {
|
|
683
|
+
const oldState = this._state;
|
|
684
|
+
if (oldState === newState) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
this._state = newState;
|
|
688
|
+
this.emit("stateChange", oldState, newState);
|
|
689
|
+
}
|
|
690
|
+
cleanup() {
|
|
691
|
+
this.stopHeartbeat();
|
|
692
|
+
if (this.audioThread) {
|
|
693
|
+
this.audioThread.stop();
|
|
694
|
+
this.audioThread = void 0;
|
|
695
|
+
}
|
|
696
|
+
if (this.ws) {
|
|
697
|
+
this.ws.removeAllListeners();
|
|
698
|
+
this.ws.close();
|
|
699
|
+
this.ws = void 0;
|
|
700
|
+
}
|
|
701
|
+
if (this.udpSocket) {
|
|
702
|
+
this.udpSocket.removeAllListeners();
|
|
703
|
+
this.udpSocket.close();
|
|
704
|
+
this.udpSocket = void 0;
|
|
705
|
+
}
|
|
706
|
+
this.secretKey = void 0;
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
var AudioPlayer = class extends events.EventEmitter {
|
|
710
|
+
_state = "idle" /* Idle */;
|
|
711
|
+
resource;
|
|
712
|
+
connections = /* @__PURE__ */ new Set();
|
|
713
|
+
playbackTimer;
|
|
714
|
+
noSubscriberBehavior;
|
|
715
|
+
constructor(options = {}) {
|
|
716
|
+
super();
|
|
717
|
+
this.noSubscriberBehavior = options.noSubscriber ?? "pause";
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Current player state.
|
|
721
|
+
*
|
|
722
|
+
* @example
|
|
723
|
+
* ```ts
|
|
724
|
+
* if (player.state === AudioPlayerState.Playing) { ... }
|
|
725
|
+
* ```
|
|
726
|
+
*/
|
|
727
|
+
get state() {
|
|
728
|
+
return this._state;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* Start playing an audio resource.
|
|
732
|
+
*
|
|
733
|
+
* @example
|
|
734
|
+
* ```ts
|
|
735
|
+
* player.play(audioResource);
|
|
736
|
+
* ```
|
|
737
|
+
*/
|
|
738
|
+
play(resource) {
|
|
739
|
+
this.stopInternal();
|
|
740
|
+
this.resource = resource;
|
|
741
|
+
this.transitionTo("playing" /* Playing */);
|
|
742
|
+
this.startPlaybackLoop();
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Pause playback.
|
|
746
|
+
*
|
|
747
|
+
* @example
|
|
748
|
+
* ```ts
|
|
749
|
+
* player.pause();
|
|
750
|
+
* ```
|
|
751
|
+
*/
|
|
752
|
+
pause() {
|
|
753
|
+
if (this._state === "playing" /* Playing */) {
|
|
754
|
+
this.transitionTo("paused" /* Paused */);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
/**
|
|
758
|
+
* Resume playback.
|
|
759
|
+
*
|
|
760
|
+
* @example
|
|
761
|
+
* ```ts
|
|
762
|
+
* player.resume();
|
|
763
|
+
* ```
|
|
764
|
+
*/
|
|
765
|
+
resume() {
|
|
766
|
+
if (this._state === "paused" /* Paused */ || this._state === "autopaused" /* AutoPaused */) {
|
|
767
|
+
this.transitionTo("playing" /* Playing */);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
771
|
+
* Stop playback and release the resource.
|
|
772
|
+
*
|
|
773
|
+
* @example
|
|
774
|
+
* ```ts
|
|
775
|
+
* player.stop();
|
|
776
|
+
* ```
|
|
777
|
+
*/
|
|
778
|
+
stop() {
|
|
779
|
+
this.stopInternal();
|
|
780
|
+
this.transitionTo("idle" /* Idle */);
|
|
781
|
+
this.emit("idle");
|
|
782
|
+
}
|
|
783
|
+
/** @internal */
|
|
784
|
+
addConnection(connection) {
|
|
785
|
+
this.connections.add(connection);
|
|
786
|
+
}
|
|
787
|
+
/** @internal */
|
|
788
|
+
removeConnection(connection) {
|
|
789
|
+
this.connections.delete(connection);
|
|
790
|
+
}
|
|
791
|
+
startPlaybackLoop() {
|
|
792
|
+
this.playbackTimer = setInterval(() => {
|
|
793
|
+
this.processFrame();
|
|
794
|
+
}, 20);
|
|
795
|
+
}
|
|
796
|
+
processFrame() {
|
|
797
|
+
if (this._state !== "playing" /* Playing */) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
if (!this.resource) {
|
|
801
|
+
this.stop();
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (this.resource.ended) {
|
|
805
|
+
this.stopInternal();
|
|
806
|
+
this.transitionTo("idle" /* Idle */);
|
|
807
|
+
this.emit("idle");
|
|
808
|
+
return;
|
|
809
|
+
}
|
|
810
|
+
const packet = this.resource.read();
|
|
811
|
+
if (!packet) {
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
for (const connection of this.connections) {
|
|
815
|
+
connection.sendOpusPacket(packet);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
stopInternal() {
|
|
819
|
+
if (this.playbackTimer) {
|
|
820
|
+
clearInterval(this.playbackTimer);
|
|
821
|
+
this.playbackTimer = void 0;
|
|
822
|
+
}
|
|
823
|
+
if (this.resource) {
|
|
824
|
+
this.resource.destroy();
|
|
825
|
+
this.resource = void 0;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
transitionTo(newState) {
|
|
829
|
+
const oldState = this._state;
|
|
830
|
+
if (oldState === newState) {
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
this._state = newState;
|
|
834
|
+
this.emit("stateChange", oldState, newState);
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
|
|
838
|
+
// src/join.ts
|
|
839
|
+
function joinVoiceChannel(options) {
|
|
840
|
+
const existing = getVoiceConnection(options.guildId);
|
|
841
|
+
if (existing) {
|
|
842
|
+
existing.destroy();
|
|
843
|
+
}
|
|
844
|
+
return new VoiceConnection(options);
|
|
845
|
+
}
|
|
846
|
+
var AudioResource = class {
|
|
847
|
+
stream;
|
|
848
|
+
packets = [];
|
|
849
|
+
_ended = false;
|
|
850
|
+
draining = false;
|
|
851
|
+
constructor(stream) {
|
|
852
|
+
this.stream = stream;
|
|
853
|
+
stream.on("data", (chunk) => {
|
|
854
|
+
this.packets.push(chunk);
|
|
855
|
+
});
|
|
856
|
+
stream.on("end", () => {
|
|
857
|
+
this._ended = true;
|
|
858
|
+
});
|
|
859
|
+
stream.on("error", () => {
|
|
860
|
+
this._ended = true;
|
|
861
|
+
});
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Read the next Opus packet.
|
|
865
|
+
*
|
|
866
|
+
* @example
|
|
867
|
+
* ```ts
|
|
868
|
+
* const packet = resource.read();
|
|
869
|
+
* ```
|
|
870
|
+
*/
|
|
871
|
+
read() {
|
|
872
|
+
return this.packets.shift() ?? null;
|
|
873
|
+
}
|
|
874
|
+
/** Whether the resource has finished producing packets. */
|
|
875
|
+
get ended() {
|
|
876
|
+
return this._ended && this.packets.length === 0;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* Destroy the resource and release underlying streams.
|
|
880
|
+
*
|
|
881
|
+
* @example
|
|
882
|
+
* ```ts
|
|
883
|
+
* resource.destroy();
|
|
884
|
+
* ```
|
|
885
|
+
*/
|
|
886
|
+
destroy() {
|
|
887
|
+
this.stream.destroy();
|
|
888
|
+
this.packets.length = 0;
|
|
889
|
+
this._ended = true;
|
|
890
|
+
}
|
|
891
|
+
};
|
|
892
|
+
function createAudioResource(input, options = {}) {
|
|
893
|
+
let readable;
|
|
894
|
+
if (typeof input === "string") {
|
|
895
|
+
const { createReadStream } = __require("fs");
|
|
896
|
+
readable = createReadStream(input);
|
|
897
|
+
} else if (Buffer.isBuffer(input)) {
|
|
898
|
+
readable = stream.Readable.from(input);
|
|
899
|
+
} else {
|
|
900
|
+
readable = input;
|
|
901
|
+
}
|
|
902
|
+
if (options.inputType === "opus") {
|
|
903
|
+
return new AudioResource(readable);
|
|
904
|
+
}
|
|
905
|
+
const encoder = new codec.OpusEncoderStream({
|
|
906
|
+
sampleRate: 48e3,
|
|
907
|
+
channels: 2,
|
|
908
|
+
application: "audio"
|
|
909
|
+
});
|
|
910
|
+
readable.pipe(encoder);
|
|
911
|
+
if (options.signal) {
|
|
912
|
+
options.signal.addEventListener(
|
|
913
|
+
"abort",
|
|
914
|
+
() => {
|
|
915
|
+
readable.destroy();
|
|
916
|
+
encoder.destroy();
|
|
917
|
+
},
|
|
918
|
+
{ once: true }
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
return new AudioResource(encoder);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
exports.AudioPlayer = AudioPlayer;
|
|
925
|
+
exports.AudioPlayerState = AudioPlayerState;
|
|
926
|
+
exports.AudioReceiver = AudioReceiver;
|
|
927
|
+
exports.EncryptionMode = EncryptionMode;
|
|
928
|
+
exports.HarmoniaVoiceError = HarmoniaVoiceError;
|
|
929
|
+
exports.VoiceConnection = VoiceConnection;
|
|
930
|
+
exports.VoiceConnectionState = VoiceConnectionState;
|
|
931
|
+
exports.createAudioResource = createAudioResource;
|
|
932
|
+
exports.getVoiceConnection = getVoiceConnection;
|
|
933
|
+
exports.getVoiceConnections = getVoiceConnections;
|
|
934
|
+
exports.joinVoiceChannel = joinVoiceChannel;
|
|
935
|
+
//# sourceMappingURL=index.cjs.map
|
|
936
|
+
//# sourceMappingURL=index.cjs.map
|