@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 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