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