@discordjs/voice 0.18.1-dev.1732709130-97ffa201a → 0.19.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.mjs CHANGED
@@ -10,7 +10,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
10
10
  });
11
11
 
12
12
  // src/VoiceConnection.ts
13
- import { EventEmitter as EventEmitter7 } from "node:events";
13
+ import { EventEmitter as EventEmitter8 } from "events";
14
14
 
15
15
  // src/DataStore.ts
16
16
  import { GatewayOpcodes } from "discord-api-types/v10";
@@ -109,13 +109,13 @@ function deleteAudioPlayer(player) {
109
109
  __name(deleteAudioPlayer, "deleteAudioPlayer");
110
110
 
111
111
  // src/networking/Networking.ts
112
- import { Buffer as Buffer4 } from "node:buffer";
113
- import crypto from "node:crypto";
114
- import { EventEmitter as EventEmitter3 } from "node:events";
115
- import { VoiceOpcodes as VoiceOpcodes2 } from "discord-api-types/voice/v4";
112
+ import { Buffer as Buffer7 } from "buffer";
113
+ import crypto from "crypto";
114
+ import { EventEmitter as EventEmitter5 } from "events";
115
+ import { VoiceEncryptionMode, VoiceOpcodes as VoiceOpcodes2 } from "discord-api-types/voice/v8";
116
116
 
117
117
  // src/util/Secretbox.ts
118
- import { Buffer as Buffer2 } from "node:buffer";
118
+ import { Buffer as Buffer2 } from "buffer";
119
119
  var libs = {
120
120
  "sodium-native": /* @__PURE__ */ __name((sodium) => ({
121
121
  crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => {
@@ -130,20 +130,12 @@ var libs = {
130
130
  }, "crypto_aead_xchacha20poly1305_ietf_encrypt")
131
131
  }), "sodium-native"),
132
132
  sodium: /* @__PURE__ */ __name((sodium) => ({
133
- crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => {
134
- return sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, null, nonce2, key);
135
- }, "crypto_aead_xchacha20poly1305_ietf_decrypt"),
136
- crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => {
137
- return sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key);
138
- }, "crypto_aead_xchacha20poly1305_ietf_encrypt")
133
+ crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_decrypt"),
134
+ crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_encrypt")
139
135
  }), "sodium"),
140
136
  "libsodium-wrappers": /* @__PURE__ */ __name((sodium) => ({
141
- crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => {
142
- return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, cipherText, additionalData, nonce2, key);
143
- }, "crypto_aead_xchacha20poly1305_ietf_decrypt"),
144
- crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => {
145
- return sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key);
146
- }, "crypto_aead_xchacha20poly1305_ietf_encrypt")
137
+ crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, cipherText, additionalData, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_decrypt"),
138
+ crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key), "crypto_aead_xchacha20poly1305_ietf_encrypt")
147
139
  }), "libsodium-wrappers"),
148
140
  "@stablelib/xchacha20poly1305": /* @__PURE__ */ __name((stablelib) => ({
149
141
  crypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, additionalData, nonce2, key) {
@@ -201,1094 +193,1608 @@ var secretboxLoadPromise = new Promise(async (resolve2) => {
201
193
  var noop = /* @__PURE__ */ __name(() => {
202
194
  }, "noop");
203
195
 
204
- // src/networking/VoiceUDPSocket.ts
205
- import { Buffer as Buffer3 } from "node:buffer";
206
- import { createSocket } from "node:dgram";
207
- import { EventEmitter } from "node:events";
208
- import { isIPv4 } from "node:net";
209
- function parseLocalPacket(message) {
210
- const packet = Buffer3.from(message);
211
- const ip = packet.slice(8, packet.indexOf(0, 8)).toString("utf8");
212
- if (!isIPv4(ip)) {
213
- throw new Error("Malformed IP address");
214
- }
215
- const port = packet.readUInt16BE(packet.length - 2);
216
- return { ip, port };
217
- }
218
- __name(parseLocalPacket, "parseLocalPacket");
219
- var KEEP_ALIVE_INTERVAL = 5e3;
220
- var MAX_COUNTER_VALUE = 2 ** 32 - 1;
221
- var VoiceUDPSocket = class extends EventEmitter {
196
+ // src/networking/DAVESession.ts
197
+ import { Buffer as Buffer4 } from "buffer";
198
+ import { EventEmitter as EventEmitter2 } from "events";
199
+
200
+ // src/audio/AudioPlayer.ts
201
+ import { Buffer as Buffer3 } from "buffer";
202
+ import { EventEmitter } from "events";
203
+
204
+ // src/audio/AudioPlayerError.ts
205
+ var AudioPlayerError = class extends Error {
222
206
  static {
223
- __name(this, "VoiceUDPSocket");
224
- }
225
- /**
226
- * The underlying network Socket for the VoiceUDPSocket.
227
- */
228
- socket;
229
- /**
230
- * The socket details for Discord (remote)
231
- */
232
- remote;
233
- /**
234
- * The counter used in the keep alive mechanism.
235
- */
236
- keepAliveCounter = 0;
237
- /**
238
- * The buffer used to write the keep alive counter into.
239
- */
240
- keepAliveBuffer;
241
- /**
242
- * The Node.js interval for the keep-alive mechanism.
243
- */
244
- keepAliveInterval;
245
- /**
246
- * The time taken to receive a response to keep alive messages.
247
- *
248
- * @deprecated This field is no longer updated as keep alive messages are no longer tracked.
249
- */
250
- ping;
251
- /**
252
- * Creates a new VoiceUDPSocket.
253
- *
254
- * @param remote - Details of the remote socket
255
- */
256
- constructor(remote) {
257
- super();
258
- this.socket = createSocket("udp4");
259
- this.socket.on("error", (error) => this.emit("error", error));
260
- this.socket.on("message", (buffer) => this.onMessage(buffer));
261
- this.socket.on("close", () => this.emit("close"));
262
- this.remote = remote;
263
- this.keepAliveBuffer = Buffer3.alloc(8);
264
- this.keepAliveInterval = setInterval(() => this.keepAlive(), KEEP_ALIVE_INTERVAL);
265
- setImmediate(() => this.keepAlive());
207
+ __name(this, "AudioPlayerError");
266
208
  }
267
209
  /**
268
- * Called when a message is received on the UDP socket.
269
- *
270
- * @param buffer - The received buffer
210
+ * The resource associated with the audio player at the time the error was thrown.
271
211
  */
272
- onMessage(buffer) {
273
- this.emit("message", buffer);
212
+ resource;
213
+ constructor(error, resource) {
214
+ super(error.message);
215
+ this.resource = resource;
216
+ this.name = error.name;
217
+ this.stack = error.stack;
274
218
  }
275
- /**
276
- * Called at a regular interval to check whether we are still able to send datagrams to Discord.
277
- */
278
- keepAlive() {
279
- this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0);
280
- this.send(this.keepAliveBuffer);
281
- this.keepAliveCounter++;
282
- if (this.keepAliveCounter > MAX_COUNTER_VALUE) {
283
- this.keepAliveCounter = 0;
284
- }
219
+ };
220
+
221
+ // src/audio/PlayerSubscription.ts
222
+ var PlayerSubscription = class {
223
+ static {
224
+ __name(this, "PlayerSubscription");
285
225
  }
286
226
  /**
287
- * Sends a buffer to Discord.
288
- *
289
- * @param buffer - The buffer to send
227
+ * The voice connection of this subscription.
290
228
  */
291
- send(buffer) {
292
- this.socket.send(buffer, this.remote.port, this.remote.ip);
293
- }
229
+ connection;
294
230
  /**
295
- * Closes the socket, the instance will not be able to be reused.
231
+ * The audio player of this subscription.
296
232
  */
297
- destroy() {
298
- try {
299
- this.socket.close();
300
- } catch {
301
- }
302
- clearInterval(this.keepAliveInterval);
233
+ player;
234
+ constructor(connection, player) {
235
+ this.connection = connection;
236
+ this.player = player;
303
237
  }
304
238
  /**
305
- * Performs IP discovery to discover the local address and port to be used for the voice connection.
306
- *
307
- * @param ssrc - The SSRC received from Discord
239
+ * Unsubscribes the connection from the audio player, meaning that the
240
+ * audio player cannot stream audio to it until a new subscription is made.
308
241
  */
309
- async performIPDiscovery(ssrc) {
310
- return new Promise((resolve2, reject) => {
311
- const listener = /* @__PURE__ */ __name((message) => {
312
- try {
313
- if (message.readUInt16BE(0) !== 2) return;
314
- const packet = parseLocalPacket(message);
315
- this.socket.off("message", listener);
316
- resolve2(packet);
317
- } catch {
318
- }
319
- }, "listener");
320
- this.socket.on("message", listener);
321
- this.socket.once("close", () => reject(new Error("Cannot perform IP discovery - socket closed")));
322
- const discoveryBuffer = Buffer3.alloc(74);
323
- discoveryBuffer.writeUInt16BE(1, 0);
324
- discoveryBuffer.writeUInt16BE(70, 2);
325
- discoveryBuffer.writeUInt32BE(ssrc, 4);
326
- this.send(discoveryBuffer);
327
- });
242
+ unsubscribe() {
243
+ this.connection["onSubscriptionRemoved"](this);
244
+ this.player["unsubscribe"](this);
328
245
  }
329
246
  };
330
247
 
331
- // src/networking/VoiceWebSocket.ts
332
- import { EventEmitter as EventEmitter2 } from "node:events";
333
- import { VoiceOpcodes } from "discord-api-types/voice/v4";
334
- import WebSocket from "ws";
335
- var VoiceWebSocket = class extends EventEmitter2 {
248
+ // src/audio/AudioPlayer.ts
249
+ var SILENCE_FRAME = Buffer3.from([248, 255, 254]);
250
+ var NoSubscriberBehavior = /* @__PURE__ */ ((NoSubscriberBehavior2) => {
251
+ NoSubscriberBehavior2["Pause"] = "pause";
252
+ NoSubscriberBehavior2["Play"] = "play";
253
+ NoSubscriberBehavior2["Stop"] = "stop";
254
+ return NoSubscriberBehavior2;
255
+ })(NoSubscriberBehavior || {});
256
+ var AudioPlayerStatus = /* @__PURE__ */ ((AudioPlayerStatus2) => {
257
+ AudioPlayerStatus2["AutoPaused"] = "autopaused";
258
+ AudioPlayerStatus2["Buffering"] = "buffering";
259
+ AudioPlayerStatus2["Idle"] = "idle";
260
+ AudioPlayerStatus2["Paused"] = "paused";
261
+ AudioPlayerStatus2["Playing"] = "playing";
262
+ return AudioPlayerStatus2;
263
+ })(AudioPlayerStatus || {});
264
+ function stringifyState(state) {
265
+ return JSON.stringify({
266
+ ...state,
267
+ resource: Reflect.has(state, "resource"),
268
+ stepTimeout: Reflect.has(state, "stepTimeout")
269
+ });
270
+ }
271
+ __name(stringifyState, "stringifyState");
272
+ var AudioPlayer = class extends EventEmitter {
336
273
  static {
337
- __name(this, "VoiceWebSocket");
274
+ __name(this, "AudioPlayer");
338
275
  }
339
276
  /**
340
- * The current heartbeat interval, if any.
341
- */
342
- heartbeatInterval;
343
- /**
344
- * The time (milliseconds since UNIX epoch) that the last heartbeat acknowledgement packet was received.
345
- * This is set to 0 if an acknowledgement packet hasn't been received yet.
346
- */
347
- lastHeartbeatAck;
348
- /**
349
- * The time (milliseconds since UNIX epoch) that the last heartbeat was sent. This is set to 0 if a heartbeat
350
- * hasn't been sent yet.
277
+ * The state that the AudioPlayer is in.
351
278
  */
352
- lastHeartbeatSend;
279
+ _state;
353
280
  /**
354
- * The number of consecutively missed heartbeats.
281
+ * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio
282
+ * to the streams in this list.
355
283
  */
356
- missedHeartbeats = 0;
284
+ subscribers = [];
357
285
  /**
358
- * The last recorded ping.
286
+ * The behavior that the player should follow when it enters certain situations.
359
287
  */
360
- ping;
288
+ behaviors;
361
289
  /**
362
290
  * The debug logger function, if debugging is enabled.
363
291
  */
364
292
  debug;
365
293
  /**
366
- * The underlying WebSocket of this wrapper.
294
+ * Creates a new AudioPlayer.
367
295
  */
368
- ws;
296
+ constructor(options = {}) {
297
+ super();
298
+ this._state = { status: "idle" /* Idle */ };
299
+ this.behaviors = {
300
+ noSubscriber: "pause" /* Pause */,
301
+ maxMissedFrames: 5,
302
+ ...options.behaviors
303
+ };
304
+ this.debug = options.debug === false ? null : (message) => this.emit("debug", message);
305
+ }
369
306
  /**
370
- * Creates a new VoiceWebSocket.
371
- *
372
- * @param address - The address to connect to
307
+ * A list of subscribed voice connections that can currently receive audio to play.
373
308
  */
374
- constructor(address, debug) {
375
- super();
376
- this.ws = new WebSocket(address);
377
- this.ws.onmessage = (err) => this.onMessage(err);
378
- this.ws.onopen = (err) => this.emit("open", err);
379
- this.ws.onerror = (err) => this.emit("error", err instanceof Error ? err : err.error);
380
- this.ws.onclose = (err) => this.emit("close", err);
381
- this.lastHeartbeatAck = 0;
382
- this.lastHeartbeatSend = 0;
383
- this.debug = debug ? (message) => this.emit("debug", message) : null;
309
+ get playable() {
310
+ return this.subscribers.filter(({ connection }) => connection.state.status === "ready" /* Ready */).map(({ connection }) => connection);
384
311
  }
385
312
  /**
386
- * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed.
313
+ * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed,
314
+ * then the existing subscription is used.
315
+ *
316
+ * @remarks
317
+ * This method should not be directly called. Instead, use VoiceConnection#subscribe.
318
+ * @param connection - The connection to subscribe
319
+ * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription
387
320
  */
388
- destroy() {
389
- try {
390
- this.debug?.("destroyed");
391
- this.setHeartbeatInterval(-1);
392
- this.ws.close(1e3);
393
- } catch (error) {
394
- const err = error;
395
- this.emit("error", err);
321
+ // @ts-ignore
322
+ subscribe(connection) {
323
+ const existingSubscription = this.subscribers.find((subscription) => subscription.connection === connection);
324
+ if (!existingSubscription) {
325
+ const subscription = new PlayerSubscription(connection, this);
326
+ this.subscribers.push(subscription);
327
+ setImmediate(() => this.emit("subscribe", subscription));
328
+ return subscription;
396
329
  }
330
+ return existingSubscription;
397
331
  }
398
332
  /**
399
- * Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them
400
- * as packets.
333
+ * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player.
401
334
  *
402
- * @param event - The message event
335
+ * @remarks
336
+ * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe.
337
+ * @param subscription - The subscription to remove
338
+ * @returns Whether or not the subscription existed on the player and was removed
403
339
  */
404
- onMessage(event) {
405
- if (typeof event.data !== "string") return;
406
- this.debug?.(`<< ${event.data}`);
407
- let packet;
408
- try {
409
- packet = JSON.parse(event.data);
410
- } catch (error) {
411
- const err = error;
412
- this.emit("error", err);
340
+ // @ts-ignore
341
+ unsubscribe(subscription) {
342
+ const index = this.subscribers.indexOf(subscription);
343
+ const exists = index !== -1;
344
+ if (exists) {
345
+ this.subscribers.splice(index, 1);
346
+ subscription.connection.setSpeaking(false);
347
+ this.emit("unsubscribe", subscription);
348
+ }
349
+ return exists;
350
+ }
351
+ /**
352
+ * The state that the player is in.
353
+ *
354
+ * @remarks
355
+ * The setter will perform clean-up operations where necessary.
356
+ */
357
+ get state() {
358
+ return this._state;
359
+ }
360
+ set state(newState) {
361
+ const oldState = this._state;
362
+ const newResource = Reflect.get(newState, "resource");
363
+ if (oldState.status !== "idle" /* Idle */ && oldState.resource !== newResource) {
364
+ oldState.resource.playStream.on("error", noop);
365
+ oldState.resource.playStream.off("error", oldState.onStreamError);
366
+ oldState.resource.audioPlayer = void 0;
367
+ oldState.resource.playStream.destroy();
368
+ oldState.resource.playStream.read();
369
+ }
370
+ if (oldState.status === "buffering" /* Buffering */ && (newState.status !== "buffering" /* Buffering */ || newState.resource !== oldState.resource)) {
371
+ oldState.resource.playStream.off("end", oldState.onFailureCallback);
372
+ oldState.resource.playStream.off("close", oldState.onFailureCallback);
373
+ oldState.resource.playStream.off("finish", oldState.onFailureCallback);
374
+ oldState.resource.playStream.off("readable", oldState.onReadableCallback);
375
+ }
376
+ if (newState.status === "idle" /* Idle */) {
377
+ this._signalStopSpeaking();
378
+ deleteAudioPlayer(this);
379
+ }
380
+ if (newResource) {
381
+ addAudioPlayer(this);
382
+ }
383
+ const didChangeResources = oldState.status !== "idle" /* Idle */ && newState.status === "playing" /* Playing */ && oldState.resource !== newState.resource;
384
+ this._state = newState;
385
+ this.emit("stateChange", oldState, this._state);
386
+ if (oldState.status !== newState.status || didChangeResources) {
387
+ this.emit(newState.status, oldState, this._state);
388
+ }
389
+ this.debug?.(`state change:
390
+ from ${stringifyState(oldState)}
391
+ to ${stringifyState(newState)}`);
392
+ }
393
+ /**
394
+ * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed
395
+ * (it cannot be reused, even in another player) and is replaced with the new resource.
396
+ *
397
+ * @remarks
398
+ * The player will transition to the Playing state once playback begins, and will return to the Idle state once
399
+ * playback is ended.
400
+ *
401
+ * If the player was previously playing a resource and this method is called, the player will not transition to the
402
+ * Idle state during the swap over.
403
+ * @param resource - The resource to play
404
+ * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player
405
+ */
406
+ play(resource) {
407
+ if (resource.ended) {
408
+ throw new Error("Cannot play a resource that has already ended.");
409
+ }
410
+ if (resource.audioPlayer) {
411
+ if (resource.audioPlayer === this) {
412
+ return;
413
+ }
414
+ throw new Error("Resource is already being played by another audio player.");
415
+ }
416
+ resource.audioPlayer = this;
417
+ const onStreamError = /* @__PURE__ */ __name((error) => {
418
+ if (this.state.status !== "idle" /* Idle */) {
419
+ this.emit("error", new AudioPlayerError(error, this.state.resource));
420
+ }
421
+ if (this.state.status !== "idle" /* Idle */ && this.state.resource === resource) {
422
+ this.state = {
423
+ status: "idle" /* Idle */
424
+ };
425
+ }
426
+ }, "onStreamError");
427
+ resource.playStream.once("error", onStreamError);
428
+ if (resource.started) {
429
+ this.state = {
430
+ status: "playing" /* Playing */,
431
+ missedFrames: 0,
432
+ playbackDuration: 0,
433
+ resource,
434
+ onStreamError
435
+ };
436
+ } else {
437
+ const onReadableCallback = /* @__PURE__ */ __name(() => {
438
+ if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) {
439
+ this.state = {
440
+ status: "playing" /* Playing */,
441
+ missedFrames: 0,
442
+ playbackDuration: 0,
443
+ resource,
444
+ onStreamError
445
+ };
446
+ }
447
+ }, "onReadableCallback");
448
+ const onFailureCallback = /* @__PURE__ */ __name(() => {
449
+ if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) {
450
+ this.state = {
451
+ status: "idle" /* Idle */
452
+ };
453
+ }
454
+ }, "onFailureCallback");
455
+ resource.playStream.once("readable", onReadableCallback);
456
+ resource.playStream.once("end", onFailureCallback);
457
+ resource.playStream.once("close", onFailureCallback);
458
+ resource.playStream.once("finish", onFailureCallback);
459
+ this.state = {
460
+ status: "buffering" /* Buffering */,
461
+ resource,
462
+ onReadableCallback,
463
+ onFailureCallback,
464
+ onStreamError
465
+ };
466
+ }
467
+ }
468
+ /**
469
+ * Pauses playback of the current resource, if any.
470
+ *
471
+ * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches
472
+ * @returns `true` if the player was successfully paused, otherwise `false`
473
+ */
474
+ pause(interpolateSilence = true) {
475
+ if (this.state.status !== "playing" /* Playing */) return false;
476
+ this.state = {
477
+ ...this.state,
478
+ status: "paused" /* Paused */,
479
+ silencePacketsRemaining: interpolateSilence ? 5 : 0
480
+ };
481
+ return true;
482
+ }
483
+ /**
484
+ * Unpauses playback of the current resource, if any.
485
+ *
486
+ * @returns `true` if the player was successfully unpaused, otherwise `false`
487
+ */
488
+ unpause() {
489
+ if (this.state.status !== "paused" /* Paused */) return false;
490
+ this.state = {
491
+ ...this.state,
492
+ status: "playing" /* Playing */,
493
+ missedFrames: 0
494
+ };
495
+ return true;
496
+ }
497
+ /**
498
+ * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state,
499
+ * or remain in its current state until the silence padding frames of the resource have been played.
500
+ *
501
+ * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames
502
+ * @returns `true` if the player will come to a stop, otherwise `false`
503
+ */
504
+ stop(force = false) {
505
+ if (this.state.status === "idle" /* Idle */) return false;
506
+ if (force || this.state.resource.silencePaddingFrames === 0) {
507
+ this.state = {
508
+ status: "idle" /* Idle */
509
+ };
510
+ } else if (this.state.resource.silenceRemaining === -1) {
511
+ this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;
512
+ }
513
+ return true;
514
+ }
515
+ /**
516
+ * Checks whether the underlying resource (if any) is playable (readable)
517
+ *
518
+ * @returns `true` if the resource is playable, otherwise `false`
519
+ */
520
+ checkPlayable() {
521
+ const state = this._state;
522
+ if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return false;
523
+ if (!state.resource.readable) {
524
+ this.state = {
525
+ status: "idle" /* Idle */
526
+ };
527
+ return false;
528
+ }
529
+ return true;
530
+ }
531
+ /**
532
+ * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered
533
+ * by the active connections of this audio player.
534
+ */
535
+ // @ts-ignore
536
+ _stepDispatch() {
537
+ const state = this._state;
538
+ if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return;
539
+ for (const connection of this.playable) {
540
+ connection.dispatchAudio();
541
+ }
542
+ }
543
+ /**
544
+ * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the
545
+ * underlying resource of the stream, and then has all the active connections of the audio player prepare it
546
+ * (encrypt it, append header data) so that it is ready to play at the start of the next cycle.
547
+ */
548
+ // @ts-ignore
549
+ _stepPrepare() {
550
+ const state = this._state;
551
+ if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return;
552
+ const playable = this.playable;
553
+ if (state.status === "autopaused" /* AutoPaused */ && playable.length > 0) {
554
+ this.state = {
555
+ ...state,
556
+ status: "playing" /* Playing */,
557
+ missedFrames: 0
558
+ };
559
+ }
560
+ if (state.status === "paused" /* Paused */ || state.status === "autopaused" /* AutoPaused */) {
561
+ if (state.silencePacketsRemaining > 0) {
562
+ state.silencePacketsRemaining--;
563
+ this._preparePacket(SILENCE_FRAME, playable, state);
564
+ if (state.silencePacketsRemaining === 0) {
565
+ this._signalStopSpeaking();
566
+ }
567
+ }
568
+ return;
569
+ }
570
+ if (playable.length === 0) {
571
+ if (this.behaviors.noSubscriber === "pause" /* Pause */) {
572
+ this.state = {
573
+ ...state,
574
+ status: "autopaused" /* AutoPaused */,
575
+ silencePacketsRemaining: 5
576
+ };
577
+ return;
578
+ } else if (this.behaviors.noSubscriber === "stop" /* Stop */) {
579
+ this.stop(true);
580
+ }
581
+ }
582
+ const packet = state.resource.read();
583
+ if (state.status === "playing" /* Playing */) {
584
+ if (packet) {
585
+ this._preparePacket(packet, playable, state);
586
+ state.missedFrames = 0;
587
+ } else {
588
+ this._preparePacket(SILENCE_FRAME, playable, state);
589
+ state.missedFrames++;
590
+ if (state.missedFrames >= this.behaviors.maxMissedFrames) {
591
+ this.stop();
592
+ }
593
+ }
594
+ }
595
+ }
596
+ /**
597
+ * Signals to all the subscribed connections that they should send a packet to Discord indicating
598
+ * they are no longer speaking. Called once playback of a resource ends.
599
+ */
600
+ _signalStopSpeaking() {
601
+ for (const { connection } of this.subscribers) {
602
+ connection.setSpeaking(false);
603
+ }
604
+ }
605
+ /**
606
+ * Instructs the given connections to each prepare this packet to be played at the start of the
607
+ * next cycle.
608
+ *
609
+ * @param packet - The Opus packet to be prepared by each receiver
610
+ * @param receivers - The connections that should play this packet
611
+ */
612
+ _preparePacket(packet, receivers, state) {
613
+ state.playbackDuration += 20;
614
+ for (const connection of receivers) {
615
+ connection.prepareAudioPacket(packet);
616
+ }
617
+ }
618
+ };
619
+ function createAudioPlayer(options) {
620
+ return new AudioPlayer(options);
621
+ }
622
+ __name(createAudioPlayer, "createAudioPlayer");
623
+
624
+ // src/networking/DAVESession.ts
625
+ var Davey = null;
626
+ var TRANSITION_EXPIRY = 10;
627
+ var TRANSITION_EXPIRY_PENDING_DOWNGRADE = 24;
628
+ var DEFAULT_DECRYPTION_FAILURE_TOLERANCE = 36;
629
+ var daveLoadPromise = new Promise(async (resolve2) => {
630
+ try {
631
+ const lib = await import("@snazzah/davey");
632
+ Davey = lib;
633
+ } catch {
634
+ }
635
+ resolve2();
636
+ });
637
+ function getMaxProtocolVersion() {
638
+ return Davey?.DAVE_PROTOCOL_VERSION;
639
+ }
640
+ __name(getMaxProtocolVersion, "getMaxProtocolVersion");
641
+ var DAVESession = class extends EventEmitter2 {
642
+ static {
643
+ __name(this, "DAVESession");
644
+ }
645
+ /**
646
+ * The channel id represented by this session.
647
+ */
648
+ channelId;
649
+ /**
650
+ * The user id represented by this session.
651
+ */
652
+ userId;
653
+ /**
654
+ * The protocol version being used.
655
+ */
656
+ protocolVersion;
657
+ /**
658
+ * The last transition id executed.
659
+ */
660
+ lastTransitionId;
661
+ /**
662
+ * The pending transition.
663
+ */
664
+ pendingTransition;
665
+ /**
666
+ * Whether this session was downgraded previously.
667
+ */
668
+ downgraded = false;
669
+ /**
670
+ * The amount of consecutive failures encountered when decrypting.
671
+ */
672
+ consecutiveFailures = 0;
673
+ /**
674
+ * The amount of consecutive failures needed to attempt to recover.
675
+ */
676
+ failureTolerance;
677
+ /**
678
+ * Whether this session is currently re-initializing due to an invalid transition.
679
+ */
680
+ reinitializing = false;
681
+ /**
682
+ * The underlying DAVE Session of this wrapper.
683
+ */
684
+ session;
685
+ constructor(protocolVersion, userId, channelId, options) {
686
+ if (Davey === null)
687
+ throw new Error(
688
+ `Cannot utilize the DAVE protocol as the @snazzah/davey package has not been installed.
689
+ - Use the generateDependencyReport() function for more information.
690
+ `
691
+ );
692
+ super();
693
+ this.protocolVersion = protocolVersion;
694
+ this.userId = userId;
695
+ this.channelId = channelId;
696
+ this.failureTolerance = options.decryptionFailureTolerance ?? DEFAULT_DECRYPTION_FAILURE_TOLERANCE;
697
+ }
698
+ /**
699
+ * The current voice privacy code of the session. Will be `null` if there is no session.
700
+ */
701
+ get voicePrivacyCode() {
702
+ if (this.protocolVersion === 0 || !this.session?.voicePrivacyCode) {
703
+ return null;
704
+ }
705
+ return this.session.voicePrivacyCode;
706
+ }
707
+ /**
708
+ * Gets the verification code for a user in the session.
709
+ *
710
+ * @throws Will throw if there is not an active session or the user id provided is invalid or not in the session.
711
+ */
712
+ async getVerificationCode(userId) {
713
+ if (!this.session) throw new Error("Session not available");
714
+ return this.session.getVerificationCode(userId);
715
+ }
716
+ /**
717
+ * Re-initializes (or initializes) the underlying session.
718
+ */
719
+ reinit() {
720
+ if (this.protocolVersion > 0) {
721
+ if (this.session) {
722
+ this.session.reinit(this.protocolVersion, this.userId, this.channelId);
723
+ this.emit("debug", `Session reinitialized for protocol version ${this.protocolVersion}`);
724
+ } else {
725
+ this.session = new Davey.DAVESession(this.protocolVersion, this.userId, this.channelId);
726
+ this.emit("debug", `Session initialized for protocol version ${this.protocolVersion}`);
727
+ }
728
+ this.emit("keyPackage", this.session.getSerializedKeyPackage());
729
+ } else if (this.session) {
730
+ this.session.reset();
731
+ this.session.setPassthroughMode(true, TRANSITION_EXPIRY);
732
+ this.emit("debug", "Session reset");
733
+ }
734
+ }
735
+ /**
736
+ * Set the external sender for this session.
737
+ *
738
+ * @param externalSender - The external sender
739
+ */
740
+ setExternalSender(externalSender) {
741
+ if (!this.session) throw new Error("No session available");
742
+ this.session.setExternalSender(externalSender);
743
+ this.emit("debug", "Set MLS external sender");
744
+ }
745
+ /**
746
+ * Prepare for a transition.
747
+ *
748
+ * @param data - The transition data
749
+ * @returns Whether we should signal to the voice server that we are ready
750
+ */
751
+ prepareTransition(data) {
752
+ this.emit("debug", `Preparing for transition (${data.transition_id}, v${data.protocol_version})`);
753
+ this.pendingTransition = data;
754
+ if (data.transition_id === 0) {
755
+ this.executeTransition(data.transition_id);
756
+ } else {
757
+ if (data.protocol_version === 0) this.session?.setPassthroughMode(true, TRANSITION_EXPIRY_PENDING_DOWNGRADE);
758
+ return true;
759
+ }
760
+ return false;
761
+ }
762
+ /**
763
+ * Execute a transition.
764
+ *
765
+ * @param transitionId - The transition id to execute on
766
+ */
767
+ executeTransition(transitionId) {
768
+ this.emit("debug", `Executing transition (${transitionId})`);
769
+ if (!this.pendingTransition) {
770
+ this.emit("debug", `Received execute transition, but we don't have a pending transition for ${transitionId}`);
413
771
  return;
414
772
  }
415
- if (packet.op === VoiceOpcodes.HeartbeatAck) {
416
- this.lastHeartbeatAck = Date.now();
417
- this.missedHeartbeats = 0;
418
- this.ping = this.lastHeartbeatAck - this.lastHeartbeatSend;
773
+ let transitioned = false;
774
+ if (transitionId === this.pendingTransition.transition_id) {
775
+ const oldVersion = this.protocolVersion;
776
+ this.protocolVersion = this.pendingTransition.protocol_version;
777
+ if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
778
+ this.downgraded = true;
779
+ this.emit("debug", "Session downgraded");
780
+ } else if (transitionId > 0 && this.downgraded) {
781
+ this.downgraded = false;
782
+ this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
783
+ this.emit("debug", "Session upgraded");
784
+ }
785
+ transitioned = true;
786
+ this.reinitializing = false;
787
+ this.lastTransitionId = transitionId;
788
+ this.emit("debug", `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
789
+ } else {
790
+ this.emit(
791
+ "debug",
792
+ `Received execute transition for an unexpected transition id (expected: ${this.pendingTransition.transition_id}, actual: ${transitionId})`
793
+ );
419
794
  }
420
- this.emit("packet", packet);
795
+ this.pendingTransition = void 0;
796
+ return transitioned;
421
797
  }
422
798
  /**
423
- * Sends a JSON-stringifiable packet over the WebSocket.
799
+ * Prepare for a new epoch.
424
800
  *
425
- * @param packet - The packet to send
801
+ * @param data - The epoch data
426
802
  */
427
- sendPacket(packet) {
803
+ prepareEpoch(data) {
804
+ this.emit("debug", `Preparing for epoch (${data.epoch})`);
805
+ if (data.epoch === 1) {
806
+ this.protocolVersion = data.protocol_version;
807
+ this.reinit();
808
+ }
809
+ }
810
+ /**
811
+ * Recover from an invalid transition by re-initializing.
812
+ *
813
+ * @param transitionId - The transition id to invalidate
814
+ */
815
+ recoverFromInvalidTransition(transitionId) {
816
+ if (this.reinitializing) return;
817
+ this.emit("debug", `Invalidating transition ${transitionId}`);
818
+ this.reinitializing = true;
819
+ this.consecutiveFailures = 0;
820
+ this.emit("invalidateTransition", transitionId);
821
+ this.reinit();
822
+ }
823
+ /**
824
+ * Processes proposals from the MLS group.
825
+ *
826
+ * @param payload - The binary message payload
827
+ * @param connectedClients - The set of connected client IDs
828
+ * @returns The payload to send back to the voice server, if there is one
829
+ */
830
+ processProposals(payload, connectedClients) {
831
+ if (!this.session) throw new Error("No session available");
832
+ const optype = payload.readUInt8(0);
833
+ const { commit, welcome } = this.session.processProposals(
834
+ optype,
835
+ payload.subarray(1),
836
+ Array.from(connectedClients)
837
+ );
838
+ this.emit("debug", "MLS proposals processed");
839
+ if (!commit) return;
840
+ return welcome ? Buffer4.concat([commit, welcome]) : commit;
841
+ }
842
+ /**
843
+ * Processes a commit from the MLS group.
844
+ *
845
+ * @param payload - The payload
846
+ * @returns The transaction id and whether it was successful
847
+ */
848
+ processCommit(payload) {
849
+ if (!this.session) throw new Error("No session available");
850
+ const transitionId = payload.readUInt16BE(0);
428
851
  try {
429
- const stringified = JSON.stringify(packet);
430
- this.debug?.(`>> ${stringified}`);
431
- this.ws.send(stringified);
852
+ this.session.processCommit(payload.subarray(2));
853
+ if (transitionId === 0) {
854
+ this.reinitializing = false;
855
+ this.lastTransitionId = transitionId;
856
+ } else {
857
+ this.pendingTransition = { transition_id: transitionId, protocol_version: this.protocolVersion };
858
+ }
859
+ this.emit("debug", `MLS commit processed (transition id: ${transitionId})`);
860
+ return { transitionId, success: true };
432
861
  } catch (error) {
433
- const err = error;
434
- this.emit("error", err);
862
+ this.emit("debug", `MLS commit errored from transition ${transitionId}: ${error}`);
863
+ this.recoverFromInvalidTransition(transitionId);
864
+ return { transitionId, success: false };
435
865
  }
436
866
  }
437
867
  /**
438
- * Sends a heartbeat over the WebSocket.
868
+ * Processes a welcome from the MLS group.
869
+ *
870
+ * @param payload - The payload
871
+ * @returns The transaction id and whether it was successful
439
872
  */
440
- sendHeartbeat() {
441
- this.lastHeartbeatSend = Date.now();
442
- this.missedHeartbeats++;
443
- const nonce2 = this.lastHeartbeatSend;
444
- this.sendPacket({
445
- op: VoiceOpcodes.Heartbeat,
446
- // eslint-disable-next-line id-length
447
- d: nonce2
448
- });
873
+ processWelcome(payload) {
874
+ if (!this.session) throw new Error("No session available");
875
+ const transitionId = payload.readUInt16BE(0);
876
+ try {
877
+ this.session.processWelcome(payload.subarray(2));
878
+ if (transitionId === 0) {
879
+ this.reinitializing = false;
880
+ this.lastTransitionId = transitionId;
881
+ } else {
882
+ this.pendingTransition = { transition_id: transitionId, protocol_version: this.protocolVersion };
883
+ }
884
+ this.emit("debug", `MLS welcome processed (transition id: ${transitionId})`);
885
+ return { transitionId, success: true };
886
+ } catch (error) {
887
+ this.emit("debug", `MLS welcome errored from transition ${transitionId}: ${error}`);
888
+ this.recoverFromInvalidTransition(transitionId);
889
+ return { transitionId, success: false };
890
+ }
449
891
  }
450
892
  /**
451
- * Sets/clears an interval to send heartbeats over the WebSocket.
893
+ * Encrypt a packet using end-to-end encryption.
452
894
  *
453
- * @param ms - The interval in milliseconds. If negative, the interval will be unset
895
+ * @param packet - The packet to encrypt
454
896
  */
455
- setHeartbeatInterval(ms) {
456
- if (this.heartbeatInterval !== void 0) clearInterval(this.heartbeatInterval);
457
- if (ms > 0) {
458
- this.heartbeatInterval = setInterval(() => {
459
- if (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) {
460
- this.ws.close();
461
- this.setHeartbeatInterval(-1);
897
+ encrypt(packet) {
898
+ if (this.protocolVersion === 0 || !this.session?.ready || packet.equals(SILENCE_FRAME)) return packet;
899
+ return this.session.encryptOpus(packet);
900
+ }
901
+ /**
902
+ * Decrypt a packet using end-to-end encryption.
903
+ *
904
+ * @param packet - The packet to decrypt
905
+ * @param userId - The user id that sent the packet
906
+ * @returns The decrypted packet, or `null` if the decryption failed but should be ignored
907
+ */
908
+ decrypt(packet, userId) {
909
+ const canDecrypt = this.session?.ready && (this.protocolVersion !== 0 || this.session?.canPassthrough(userId));
910
+ if (packet.equals(SILENCE_FRAME) || !canDecrypt || !this.session) return packet;
911
+ try {
912
+ const buffer = this.session.decrypt(userId, Davey.MediaType.AUDIO, packet);
913
+ this.consecutiveFailures = 0;
914
+ return buffer;
915
+ } catch (error) {
916
+ if (!this.reinitializing && !this.pendingTransition) {
917
+ this.consecutiveFailures++;
918
+ this.emit("debug", `Failed to decrypt a packet (${this.consecutiveFailures} consecutive fails)`);
919
+ if (this.consecutiveFailures > this.failureTolerance) {
920
+ if (this.lastTransitionId) this.recoverFromInvalidTransition(this.lastTransitionId);
921
+ else throw error;
462
922
  }
463
- this.sendHeartbeat();
464
- }, ms);
923
+ } else if (this.reinitializing) {
924
+ this.emit("debug", "Failed to decrypt a packet (reinitializing session)");
925
+ } else if (this.pendingTransition) {
926
+ this.emit(
927
+ "debug",
928
+ `Failed to decrypt a packet (pending transition ${this.pendingTransition.transition_id} to v${this.pendingTransition.protocol_version})`
929
+ );
930
+ }
931
+ }
932
+ return null;
933
+ }
934
+ /**
935
+ * Resets the session.
936
+ */
937
+ destroy() {
938
+ try {
939
+ this.session?.reset();
940
+ } catch {
465
941
  }
466
942
  }
467
943
  };
468
944
 
469
- // src/networking/Networking.ts
470
- var CHANNELS = 2;
471
- var TIMESTAMP_INC = 48e3 / 100 * CHANNELS;
472
- var MAX_NONCE_SIZE = 2 ** 32 - 1;
473
- var SUPPORTED_ENCRYPTION_MODES = ["aead_xchacha20_poly1305_rtpsize"];
474
- if (crypto.getCiphers().includes("aes-256-gcm")) {
475
- SUPPORTED_ENCRYPTION_MODES.unshift("aead_aes256_gcm_rtpsize");
476
- }
477
- var nonce = Buffer4.alloc(24);
478
- function stringifyState(state) {
479
- return JSON.stringify({
480
- ...state,
481
- ws: Reflect.has(state, "ws"),
482
- udp: Reflect.has(state, "udp")
483
- });
484
- }
485
- __name(stringifyState, "stringifyState");
486
- function chooseEncryptionMode(options) {
487
- const option = options.find((option2) => SUPPORTED_ENCRYPTION_MODES.includes(option2));
488
- if (!option) {
489
- throw new Error(`No compatible encryption modes. Available include: ${options.join(", ")}`);
945
+ // src/networking/VoiceUDPSocket.ts
946
+ import { Buffer as Buffer5 } from "buffer";
947
+ import { createSocket } from "dgram";
948
+ import { EventEmitter as EventEmitter3 } from "events";
949
+ import { isIPv4 } from "net";
950
+ function parseLocalPacket(message) {
951
+ const packet = Buffer5.from(message);
952
+ const ip = packet.slice(8, packet.indexOf(0, 8)).toString("utf8");
953
+ if (!isIPv4(ip)) {
954
+ throw new Error("Malformed IP address");
490
955
  }
491
- return option;
492
- }
493
- __name(chooseEncryptionMode, "chooseEncryptionMode");
494
- function randomNBit(numberOfBits) {
495
- return Math.floor(Math.random() * 2 ** numberOfBits);
956
+ const port = packet.readUInt16BE(packet.length - 2);
957
+ return { ip, port };
496
958
  }
497
- __name(randomNBit, "randomNBit");
498
- var Networking = class extends EventEmitter3 {
959
+ __name(parseLocalPacket, "parseLocalPacket");
960
+ var KEEP_ALIVE_INTERVAL = 5e3;
961
+ var MAX_COUNTER_VALUE = 2 ** 32 - 1;
962
+ var VoiceUDPSocket = class extends EventEmitter3 {
499
963
  static {
500
- __name(this, "Networking");
964
+ __name(this, "VoiceUDPSocket");
501
965
  }
502
- _state;
503
966
  /**
504
- * The debug logger function, if debugging is enabled.
967
+ * The underlying network Socket for the VoiceUDPSocket.
505
968
  */
506
- debug;
969
+ socket;
507
970
  /**
508
- * Creates a new Networking instance.
971
+ * The socket details for Discord (remote)
509
972
  */
510
- constructor(options, debug) {
511
- super();
512
- this.onWsOpen = this.onWsOpen.bind(this);
513
- this.onChildError = this.onChildError.bind(this);
514
- this.onWsPacket = this.onWsPacket.bind(this);
515
- this.onWsClose = this.onWsClose.bind(this);
516
- this.onWsDebug = this.onWsDebug.bind(this);
517
- this.onUdpDebug = this.onUdpDebug.bind(this);
518
- this.onUdpClose = this.onUdpClose.bind(this);
519
- this.debug = debug ? (message) => this.emit("debug", message) : null;
520
- this._state = {
521
- code: 0 /* OpeningWs */,
522
- ws: this.createWebSocket(options.endpoint),
523
- connectionOptions: options
524
- };
525
- }
973
+ remote;
526
974
  /**
527
- * Destroys the Networking instance, transitioning it into the Closed state.
975
+ * The counter used in the keep alive mechanism.
528
976
  */
529
- destroy() {
530
- this.state = {
531
- code: 6 /* Closed */
532
- };
533
- }
977
+ keepAliveCounter = 0;
534
978
  /**
535
- * The current state of the networking instance.
979
+ * The buffer used to write the keep alive counter into.
536
980
  */
537
- get state() {
538
- return this._state;
539
- }
981
+ keepAliveBuffer;
540
982
  /**
541
- * Sets a new state for the networking instance, performing clean-up operations where necessary.
983
+ * The Node.js interval for the keep-alive mechanism.
542
984
  */
543
- set state(newState) {
544
- const oldWs = Reflect.get(this._state, "ws");
545
- const newWs = Reflect.get(newState, "ws");
546
- if (oldWs && oldWs !== newWs) {
547
- oldWs.off("debug", this.onWsDebug);
548
- oldWs.on("error", noop);
549
- oldWs.off("error", this.onChildError);
550
- oldWs.off("open", this.onWsOpen);
551
- oldWs.off("packet", this.onWsPacket);
552
- oldWs.off("close", this.onWsClose);
553
- oldWs.destroy();
554
- }
555
- const oldUdp = Reflect.get(this._state, "udp");
556
- const newUdp = Reflect.get(newState, "udp");
557
- if (oldUdp && oldUdp !== newUdp) {
558
- oldUdp.on("error", noop);
559
- oldUdp.off("error", this.onChildError);
560
- oldUdp.off("close", this.onUdpClose);
561
- oldUdp.off("debug", this.onUdpDebug);
562
- oldUdp.destroy();
563
- }
564
- const oldState = this._state;
565
- this._state = newState;
566
- this.emit("stateChange", oldState, newState);
567
- this.debug?.(`state change:
568
- from ${stringifyState(oldState)}
569
- to ${stringifyState(newState)}`);
570
- }
985
+ keepAliveInterval;
571
986
  /**
572
- * Creates a new WebSocket to a Discord Voice gateway.
987
+ * The time taken to receive a response to keep alive messages.
573
988
  *
574
- * @param endpoint - The endpoint to connect to
989
+ * @deprecated This field is no longer updated as keep alive messages are no longer tracked.
575
990
  */
576
- createWebSocket(endpoint) {
577
- const ws = new VoiceWebSocket(`wss://${endpoint}?v=4`, Boolean(this.debug));
578
- ws.on("error", this.onChildError);
579
- ws.once("open", this.onWsOpen);
580
- ws.on("packet", this.onWsPacket);
581
- ws.once("close", this.onWsClose);
582
- ws.on("debug", this.onWsDebug);
583
- return ws;
584
- }
991
+ ping;
585
992
  /**
586
- * Propagates errors from the children VoiceWebSocket and VoiceUDPSocket.
993
+ * Creates a new VoiceUDPSocket.
587
994
  *
588
- * @param error - The error that was emitted by a child
589
- */
590
- onChildError(error) {
591
- this.emit("error", error);
592
- }
593
- /**
594
- * Called when the WebSocket opens. Depending on the state that the instance is in,
595
- * it will either identify with a new session, or it will attempt to resume an existing session.
995
+ * @param remote - Details of the remote socket
596
996
  */
597
- onWsOpen() {
598
- if (this.state.code === 0 /* OpeningWs */) {
599
- const packet = {
600
- op: VoiceOpcodes2.Identify,
601
- d: {
602
- server_id: this.state.connectionOptions.serverId,
603
- user_id: this.state.connectionOptions.userId,
604
- session_id: this.state.connectionOptions.sessionId,
605
- token: this.state.connectionOptions.token
606
- }
607
- };
608
- this.state.ws.sendPacket(packet);
609
- this.state = {
610
- ...this.state,
611
- code: 1 /* Identifying */
612
- };
613
- } else if (this.state.code === 5 /* Resuming */) {
614
- const packet = {
615
- op: VoiceOpcodes2.Resume,
616
- d: {
617
- server_id: this.state.connectionOptions.serverId,
618
- session_id: this.state.connectionOptions.sessionId,
619
- token: this.state.connectionOptions.token
620
- }
621
- };
622
- this.state.ws.sendPacket(packet);
623
- }
997
+ constructor(remote) {
998
+ super();
999
+ this.socket = createSocket("udp4");
1000
+ this.socket.on("error", (error) => this.emit("error", error));
1001
+ this.socket.on("message", (buffer) => this.onMessage(buffer));
1002
+ this.socket.on("close", () => this.emit("close"));
1003
+ this.remote = remote;
1004
+ this.keepAliveBuffer = Buffer5.alloc(8);
1005
+ this.keepAliveInterval = setInterval(() => this.keepAlive(), KEEP_ALIVE_INTERVAL);
1006
+ setImmediate(() => this.keepAlive());
624
1007
  }
625
1008
  /**
626
- * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter),
627
- * the instance will either attempt to resume, or enter the closed state and emit a 'close' event
628
- * with the close code, allowing the user to decide whether or not they would like to reconnect.
1009
+ * Called when a message is received on the UDP socket.
629
1010
  *
630
- * @param code - The close code
1011
+ * @param buffer - The received buffer
631
1012
  */
632
- onWsClose({ code }) {
633
- const canResume = code === 4015 || code < 4e3;
634
- if (canResume && this.state.code === 4 /* Ready */) {
635
- this.state = {
636
- ...this.state,
637
- code: 5 /* Resuming */,
638
- ws: this.createWebSocket(this.state.connectionOptions.endpoint)
639
- };
640
- } else if (this.state.code !== 6 /* Closed */) {
641
- this.destroy();
642
- this.emit("close", code);
643
- }
1013
+ onMessage(buffer) {
1014
+ this.emit("message", buffer);
644
1015
  }
645
1016
  /**
646
- * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord.
1017
+ * Called at a regular interval to check whether we are still able to send datagrams to Discord.
647
1018
  */
648
- onUdpClose() {
649
- if (this.state.code === 4 /* Ready */) {
650
- this.state = {
651
- ...this.state,
652
- code: 5 /* Resuming */,
653
- ws: this.createWebSocket(this.state.connectionOptions.endpoint)
654
- };
1019
+ keepAlive() {
1020
+ this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0);
1021
+ this.send(this.keepAliveBuffer);
1022
+ this.keepAliveCounter++;
1023
+ if (this.keepAliveCounter > MAX_COUNTER_VALUE) {
1024
+ this.keepAliveCounter = 0;
655
1025
  }
656
1026
  }
657
1027
  /**
658
- * Called when a packet is received on the connection's WebSocket.
1028
+ * Sends a buffer to Discord.
659
1029
  *
660
- * @param packet - The received packet
1030
+ * @param buffer - The buffer to send
661
1031
  */
662
- onWsPacket(packet) {
663
- if (packet.op === VoiceOpcodes2.Hello && this.state.code !== 6 /* Closed */) {
664
- this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval);
665
- } else if (packet.op === VoiceOpcodes2.Ready && this.state.code === 1 /* Identifying */) {
666
- const { ip, port, ssrc, modes } = packet.d;
667
- const udp = new VoiceUDPSocket({ ip, port });
668
- udp.on("error", this.onChildError);
669
- udp.on("debug", this.onUdpDebug);
670
- udp.once("close", this.onUdpClose);
671
- udp.performIPDiscovery(ssrc).then((localConfig) => {
672
- if (this.state.code !== 2 /* UdpHandshaking */) return;
673
- this.state.ws.sendPacket({
674
- op: VoiceOpcodes2.SelectProtocol,
675
- d: {
676
- protocol: "udp",
677
- data: {
678
- address: localConfig.ip,
679
- port: localConfig.port,
680
- mode: chooseEncryptionMode(modes)
681
- }
682
- }
683
- });
684
- this.state = {
685
- ...this.state,
686
- code: 3 /* SelectingProtocol */
687
- };
688
- }).catch((error) => this.emit("error", error));
689
- this.state = {
690
- ...this.state,
691
- code: 2 /* UdpHandshaking */,
692
- udp,
693
- connectionData: {
694
- ssrc
695
- }
696
- };
697
- } else if (packet.op === VoiceOpcodes2.SessionDescription && this.state.code === 3 /* SelectingProtocol */) {
698
- const { mode: encryptionMode, secret_key: secretKey } = packet.d;
699
- this.state = {
700
- ...this.state,
701
- code: 4 /* Ready */,
702
- connectionData: {
703
- ...this.state.connectionData,
704
- encryptionMode,
705
- secretKey: new Uint8Array(secretKey),
706
- sequence: randomNBit(16),
707
- timestamp: randomNBit(32),
708
- nonce: 0,
709
- nonceBuffer: encryptionMode === "aead_aes256_gcm_rtpsize" ? Buffer4.alloc(12) : Buffer4.alloc(24),
710
- speaking: false,
711
- packetsPlayed: 0
712
- }
713
- };
714
- } else if (packet.op === VoiceOpcodes2.Resumed && this.state.code === 5 /* Resuming */) {
715
- this.state = {
716
- ...this.state,
717
- code: 4 /* Ready */
718
- };
719
- this.state.connectionData.speaking = false;
720
- }
1032
+ send(buffer) {
1033
+ this.socket.send(buffer, this.remote.port, this.remote.ip);
721
1034
  }
722
1035
  /**
723
- * Propagates debug messages from the child WebSocket.
724
- *
725
- * @param message - The emitted debug message
1036
+ * Closes the socket, the instance will not be able to be reused.
726
1037
  */
727
- onWsDebug(message) {
728
- this.debug?.(`[WS] ${message}`);
1038
+ destroy() {
1039
+ try {
1040
+ this.socket.close();
1041
+ } catch {
1042
+ }
1043
+ clearInterval(this.keepAliveInterval);
729
1044
  }
730
1045
  /**
731
- * Propagates debug messages from the child UDPSocket.
1046
+ * Performs IP discovery to discover the local address and port to be used for the voice connection.
732
1047
  *
733
- * @param message - The emitted debug message
1048
+ * @param ssrc - The SSRC received from Discord
734
1049
  */
735
- onUdpDebug(message) {
736
- this.debug?.(`[UDP] ${message}`);
1050
+ async performIPDiscovery(ssrc) {
1051
+ return new Promise((resolve2, reject) => {
1052
+ const listener = /* @__PURE__ */ __name((message) => {
1053
+ try {
1054
+ if (message.readUInt16BE(0) !== 2) return;
1055
+ const packet = parseLocalPacket(message);
1056
+ this.socket.off("message", listener);
1057
+ resolve2(packet);
1058
+ } catch {
1059
+ }
1060
+ }, "listener");
1061
+ this.socket.on("message", listener);
1062
+ this.socket.once("close", () => reject(new Error("Cannot perform IP discovery - socket closed")));
1063
+ const discoveryBuffer = Buffer5.alloc(74);
1064
+ discoveryBuffer.writeUInt16BE(1, 0);
1065
+ discoveryBuffer.writeUInt16BE(70, 2);
1066
+ discoveryBuffer.writeUInt32BE(ssrc, 4);
1067
+ this.send(discoveryBuffer);
1068
+ });
1069
+ }
1070
+ };
1071
+
1072
+ // src/networking/VoiceWebSocket.ts
1073
+ import { Buffer as Buffer6 } from "buffer";
1074
+ import { EventEmitter as EventEmitter4 } from "events";
1075
+ import { VoiceOpcodes } from "discord-api-types/voice/v8";
1076
+ import WebSocket from "ws";
1077
+ var VoiceWebSocket = class extends EventEmitter4 {
1078
+ static {
1079
+ __name(this, "VoiceWebSocket");
737
1080
  }
738
1081
  /**
739
- * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it.
740
- * It will be stored within the instance, and can be played by dispatchAudio()
741
- *
742
- * @remarks
743
- * Calling this method while there is already a prepared audio packet that has not yet been dispatched
744
- * will overwrite the existing audio packet. This should be avoided.
745
- * @param opusPacket - The Opus packet to encrypt
746
- * @returns The audio packet that was prepared
1082
+ * The current heartbeat interval, if any.
747
1083
  */
748
- prepareAudioPacket(opusPacket) {
749
- const state = this.state;
750
- if (state.code !== 4 /* Ready */) return;
751
- state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData);
752
- return state.preparedPacket;
753
- }
1084
+ heartbeatInterval;
754
1085
  /**
755
- * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet
756
- * is consumed and cannot be dispatched again.
1086
+ * The time (milliseconds since UNIX epoch) that the last heartbeat acknowledgement packet was received.
1087
+ * This is set to 0 if an acknowledgement packet hasn't been received yet.
757
1088
  */
758
- dispatchAudio() {
759
- const state = this.state;
760
- if (state.code !== 4 /* Ready */) return false;
761
- if (state.preparedPacket !== void 0) {
762
- this.playAudioPacket(state.preparedPacket);
763
- state.preparedPacket = void 0;
764
- return true;
765
- }
766
- return false;
767
- }
1089
+ lastHeartbeatAck;
768
1090
  /**
769
- * Plays an audio packet, updating timing metadata used for playback.
770
- *
771
- * @param audioPacket - The audio packet to play
1091
+ * The time (milliseconds since UNIX epoch) that the last heartbeat was sent. This is set to 0 if a heartbeat
1092
+ * hasn't been sent yet.
772
1093
  */
773
- playAudioPacket(audioPacket) {
774
- const state = this.state;
775
- if (state.code !== 4 /* Ready */) return;
776
- const { connectionData } = state;
777
- connectionData.packetsPlayed++;
778
- connectionData.sequence++;
779
- connectionData.timestamp += TIMESTAMP_INC;
780
- if (connectionData.sequence >= 2 ** 16) connectionData.sequence = 0;
781
- if (connectionData.timestamp >= 2 ** 32) connectionData.timestamp = 0;
782
- this.setSpeaking(true);
783
- state.udp.send(audioPacket);
784
- }
1094
+ lastHeartbeatSend;
785
1095
  /**
786
- * Sends a packet to the voice gateway indicating that the client has start/stopped sending
787
- * audio.
1096
+ * The number of consecutively missed heartbeats.
1097
+ */
1098
+ missedHeartbeats = 0;
1099
+ /**
1100
+ * The last recorded ping.
1101
+ */
1102
+ ping;
1103
+ /**
1104
+ * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received.
1105
+ */
1106
+ sequence = -1;
1107
+ /**
1108
+ * The debug logger function, if debugging is enabled.
1109
+ */
1110
+ debug;
1111
+ /**
1112
+ * The underlying WebSocket of this wrapper.
1113
+ */
1114
+ ws;
1115
+ /**
1116
+ * Creates a new VoiceWebSocket.
788
1117
  *
789
- * @param speaking - Whether or not the client should be shown as speaking
1118
+ * @param address - The address to connect to
790
1119
  */
791
- setSpeaking(speaking) {
792
- const state = this.state;
793
- if (state.code !== 4 /* Ready */) return;
794
- if (state.connectionData.speaking === speaking) return;
795
- state.connectionData.speaking = speaking;
796
- state.ws.sendPacket({
797
- op: VoiceOpcodes2.Speaking,
798
- d: {
799
- speaking: speaking ? 1 : 0,
800
- delay: 0,
801
- ssrc: state.connectionData.ssrc
802
- }
803
- });
1120
+ constructor(address, debug) {
1121
+ super();
1122
+ this.ws = new WebSocket(address);
1123
+ this.ws.onmessage = (err) => this.onMessage(err);
1124
+ this.ws.onopen = (err) => this.emit("open", err);
1125
+ this.ws.onerror = (err) => this.emit("error", err instanceof Error ? err : err.error);
1126
+ this.ws.onclose = (err) => this.emit("close", err);
1127
+ this.lastHeartbeatAck = 0;
1128
+ this.lastHeartbeatSend = 0;
1129
+ this.debug = debug ? (message) => this.emit("debug", message) : null;
804
1130
  }
805
1131
  /**
806
- * Creates a new audio packet from an Opus packet. This involves encrypting the packet,
807
- * then prepending a header that includes metadata.
808
- *
809
- * @param opusPacket - The Opus packet to prepare
810
- * @param connectionData - The current connection data of the instance
1132
+ * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed.
811
1133
  */
812
- createAudioPacket(opusPacket, connectionData) {
813
- const rtpHeader = Buffer4.alloc(12);
814
- rtpHeader[0] = 128;
815
- rtpHeader[1] = 120;
816
- const { sequence, timestamp, ssrc } = connectionData;
817
- rtpHeader.writeUIntBE(sequence, 2, 2);
818
- rtpHeader.writeUIntBE(timestamp, 4, 4);
819
- rtpHeader.writeUIntBE(ssrc, 8, 4);
820
- rtpHeader.copy(nonce, 0, 0, 12);
821
- return Buffer4.concat([rtpHeader, ...this.encryptOpusPacket(opusPacket, connectionData, rtpHeader)]);
1134
+ destroy() {
1135
+ try {
1136
+ this.debug?.("destroyed");
1137
+ this.setHeartbeatInterval(-1);
1138
+ this.ws.close(1e3);
1139
+ } catch (error) {
1140
+ const err = error;
1141
+ this.emit("error", err);
1142
+ }
822
1143
  }
823
1144
  /**
824
- * Encrypts an Opus packet using the format agreed upon by the instance and Discord.
1145
+ * Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them
1146
+ * as packets. Binary messages will be parsed and emitted.
825
1147
  *
826
- * @param opusPacket - The Opus packet to encrypt
827
- * @param connectionData - The current connection data of the instance
1148
+ * @param event - The message event
828
1149
  */
829
- encryptOpusPacket(opusPacket, connectionData, additionalData) {
830
- const { secretKey, encryptionMode } = connectionData;
831
- connectionData.nonce++;
832
- if (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0;
833
- connectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0);
834
- const noncePadding = connectionData.nonceBuffer.subarray(0, 4);
835
- let encrypted;
836
- switch (encryptionMode) {
837
- case "aead_aes256_gcm_rtpsize": {
838
- const cipher = crypto.createCipheriv("aes-256-gcm", secretKey, connectionData.nonceBuffer);
839
- cipher.setAAD(additionalData);
840
- encrypted = Buffer4.concat([cipher.update(opusPacket), cipher.final(), cipher.getAuthTag()]);
841
- return [encrypted, noncePadding];
842
- }
843
- case "aead_xchacha20_poly1305_rtpsize": {
844
- encrypted = methods.crypto_aead_xchacha20poly1305_ietf_encrypt(
845
- opusPacket,
846
- additionalData,
847
- connectionData.nonceBuffer,
848
- secretKey
849
- );
850
- return [encrypted, noncePadding];
851
- }
852
- default: {
853
- throw new RangeError(`Unsupported encryption method: ${encryptionMode}`);
854
- }
1150
+ onMessage(event) {
1151
+ if (event.data instanceof Buffer6 || event.data instanceof ArrayBuffer) {
1152
+ const buffer = event.data instanceof ArrayBuffer ? Buffer6.from(event.data) : event.data;
1153
+ const seq = buffer.readUInt16BE(0);
1154
+ const op = buffer.readUInt8(2);
1155
+ const payload = buffer.subarray(3);
1156
+ this.sequence = seq;
1157
+ this.debug?.(`<< [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes`);
1158
+ this.emit("binary", { op, seq, payload });
1159
+ return;
1160
+ } else if (typeof event.data !== "string") {
1161
+ return;
855
1162
  }
856
- }
857
- };
858
-
859
- // src/receive/VoiceReceiver.ts
860
- import { Buffer as Buffer6 } from "node:buffer";
861
- import crypto2 from "node:crypto";
862
- import { VoiceOpcodes as VoiceOpcodes3 } from "discord-api-types/voice/v4";
863
-
864
- // src/receive/AudioReceiveStream.ts
865
- import { Readable } from "node:stream";
866
-
867
- // src/audio/AudioPlayer.ts
868
- import { Buffer as Buffer5 } from "node:buffer";
869
- import { EventEmitter as EventEmitter4 } from "node:events";
870
-
871
- // src/audio/AudioPlayerError.ts
872
- var AudioPlayerError = class extends Error {
873
- static {
874
- __name(this, "AudioPlayerError");
1163
+ this.debug?.(`<< ${event.data}`);
1164
+ let packet;
1165
+ try {
1166
+ packet = JSON.parse(event.data);
1167
+ } catch (error) {
1168
+ const err = error;
1169
+ this.emit("error", err);
1170
+ return;
1171
+ }
1172
+ if (packet.seq) {
1173
+ this.sequence = packet.seq;
1174
+ }
1175
+ if (packet.op === VoiceOpcodes.HeartbeatAck) {
1176
+ this.lastHeartbeatAck = Date.now();
1177
+ this.missedHeartbeats = 0;
1178
+ this.ping = this.lastHeartbeatAck - this.lastHeartbeatSend;
1179
+ }
1180
+ this.emit("packet", packet);
875
1181
  }
876
1182
  /**
877
- * The resource associated with the audio player at the time the error was thrown.
1183
+ * Sends a JSON-stringifiable packet over the WebSocket.
1184
+ *
1185
+ * @param packet - The packet to send
878
1186
  */
879
- resource;
880
- constructor(error, resource) {
881
- super(error.message);
882
- this.resource = resource;
883
- this.name = error.name;
884
- this.stack = error.stack;
885
- }
886
- };
887
-
888
- // src/audio/PlayerSubscription.ts
889
- var PlayerSubscription = class {
890
- static {
891
- __name(this, "PlayerSubscription");
1187
+ sendPacket(packet) {
1188
+ try {
1189
+ const stringified = JSON.stringify(packet);
1190
+ this.debug?.(`>> ${stringified}`);
1191
+ this.ws.send(stringified);
1192
+ } catch (error) {
1193
+ const err = error;
1194
+ this.emit("error", err);
1195
+ }
892
1196
  }
893
1197
  /**
894
- * The voice connection of this subscription.
1198
+ * Sends a binary message over the WebSocket.
1199
+ *
1200
+ * @param opcode - The opcode to use
1201
+ * @param payload - The payload to send
895
1202
  */
896
- connection;
1203
+ sendBinaryMessage(opcode, payload) {
1204
+ try {
1205
+ const message = Buffer6.concat([new Uint8Array([opcode]), payload]);
1206
+ this.debug?.(`>> [bin] opcode ${opcode}, ${payload.byteLength} bytes`);
1207
+ this.ws.send(message);
1208
+ } catch (error) {
1209
+ const err = error;
1210
+ this.emit("error", err);
1211
+ }
1212
+ }
897
1213
  /**
898
- * The audio player of this subscription.
1214
+ * Sends a heartbeat over the WebSocket.
899
1215
  */
900
- player;
901
- constructor(connection, player) {
902
- this.connection = connection;
903
- this.player = player;
1216
+ sendHeartbeat() {
1217
+ this.lastHeartbeatSend = Date.now();
1218
+ this.missedHeartbeats++;
1219
+ const nonce2 = this.lastHeartbeatSend;
1220
+ this.sendPacket({
1221
+ op: VoiceOpcodes.Heartbeat,
1222
+ // eslint-disable-next-line id-length
1223
+ d: {
1224
+ // eslint-disable-next-line id-length
1225
+ t: nonce2,
1226
+ seq_ack: this.sequence
1227
+ }
1228
+ });
904
1229
  }
905
1230
  /**
906
- * Unsubscribes the connection from the audio player, meaning that the
907
- * audio player cannot stream audio to it until a new subscription is made.
1231
+ * Sets/clears an interval to send heartbeats over the WebSocket.
1232
+ *
1233
+ * @param ms - The interval in milliseconds. If negative, the interval will be unset
908
1234
  */
909
- unsubscribe() {
910
- this.connection["onSubscriptionRemoved"](this);
911
- this.player["unsubscribe"](this);
1235
+ setHeartbeatInterval(ms) {
1236
+ if (this.heartbeatInterval !== void 0) clearInterval(this.heartbeatInterval);
1237
+ if (ms > 0) {
1238
+ this.heartbeatInterval = setInterval(() => {
1239
+ if (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) {
1240
+ this.ws.close();
1241
+ this.setHeartbeatInterval(-1);
1242
+ }
1243
+ this.sendHeartbeat();
1244
+ }, ms);
1245
+ }
912
1246
  }
913
1247
  };
914
1248
 
915
- // src/audio/AudioPlayer.ts
916
- var SILENCE_FRAME = Buffer5.from([248, 255, 254]);
917
- var NoSubscriberBehavior = /* @__PURE__ */ ((NoSubscriberBehavior2) => {
918
- NoSubscriberBehavior2["Pause"] = "pause";
919
- NoSubscriberBehavior2["Play"] = "play";
920
- NoSubscriberBehavior2["Stop"] = "stop";
921
- return NoSubscriberBehavior2;
922
- })(NoSubscriberBehavior || {});
923
- var AudioPlayerStatus = /* @__PURE__ */ ((AudioPlayerStatus2) => {
924
- AudioPlayerStatus2["AutoPaused"] = "autopaused";
925
- AudioPlayerStatus2["Buffering"] = "buffering";
926
- AudioPlayerStatus2["Idle"] = "idle";
927
- AudioPlayerStatus2["Paused"] = "paused";
928
- AudioPlayerStatus2["Playing"] = "playing";
929
- return AudioPlayerStatus2;
930
- })(AudioPlayerStatus || {});
1249
+ // src/networking/Networking.ts
1250
+ var CHANNELS = 2;
1251
+ var TIMESTAMP_INC = 48e3 / 100 * CHANNELS;
1252
+ var MAX_NONCE_SIZE = 2 ** 32 - 1;
1253
+ var SUPPORTED_ENCRYPTION_MODES = [VoiceEncryptionMode.AeadXChaCha20Poly1305RtpSize];
1254
+ if (crypto.getCiphers().includes("aes-256-gcm")) {
1255
+ SUPPORTED_ENCRYPTION_MODES.unshift(VoiceEncryptionMode.AeadAes256GcmRtpSize);
1256
+ }
1257
+ var NetworkingStatusCode = /* @__PURE__ */ ((NetworkingStatusCode2) => {
1258
+ NetworkingStatusCode2[NetworkingStatusCode2["OpeningWs"] = 0] = "OpeningWs";
1259
+ NetworkingStatusCode2[NetworkingStatusCode2["Identifying"] = 1] = "Identifying";
1260
+ NetworkingStatusCode2[NetworkingStatusCode2["UdpHandshaking"] = 2] = "UdpHandshaking";
1261
+ NetworkingStatusCode2[NetworkingStatusCode2["SelectingProtocol"] = 3] = "SelectingProtocol";
1262
+ NetworkingStatusCode2[NetworkingStatusCode2["Ready"] = 4] = "Ready";
1263
+ NetworkingStatusCode2[NetworkingStatusCode2["Resuming"] = 5] = "Resuming";
1264
+ NetworkingStatusCode2[NetworkingStatusCode2["Closed"] = 6] = "Closed";
1265
+ return NetworkingStatusCode2;
1266
+ })(NetworkingStatusCode || {});
1267
+ var nonce = Buffer7.alloc(24);
931
1268
  function stringifyState2(state) {
932
1269
  return JSON.stringify({
933
1270
  ...state,
934
- resource: Reflect.has(state, "resource"),
935
- stepTimeout: Reflect.has(state, "stepTimeout")
1271
+ ws: Reflect.has(state, "ws"),
1272
+ udp: Reflect.has(state, "udp")
936
1273
  });
937
1274
  }
938
1275
  __name(stringifyState2, "stringifyState");
939
- var AudioPlayer = class extends EventEmitter4 {
1276
+ function chooseEncryptionMode(options) {
1277
+ const option = options.find((option2) => SUPPORTED_ENCRYPTION_MODES.includes(option2));
1278
+ if (!option) {
1279
+ throw new Error(`No compatible encryption modes. Available include: ${options.join(", ")}`);
1280
+ }
1281
+ return option;
1282
+ }
1283
+ __name(chooseEncryptionMode, "chooseEncryptionMode");
1284
+ function randomNBit(numberOfBits) {
1285
+ return Math.floor(Math.random() * 2 ** numberOfBits);
1286
+ }
1287
+ __name(randomNBit, "randomNBit");
1288
+ var Networking = class extends EventEmitter5 {
940
1289
  static {
941
- __name(this, "AudioPlayer");
1290
+ __name(this, "Networking");
942
1291
  }
943
- /**
944
- * The state that the AudioPlayer is in.
945
- */
946
1292
  _state;
947
1293
  /**
948
- * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio
949
- * to the streams in this list.
1294
+ * The debug logger function, if debugging is enabled.
950
1295
  */
951
- subscribers = [];
1296
+ debug;
952
1297
  /**
953
- * The behavior that the player should follow when it enters certain situations.
1298
+ * The options used to create this Networking instance.
954
1299
  */
955
- behaviors;
1300
+ options;
956
1301
  /**
957
- * The debug logger function, if debugging is enabled.
1302
+ * Creates a new Networking instance.
958
1303
  */
959
- debug;
1304
+ constructor(connectionOptions, options) {
1305
+ super();
1306
+ this.onWsOpen = this.onWsOpen.bind(this);
1307
+ this.onChildError = this.onChildError.bind(this);
1308
+ this.onWsPacket = this.onWsPacket.bind(this);
1309
+ this.onWsBinary = this.onWsBinary.bind(this);
1310
+ this.onWsClose = this.onWsClose.bind(this);
1311
+ this.onWsDebug = this.onWsDebug.bind(this);
1312
+ this.onUdpDebug = this.onUdpDebug.bind(this);
1313
+ this.onUdpClose = this.onUdpClose.bind(this);
1314
+ this.onDaveDebug = this.onDaveDebug.bind(this);
1315
+ this.onDaveKeyPackage = this.onDaveKeyPackage.bind(this);
1316
+ this.onDaveInvalidateTransition = this.onDaveInvalidateTransition.bind(this);
1317
+ this.debug = options?.debug ? (message) => this.emit("debug", message) : null;
1318
+ this._state = {
1319
+ code: 0 /* OpeningWs */,
1320
+ ws: this.createWebSocket(connectionOptions.endpoint),
1321
+ connectionOptions
1322
+ };
1323
+ this.options = options;
1324
+ }
960
1325
  /**
961
- * Creates a new AudioPlayer.
1326
+ * Destroys the Networking instance, transitioning it into the Closed state.
962
1327
  */
963
- constructor(options = {}) {
964
- super();
965
- this._state = { status: "idle" /* Idle */ };
966
- this.behaviors = {
967
- noSubscriber: "pause" /* Pause */,
968
- maxMissedFrames: 5,
969
- ...options.behaviors
1328
+ destroy() {
1329
+ this.state = {
1330
+ code: 6 /* Closed */
970
1331
  };
971
- this.debug = options.debug === false ? null : (message) => this.emit("debug", message);
972
1332
  }
973
1333
  /**
974
- * A list of subscribed voice connections that can currently receive audio to play.
1334
+ * The current state of the networking instance.
1335
+ *
1336
+ * @remarks
1337
+ * The setter will perform clean-up operations where necessary.
975
1338
  */
976
- get playable() {
977
- return this.subscribers.filter(({ connection }) => connection.state.status === "ready" /* Ready */).map(({ connection }) => connection);
1339
+ get state() {
1340
+ return this._state;
1341
+ }
1342
+ set state(newState) {
1343
+ const oldWs = Reflect.get(this._state, "ws");
1344
+ const newWs = Reflect.get(newState, "ws");
1345
+ if (oldWs && oldWs !== newWs) {
1346
+ oldWs.off("debug", this.onWsDebug);
1347
+ oldWs.on("error", noop);
1348
+ oldWs.off("error", this.onChildError);
1349
+ oldWs.off("open", this.onWsOpen);
1350
+ oldWs.off("packet", this.onWsPacket);
1351
+ oldWs.off("binary", this.onWsBinary);
1352
+ oldWs.off("close", this.onWsClose);
1353
+ oldWs.destroy();
1354
+ }
1355
+ const oldUdp = Reflect.get(this._state, "udp");
1356
+ const newUdp = Reflect.get(newState, "udp");
1357
+ if (oldUdp && oldUdp !== newUdp) {
1358
+ oldUdp.on("error", noop);
1359
+ oldUdp.off("error", this.onChildError);
1360
+ oldUdp.off("close", this.onUdpClose);
1361
+ oldUdp.off("debug", this.onUdpDebug);
1362
+ oldUdp.destroy();
1363
+ }
1364
+ const oldDave = Reflect.get(this._state, "dave");
1365
+ const newDave = Reflect.get(newState, "dave");
1366
+ if (oldDave && oldDave !== newDave) {
1367
+ oldDave.off("error", this.onChildError);
1368
+ oldDave.off("debug", this.onDaveDebug);
1369
+ oldDave.off("keyPackage", this.onDaveKeyPackage);
1370
+ oldDave.off("invalidateTransition", this.onDaveInvalidateTransition);
1371
+ oldDave.destroy();
1372
+ }
1373
+ const oldState = this._state;
1374
+ this._state = newState;
1375
+ this.emit("stateChange", oldState, newState);
1376
+ this.debug?.(`state change:
1377
+ from ${stringifyState2(oldState)}
1378
+ to ${stringifyState2(newState)}`);
1379
+ }
1380
+ /**
1381
+ * Creates a new WebSocket to a Discord Voice gateway.
1382
+ *
1383
+ * @param endpoint - The endpoint to connect to
1384
+ * @param lastSequence - The last sequence to set for this WebSocket
1385
+ */
1386
+ createWebSocket(endpoint, lastSequence) {
1387
+ const ws = new VoiceWebSocket(`wss://${endpoint}?v=8`, Boolean(this.debug));
1388
+ if (lastSequence !== void 0) {
1389
+ ws.sequence = lastSequence;
1390
+ }
1391
+ ws.on("error", this.onChildError);
1392
+ ws.once("open", this.onWsOpen);
1393
+ ws.on("packet", this.onWsPacket);
1394
+ ws.on("binary", this.onWsBinary);
1395
+ ws.once("close", this.onWsClose);
1396
+ ws.on("debug", this.onWsDebug);
1397
+ return ws;
978
1398
  }
979
1399
  /**
980
- * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed,
981
- * then the existing subscription is used.
1400
+ * Creates a new DAVE session for this voice connection if we can create one.
982
1401
  *
983
- * @remarks
984
- * This method should not be directly called. Instead, use VoiceConnection#subscribe.
985
- * @param connection - The connection to subscribe
986
- * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription
1402
+ * @param protocolVersion - The protocol version to use
987
1403
  */
988
- // @ts-ignore
989
- subscribe(connection) {
990
- const existingSubscription = this.subscribers.find((subscription) => subscription.connection === connection);
991
- if (!existingSubscription) {
992
- const subscription = new PlayerSubscription(connection, this);
993
- this.subscribers.push(subscription);
994
- setImmediate(() => this.emit("subscribe", subscription));
995
- return subscription;
1404
+ createDaveSession(protocolVersion) {
1405
+ if (getMaxProtocolVersion() === null || this.options.daveEncryption === false || this.state.code !== 3 /* SelectingProtocol */ && this.state.code !== 4 /* Ready */ && this.state.code !== 5 /* Resuming */) {
1406
+ return;
996
1407
  }
997
- return existingSubscription;
1408
+ const session = new DAVESession(
1409
+ protocolVersion,
1410
+ this.state.connectionOptions.userId,
1411
+ this.state.connectionOptions.channelId,
1412
+ {
1413
+ decryptionFailureTolerance: this.options.decryptionFailureTolerance
1414
+ }
1415
+ );
1416
+ session.on("error", this.onChildError);
1417
+ session.on("debug", this.onDaveDebug);
1418
+ session.on("keyPackage", this.onDaveKeyPackage);
1419
+ session.on("invalidateTransition", this.onDaveInvalidateTransition);
1420
+ session.reinit();
1421
+ return session;
998
1422
  }
999
1423
  /**
1000
- * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player.
1424
+ * Propagates errors from the children VoiceWebSocket, VoiceUDPSocket and DAVESession.
1001
1425
  *
1002
- * @remarks
1003
- * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe.
1004
- * @param subscription - The subscription to remove
1005
- * @returns Whether or not the subscription existed on the player and was removed
1426
+ * @param error - The error that was emitted by a child
1006
1427
  */
1007
- // @ts-ignore
1008
- unsubscribe(subscription) {
1009
- const index = this.subscribers.indexOf(subscription);
1010
- const exists = index !== -1;
1011
- if (exists) {
1012
- this.subscribers.splice(index, 1);
1013
- subscription.connection.setSpeaking(false);
1014
- this.emit("unsubscribe", subscription);
1015
- }
1016
- return exists;
1428
+ onChildError(error) {
1429
+ this.emit("error", error);
1017
1430
  }
1018
1431
  /**
1019
- * The state that the player is in.
1432
+ * Called when the WebSocket opens. Depending on the state that the instance is in,
1433
+ * it will either identify with a new session, or it will attempt to resume an existing session.
1020
1434
  */
1021
- get state() {
1022
- return this._state;
1435
+ onWsOpen() {
1436
+ if (this.state.code === 0 /* OpeningWs */) {
1437
+ this.state.ws.sendPacket({
1438
+ op: VoiceOpcodes2.Identify,
1439
+ d: {
1440
+ server_id: this.state.connectionOptions.serverId,
1441
+ user_id: this.state.connectionOptions.userId,
1442
+ session_id: this.state.connectionOptions.sessionId,
1443
+ token: this.state.connectionOptions.token,
1444
+ max_dave_protocol_version: this.options.daveEncryption === false ? 0 : getMaxProtocolVersion() ?? 0
1445
+ }
1446
+ });
1447
+ this.state = {
1448
+ ...this.state,
1449
+ code: 1 /* Identifying */
1450
+ };
1451
+ } else if (this.state.code === 5 /* Resuming */) {
1452
+ this.state.ws.sendPacket({
1453
+ op: VoiceOpcodes2.Resume,
1454
+ d: {
1455
+ server_id: this.state.connectionOptions.serverId,
1456
+ session_id: this.state.connectionOptions.sessionId,
1457
+ token: this.state.connectionOptions.token,
1458
+ seq_ack: this.state.ws.sequence
1459
+ }
1460
+ });
1461
+ }
1023
1462
  }
1024
1463
  /**
1025
- * Sets a new state for the player, performing clean-up operations where necessary.
1464
+ * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter),
1465
+ * the instance will either attempt to resume, or enter the closed state and emit a 'close' event
1466
+ * with the close code, allowing the user to decide whether or not they would like to reconnect.
1467
+ *
1468
+ * @param code - The close code
1026
1469
  */
1027
- set state(newState) {
1028
- const oldState = this._state;
1029
- const newResource = Reflect.get(newState, "resource");
1030
- if (oldState.status !== "idle" /* Idle */ && oldState.resource !== newResource) {
1031
- oldState.resource.playStream.on("error", noop);
1032
- oldState.resource.playStream.off("error", oldState.onStreamError);
1033
- oldState.resource.audioPlayer = void 0;
1034
- oldState.resource.playStream.destroy();
1035
- oldState.resource.playStream.read();
1036
- }
1037
- if (oldState.status === "buffering" /* Buffering */ && (newState.status !== "buffering" /* Buffering */ || newState.resource !== oldState.resource)) {
1038
- oldState.resource.playStream.off("end", oldState.onFailureCallback);
1039
- oldState.resource.playStream.off("close", oldState.onFailureCallback);
1040
- oldState.resource.playStream.off("finish", oldState.onFailureCallback);
1041
- oldState.resource.playStream.off("readable", oldState.onReadableCallback);
1042
- }
1043
- if (newState.status === "idle" /* Idle */) {
1044
- this._signalStopSpeaking();
1045
- deleteAudioPlayer(this);
1046
- }
1047
- if (newResource) {
1048
- addAudioPlayer(this);
1470
+ onWsClose({ code }) {
1471
+ const canResume = code === 4015 || code < 4e3;
1472
+ if (canResume && this.state.code === 4 /* Ready */) {
1473
+ const lastSequence = this.state.ws.sequence;
1474
+ this.state = {
1475
+ ...this.state,
1476
+ code: 5 /* Resuming */,
1477
+ ws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence)
1478
+ };
1479
+ } else if (this.state.code !== 6 /* Closed */) {
1480
+ this.destroy();
1481
+ this.emit("close", code);
1049
1482
  }
1050
- const didChangeResources = oldState.status !== "idle" /* Idle */ && newState.status === "playing" /* Playing */ && oldState.resource !== newState.resource;
1051
- this._state = newState;
1052
- this.emit("stateChange", oldState, this._state);
1053
- if (oldState.status !== newState.status || didChangeResources) {
1054
- this.emit(newState.status, oldState, this._state);
1483
+ }
1484
+ /**
1485
+ * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord.
1486
+ */
1487
+ onUdpClose() {
1488
+ if (this.state.code === 4 /* Ready */) {
1489
+ const lastSequence = this.state.ws.sequence;
1490
+ this.state = {
1491
+ ...this.state,
1492
+ code: 5 /* Resuming */,
1493
+ ws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence)
1494
+ };
1055
1495
  }
1056
- this.debug?.(`state change:
1057
- from ${stringifyState2(oldState)}
1058
- to ${stringifyState2(newState)}`);
1059
1496
  }
1060
1497
  /**
1061
- * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed
1062
- * (it cannot be reused, even in another player) and is replaced with the new resource.
1063
- *
1064
- * @remarks
1065
- * The player will transition to the Playing state once playback begins, and will return to the Idle state once
1066
- * playback is ended.
1498
+ * Called when a packet is received on the connection's WebSocket.
1067
1499
  *
1068
- * If the player was previously playing a resource and this method is called, the player will not transition to the
1069
- * Idle state during the swap over.
1070
- * @param resource - The resource to play
1071
- * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player
1500
+ * @param packet - The received packet
1072
1501
  */
1073
- play(resource) {
1074
- if (resource.ended) {
1075
- throw new Error("Cannot play a resource that has already ended.");
1076
- }
1077
- if (resource.audioPlayer) {
1078
- if (resource.audioPlayer === this) {
1079
- return;
1080
- }
1081
- throw new Error("Resource is already being played by another audio player.");
1082
- }
1083
- resource.audioPlayer = this;
1084
- const onStreamError = /* @__PURE__ */ __name((error) => {
1085
- if (this.state.status !== "idle" /* Idle */) {
1086
- this.emit("error", new AudioPlayerError(error, this.state.resource));
1087
- }
1088
- if (this.state.status !== "idle" /* Idle */ && this.state.resource === resource) {
1502
+ onWsPacket(packet) {
1503
+ if (packet.op === VoiceOpcodes2.Hello && this.state.code !== 6 /* Closed */) {
1504
+ this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval);
1505
+ } else if (packet.op === VoiceOpcodes2.Ready && this.state.code === 1 /* Identifying */) {
1506
+ const { ip, port, ssrc, modes } = packet.d;
1507
+ const udp = new VoiceUDPSocket({ ip, port });
1508
+ udp.on("error", this.onChildError);
1509
+ udp.on("debug", this.onUdpDebug);
1510
+ udp.once("close", this.onUdpClose);
1511
+ udp.performIPDiscovery(ssrc).then((localConfig) => {
1512
+ if (this.state.code !== 2 /* UdpHandshaking */) return;
1513
+ this.state.ws.sendPacket({
1514
+ op: VoiceOpcodes2.SelectProtocol,
1515
+ d: {
1516
+ protocol: "udp",
1517
+ data: {
1518
+ address: localConfig.ip,
1519
+ port: localConfig.port,
1520
+ mode: chooseEncryptionMode(modes)
1521
+ }
1522
+ }
1523
+ });
1089
1524
  this.state = {
1090
- status: "idle" /* Idle */
1525
+ ...this.state,
1526
+ code: 3 /* SelectingProtocol */
1091
1527
  };
1092
- }
1093
- }, "onStreamError");
1094
- resource.playStream.once("error", onStreamError);
1095
- if (resource.started) {
1528
+ }).catch((error) => this.emit("error", error));
1096
1529
  this.state = {
1097
- status: "playing" /* Playing */,
1098
- missedFrames: 0,
1099
- playbackDuration: 0,
1100
- resource,
1101
- onStreamError
1102
- };
1103
- } else {
1104
- const onReadableCallback = /* @__PURE__ */ __name(() => {
1105
- if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) {
1106
- this.state = {
1107
- status: "playing" /* Playing */,
1108
- missedFrames: 0,
1109
- playbackDuration: 0,
1110
- resource,
1111
- onStreamError
1112
- };
1530
+ ...this.state,
1531
+ code: 2 /* UdpHandshaking */,
1532
+ udp,
1533
+ connectionData: {
1534
+ ssrc,
1535
+ connectedClients: /* @__PURE__ */ new Set()
1113
1536
  }
1114
- }, "onReadableCallback");
1115
- const onFailureCallback = /* @__PURE__ */ __name(() => {
1116
- if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) {
1117
- this.state = {
1118
- status: "idle" /* Idle */
1119
- };
1537
+ };
1538
+ } else if (packet.op === VoiceOpcodes2.SessionDescription && this.state.code === 3 /* SelectingProtocol */) {
1539
+ const { mode: encryptionMode, secret_key: secretKey, dave_protocol_version: daveProtocolVersion } = packet.d;
1540
+ this.state = {
1541
+ ...this.state,
1542
+ code: 4 /* Ready */,
1543
+ dave: this.createDaveSession(daveProtocolVersion),
1544
+ connectionData: {
1545
+ ...this.state.connectionData,
1546
+ encryptionMode,
1547
+ secretKey: new Uint8Array(secretKey),
1548
+ sequence: randomNBit(16),
1549
+ timestamp: randomNBit(32),
1550
+ nonce: 0,
1551
+ nonceBuffer: encryptionMode === "aead_aes256_gcm_rtpsize" ? Buffer7.alloc(12) : Buffer7.alloc(24),
1552
+ speaking: false,
1553
+ packetsPlayed: 0
1120
1554
  }
1121
- }, "onFailureCallback");
1122
- resource.playStream.once("readable", onReadableCallback);
1123
- resource.playStream.once("end", onFailureCallback);
1124
- resource.playStream.once("close", onFailureCallback);
1125
- resource.playStream.once("finish", onFailureCallback);
1555
+ };
1556
+ } else if (packet.op === VoiceOpcodes2.Resumed && this.state.code === 5 /* Resuming */) {
1126
1557
  this.state = {
1127
- status: "buffering" /* Buffering */,
1128
- resource,
1129
- onReadableCallback,
1130
- onFailureCallback,
1131
- onStreamError
1558
+ ...this.state,
1559
+ code: 4 /* Ready */
1132
1560
  };
1561
+ this.state.connectionData.speaking = false;
1562
+ } else if ((packet.op === VoiceOpcodes2.ClientsConnect || packet.op === VoiceOpcodes2.ClientDisconnect) && (this.state.code === 4 /* Ready */ || this.state.code === 2 /* UdpHandshaking */ || this.state.code === 3 /* SelectingProtocol */ || this.state.code === 5 /* Resuming */)) {
1563
+ const { connectionData } = this.state;
1564
+ if (packet.op === VoiceOpcodes2.ClientsConnect)
1565
+ for (const id of packet.d.user_ids) connectionData.connectedClients.add(id);
1566
+ else {
1567
+ connectionData.connectedClients.delete(packet.d.user_id);
1568
+ }
1569
+ } else if ((this.state.code === 4 /* Ready */ || this.state.code === 5 /* Resuming */) && this.state.dave) {
1570
+ if (packet.op === VoiceOpcodes2.DavePrepareTransition) {
1571
+ const sendReady = this.state.dave.prepareTransition(packet.d);
1572
+ if (sendReady)
1573
+ this.state.ws.sendPacket({
1574
+ op: VoiceOpcodes2.DaveTransitionReady,
1575
+ d: { transition_id: packet.d.transition_id }
1576
+ });
1577
+ if (packet.d.transition_id === 0) {
1578
+ this.emit("transitioned", 0);
1579
+ }
1580
+ } else if (packet.op === VoiceOpcodes2.DaveExecuteTransition) {
1581
+ const transitioned = this.state.dave.executeTransition(packet.d.transition_id);
1582
+ if (transitioned) this.emit("transitioned", packet.d.transition_id);
1583
+ } else if (packet.op === VoiceOpcodes2.DavePrepareEpoch) this.state.dave.prepareEpoch(packet.d);
1584
+ }
1585
+ }
1586
+ /**
1587
+ * Called when a binary message is received on the connection's WebSocket.
1588
+ *
1589
+ * @param message - The received message
1590
+ */
1591
+ onWsBinary(message) {
1592
+ if (this.state.code === 4 /* Ready */ && this.state.dave) {
1593
+ if (message.op === VoiceOpcodes2.DaveMlsExternalSender) {
1594
+ this.state.dave.setExternalSender(message.payload);
1595
+ } else if (message.op === VoiceOpcodes2.DaveMlsProposals) {
1596
+ const payload = this.state.dave.processProposals(message.payload, this.state.connectionData.connectedClients);
1597
+ if (payload) this.state.ws.sendBinaryMessage(VoiceOpcodes2.DaveMlsCommitWelcome, payload);
1598
+ } else if (message.op === VoiceOpcodes2.DaveMlsAnnounceCommitTransition) {
1599
+ const { transitionId, success } = this.state.dave.processCommit(message.payload);
1600
+ if (success) {
1601
+ if (transitionId === 0) this.emit("transitioned", transitionId);
1602
+ else
1603
+ this.state.ws.sendPacket({
1604
+ op: VoiceOpcodes2.DaveTransitionReady,
1605
+ d: { transition_id: transitionId }
1606
+ });
1607
+ }
1608
+ } else if (message.op === VoiceOpcodes2.DaveMlsWelcome) {
1609
+ const { transitionId, success } = this.state.dave.processWelcome(message.payload);
1610
+ if (success) {
1611
+ if (transitionId === 0) this.emit("transitioned", transitionId);
1612
+ else
1613
+ this.state.ws.sendPacket({
1614
+ op: VoiceOpcodes2.DaveTransitionReady,
1615
+ d: { transition_id: transitionId }
1616
+ });
1617
+ }
1618
+ }
1133
1619
  }
1134
1620
  }
1135
1621
  /**
1136
- * Pauses playback of the current resource, if any.
1622
+ * Called when a new key package is ready to be sent to the voice server.
1623
+ *
1624
+ * @param keyPackage - The new key package
1625
+ */
1626
+ onDaveKeyPackage(keyPackage) {
1627
+ if (this.state.code === 3 /* SelectingProtocol */ || this.state.code === 4 /* Ready */)
1628
+ this.state.ws.sendBinaryMessage(VoiceOpcodes2.DaveMlsKeyPackage, keyPackage);
1629
+ }
1630
+ /**
1631
+ * Called when the DAVE session wants to invalidate their transition and re-initialize.
1632
+ *
1633
+ * @param transitionId - The transition to invalidate
1634
+ */
1635
+ onDaveInvalidateTransition(transitionId) {
1636
+ if (this.state.code === 3 /* SelectingProtocol */ || this.state.code === 4 /* Ready */)
1637
+ this.state.ws.sendPacket({
1638
+ op: VoiceOpcodes2.DaveMlsInvalidCommitWelcome,
1639
+ d: { transition_id: transitionId }
1640
+ });
1641
+ }
1642
+ /**
1643
+ * Propagates debug messages from the child WebSocket.
1644
+ *
1645
+ * @param message - The emitted debug message
1646
+ */
1647
+ onWsDebug(message) {
1648
+ this.debug?.(`[WS] ${message}`);
1649
+ }
1650
+ /**
1651
+ * Propagates debug messages from the child UDPSocket.
1137
1652
  *
1138
- * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches
1139
- * @returns `true` if the player was successfully paused, otherwise `false`
1653
+ * @param message - The emitted debug message
1140
1654
  */
1141
- pause(interpolateSilence = true) {
1142
- if (this.state.status !== "playing" /* Playing */) return false;
1143
- this.state = {
1144
- ...this.state,
1145
- status: "paused" /* Paused */,
1146
- silencePacketsRemaining: interpolateSilence ? 5 : 0
1147
- };
1148
- return true;
1655
+ onUdpDebug(message) {
1656
+ this.debug?.(`[UDP] ${message}`);
1149
1657
  }
1150
1658
  /**
1151
- * Unpauses playback of the current resource, if any.
1659
+ * Propagates debug messages from the child DAVESession.
1152
1660
  *
1153
- * @returns `true` if the player was successfully unpaused, otherwise `false`
1661
+ * @param message - The emitted debug message
1154
1662
  */
1155
- unpause() {
1156
- if (this.state.status !== "paused" /* Paused */) return false;
1157
- this.state = {
1158
- ...this.state,
1159
- status: "playing" /* Playing */,
1160
- missedFrames: 0
1161
- };
1162
- return true;
1663
+ onDaveDebug(message) {
1664
+ this.debug?.(`[DAVE] ${message}`);
1163
1665
  }
1164
1666
  /**
1165
- * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state,
1166
- * or remain in its current state until the silence padding frames of the resource have been played.
1667
+ * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it.
1668
+ * It will be stored within the instance, and can be played by dispatchAudio()
1167
1669
  *
1168
- * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames
1169
- * @returns `true` if the player will come to a stop, otherwise `false`
1670
+ * @remarks
1671
+ * Calling this method while there is already a prepared audio packet that has not yet been dispatched
1672
+ * will overwrite the existing audio packet. This should be avoided.
1673
+ * @param opusPacket - The Opus packet to encrypt
1674
+ * @returns The audio packet that was prepared
1170
1675
  */
1171
- stop(force = false) {
1172
- if (this.state.status === "idle" /* Idle */) return false;
1173
- if (force || this.state.resource.silencePaddingFrames === 0) {
1174
- this.state = {
1175
- status: "idle" /* Idle */
1176
- };
1177
- } else if (this.state.resource.silenceRemaining === -1) {
1178
- this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;
1179
- }
1180
- return true;
1676
+ prepareAudioPacket(opusPacket) {
1677
+ const state = this.state;
1678
+ if (state.code !== 4 /* Ready */) return;
1679
+ state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData, state.dave);
1680
+ return state.preparedPacket;
1181
1681
  }
1182
1682
  /**
1183
- * Checks whether the underlying resource (if any) is playable (readable)
1184
- *
1185
- * @returns `true` if the resource is playable, otherwise `false`
1683
+ * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet
1684
+ * is consumed and cannot be dispatched again.
1186
1685
  */
1187
- checkPlayable() {
1188
- const state = this._state;
1189
- if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return false;
1190
- if (!state.resource.readable) {
1191
- this.state = {
1192
- status: "idle" /* Idle */
1193
- };
1194
- return false;
1686
+ dispatchAudio() {
1687
+ const state = this.state;
1688
+ if (state.code !== 4 /* Ready */) return false;
1689
+ if (state.preparedPacket !== void 0) {
1690
+ this.playAudioPacket(state.preparedPacket);
1691
+ state.preparedPacket = void 0;
1692
+ return true;
1195
1693
  }
1196
- return true;
1694
+ return false;
1197
1695
  }
1198
1696
  /**
1199
- * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered
1200
- * by the active connections of this audio player.
1697
+ * Plays an audio packet, updating timing metadata used for playback.
1698
+ *
1699
+ * @param audioPacket - The audio packet to play
1201
1700
  */
1202
- // @ts-ignore
1203
- _stepDispatch() {
1204
- const state = this._state;
1205
- if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return;
1206
- for (const connection of this.playable) {
1207
- connection.dispatchAudio();
1208
- }
1701
+ playAudioPacket(audioPacket) {
1702
+ const state = this.state;
1703
+ if (state.code !== 4 /* Ready */) return;
1704
+ const { connectionData } = state;
1705
+ connectionData.packetsPlayed++;
1706
+ connectionData.sequence++;
1707
+ connectionData.timestamp += TIMESTAMP_INC;
1708
+ if (connectionData.sequence >= 2 ** 16) connectionData.sequence = 0;
1709
+ if (connectionData.timestamp >= 2 ** 32) connectionData.timestamp = 0;
1710
+ this.setSpeaking(true);
1711
+ state.udp.send(audioPacket);
1209
1712
  }
1210
1713
  /**
1211
- * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the
1212
- * underlying resource of the stream, and then has all the active connections of the audio player prepare it
1213
- * (encrypt it, append header data) so that it is ready to play at the start of the next cycle.
1714
+ * Sends a packet to the voice gateway indicating that the client has start/stopped sending
1715
+ * audio.
1716
+ *
1717
+ * @param speaking - Whether or not the client should be shown as speaking
1214
1718
  */
1215
- // @ts-ignore
1216
- _stepPrepare() {
1217
- const state = this._state;
1218
- if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return;
1219
- const playable = this.playable;
1220
- if (state.status === "autopaused" /* AutoPaused */ && playable.length > 0) {
1221
- this.state = {
1222
- ...state,
1223
- status: "playing" /* Playing */,
1224
- missedFrames: 0
1225
- };
1226
- }
1227
- if (state.status === "paused" /* Paused */ || state.status === "autopaused" /* AutoPaused */) {
1228
- if (state.silencePacketsRemaining > 0) {
1229
- state.silencePacketsRemaining--;
1230
- this._preparePacket(SILENCE_FRAME, playable, state);
1231
- if (state.silencePacketsRemaining === 0) {
1232
- this._signalStopSpeaking();
1233
- }
1234
- }
1235
- return;
1236
- }
1237
- if (playable.length === 0) {
1238
- if (this.behaviors.noSubscriber === "pause" /* Pause */) {
1239
- this.state = {
1240
- ...state,
1241
- status: "autopaused" /* AutoPaused */,
1242
- silencePacketsRemaining: 5
1243
- };
1244
- return;
1245
- } else if (this.behaviors.noSubscriber === "stop" /* Stop */) {
1246
- this.stop(true);
1247
- }
1248
- }
1249
- const packet = state.resource.read();
1250
- if (state.status === "playing" /* Playing */) {
1251
- if (packet) {
1252
- this._preparePacket(packet, playable, state);
1253
- state.missedFrames = 0;
1254
- } else {
1255
- this._preparePacket(SILENCE_FRAME, playable, state);
1256
- state.missedFrames++;
1257
- if (state.missedFrames >= this.behaviors.maxMissedFrames) {
1258
- this.stop();
1259
- }
1719
+ setSpeaking(speaking) {
1720
+ const state = this.state;
1721
+ if (state.code !== 4 /* Ready */) return;
1722
+ if (state.connectionData.speaking === speaking) return;
1723
+ state.connectionData.speaking = speaking;
1724
+ state.ws.sendPacket({
1725
+ op: VoiceOpcodes2.Speaking,
1726
+ d: {
1727
+ speaking: speaking ? 1 : 0,
1728
+ delay: 0,
1729
+ ssrc: state.connectionData.ssrc
1260
1730
  }
1261
- }
1731
+ });
1262
1732
  }
1263
1733
  /**
1264
- * Signals to all the subscribed connections that they should send a packet to Discord indicating
1265
- * they are no longer speaking. Called once playback of a resource ends.
1734
+ * Creates a new audio packet from an Opus packet. This involves encrypting the packet,
1735
+ * then prepending a header that includes metadata.
1736
+ *
1737
+ * @param opusPacket - The Opus packet to prepare
1738
+ * @param connectionData - The current connection data of the instance
1739
+ * @param daveSession - The DAVE session to use for encryption
1266
1740
  */
1267
- _signalStopSpeaking() {
1268
- for (const { connection } of this.subscribers) {
1269
- connection.setSpeaking(false);
1270
- }
1741
+ createAudioPacket(opusPacket, connectionData, daveSession) {
1742
+ const rtpHeader = Buffer7.alloc(12);
1743
+ rtpHeader[0] = 128;
1744
+ rtpHeader[1] = 120;
1745
+ const { sequence, timestamp, ssrc } = connectionData;
1746
+ rtpHeader.writeUIntBE(sequence, 2, 2);
1747
+ rtpHeader.writeUIntBE(timestamp, 4, 4);
1748
+ rtpHeader.writeUIntBE(ssrc, 8, 4);
1749
+ rtpHeader.copy(nonce, 0, 0, 12);
1750
+ return Buffer7.concat([rtpHeader, ...this.encryptOpusPacket(opusPacket, connectionData, rtpHeader, daveSession)]);
1271
1751
  }
1272
1752
  /**
1273
- * Instructs the given connections to each prepare this packet to be played at the start of the
1274
- * next cycle.
1753
+ * Encrypts an Opus packet using the format agreed upon by the instance and Discord.
1275
1754
  *
1276
- * @param packet - The Opus packet to be prepared by each receiver
1277
- * @param receivers - The connections that should play this packet
1755
+ * @param opusPacket - The Opus packet to encrypt
1756
+ * @param connectionData - The current connection data of the instance
1757
+ * @param daveSession - The DAVE session to use for encryption
1278
1758
  */
1279
- _preparePacket(packet, receivers, state) {
1280
- state.playbackDuration += 20;
1281
- for (const connection of receivers) {
1282
- connection.prepareAudioPacket(packet);
1759
+ encryptOpusPacket(opusPacket, connectionData, additionalData, daveSession) {
1760
+ const { secretKey, encryptionMode } = connectionData;
1761
+ const packet = daveSession?.encrypt(opusPacket) ?? opusPacket;
1762
+ connectionData.nonce++;
1763
+ if (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0;
1764
+ connectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0);
1765
+ const noncePadding = connectionData.nonceBuffer.subarray(0, 4);
1766
+ let encrypted;
1767
+ switch (encryptionMode) {
1768
+ case "aead_aes256_gcm_rtpsize": {
1769
+ const cipher = crypto.createCipheriv("aes-256-gcm", secretKey, connectionData.nonceBuffer);
1770
+ cipher.setAAD(additionalData);
1771
+ encrypted = Buffer7.concat([cipher.update(packet), cipher.final(), cipher.getAuthTag()]);
1772
+ return [encrypted, noncePadding];
1773
+ }
1774
+ case "aead_xchacha20_poly1305_rtpsize": {
1775
+ encrypted = methods.crypto_aead_xchacha20poly1305_ietf_encrypt(
1776
+ packet,
1777
+ additionalData,
1778
+ connectionData.nonceBuffer,
1779
+ secretKey
1780
+ );
1781
+ return [encrypted, noncePadding];
1782
+ }
1783
+ default: {
1784
+ throw new RangeError(`Unsupported encryption method: ${encryptionMode}`);
1785
+ }
1283
1786
  }
1284
1787
  }
1285
1788
  };
1286
- function createAudioPlayer(options) {
1287
- return new AudioPlayer(options);
1288
- }
1289
- __name(createAudioPlayer, "createAudioPlayer");
1789
+
1790
+ // src/receive/VoiceReceiver.ts
1791
+ import { Buffer as Buffer8 } from "buffer";
1792
+ import crypto2 from "crypto";
1793
+ import { VoiceOpcodes as VoiceOpcodes3 } from "discord-api-types/voice/v8";
1290
1794
 
1291
1795
  // src/receive/AudioReceiveStream.ts
1796
+ import { nextTick } from "process";
1797
+ import { Readable } from "stream";
1292
1798
  var EndBehaviorType = /* @__PURE__ */ ((EndBehaviorType2) => {
1293
1799
  EndBehaviorType2[EndBehaviorType2["Manual"] = 0] = "Manual";
1294
1800
  EndBehaviorType2[EndBehaviorType2["AfterSilence"] = 1] = "AfterSilence";
@@ -1312,9 +1818,10 @@ var AudioReceiveStream = class extends Readable {
1312
1818
  */
1313
1819
  end;
1314
1820
  endTimeout;
1315
- constructor({ end, ...options }) {
1821
+ constructor(options) {
1822
+ const { end, ...rest } = options;
1316
1823
  super({
1317
- ...options,
1824
+ ...rest,
1318
1825
  objectMode: true
1319
1826
  });
1320
1827
  this.end = end;
@@ -1323,6 +1830,9 @@ var AudioReceiveStream = class extends Readable {
1323
1830
  if (buffer && (this.end.behavior === 2 /* AfterInactivity */ || this.end.behavior === 1 /* AfterSilence */ && (buffer.compare(SILENCE_FRAME) !== 0 || this.endTimeout === void 0))) {
1324
1831
  this.renewEndTimeout(this.end);
1325
1832
  }
1833
+ if (buffer === null) {
1834
+ nextTick(() => this.destroy());
1835
+ }
1326
1836
  return super.push(buffer);
1327
1837
  }
1328
1838
  renewEndTimeout(end) {
@@ -1336,8 +1846,8 @@ var AudioReceiveStream = class extends Readable {
1336
1846
  };
1337
1847
 
1338
1848
  // src/receive/SSRCMap.ts
1339
- import { EventEmitter as EventEmitter5 } from "node:events";
1340
- var SSRCMap = class extends EventEmitter5 {
1849
+ import { EventEmitter as EventEmitter6 } from "events";
1850
+ var SSRCMap = class extends EventEmitter6 {
1341
1851
  static {
1342
1852
  __name(this, "SSRCMap");
1343
1853
  }
@@ -1407,8 +1917,8 @@ var SSRCMap = class extends EventEmitter5 {
1407
1917
  };
1408
1918
 
1409
1919
  // src/receive/SpeakingMap.ts
1410
- import { EventEmitter as EventEmitter6 } from "node:events";
1411
- var SpeakingMap = class _SpeakingMap extends EventEmitter6 {
1920
+ import { EventEmitter as EventEmitter7 } from "events";
1921
+ var SpeakingMap = class _SpeakingMap extends EventEmitter7 {
1412
1922
  static {
1413
1923
  __name(this, "SpeakingMap");
1414
1924
  }
@@ -1449,7 +1959,7 @@ var SpeakingMap = class _SpeakingMap extends EventEmitter6 {
1449
1959
  };
1450
1960
 
1451
1961
  // src/receive/VoiceReceiver.ts
1452
- var HEADER_EXTENSION_BYTE = Buffer6.from([190, 222]);
1962
+ var HEADER_EXTENSION_BYTE = Buffer8.from([190, 222]);
1453
1963
  var UNPADDED_NONCE_LENGTH = 4;
1454
1964
  var AUTH_TAG_LENGTH = 16;
1455
1965
  var VoiceReceiver = class {
@@ -1494,16 +2004,10 @@ var VoiceReceiver = class {
1494
2004
  * @internal
1495
2005
  */
1496
2006
  onWsPacket(packet) {
1497
- if (packet.op === VoiceOpcodes3.ClientDisconnect && typeof packet.d?.user_id === "string") {
2007
+ if (packet.op === VoiceOpcodes3.ClientDisconnect) {
1498
2008
  this.ssrcMap.delete(packet.d.user_id);
1499
- } else if (packet.op === VoiceOpcodes3.Speaking && typeof packet.d?.user_id === "string" && typeof packet.d?.ssrc === "number") {
2009
+ } else if (packet.op === VoiceOpcodes3.Speaking) {
1500
2010
  this.ssrcMap.update({ userId: packet.d.user_id, audioSSRC: packet.d.ssrc });
1501
- } else if (packet.op === VoiceOpcodes3.ClientConnect && typeof packet.d?.user_id === "string" && typeof packet.d?.audio_ssrc === "number") {
1502
- this.ssrcMap.update({
1503
- userId: packet.d.user_id,
1504
- audioSSRC: packet.d.audio_ssrc,
1505
- videoSSRC: packet.d.video_ssrc === 0 ? void 0 : packet.d.video_ssrc
1506
- });
1507
2011
  }
1508
2012
  }
1509
2013
  decrypt(buffer, mode, nonce2, secretKey) {
@@ -1522,12 +2026,12 @@ var VoiceReceiver = class {
1522
2026
  const decipheriv = crypto2.createDecipheriv("aes-256-gcm", secretKey, nonce2);
1523
2027
  decipheriv.setAAD(header);
1524
2028
  decipheriv.setAuthTag(authTag);
1525
- return Buffer6.concat([decipheriv.update(encrypted), decipheriv.final()]);
2029
+ return Buffer8.concat([decipheriv.update(encrypted), decipheriv.final()]);
1526
2030
  }
1527
2031
  case "aead_xchacha20_poly1305_rtpsize": {
1528
- return Buffer6.from(
2032
+ return Buffer8.from(
1529
2033
  methods.crypto_aead_xchacha20poly1305_ietf_decrypt(
1530
- Buffer6.concat([encrypted, authTag]),
2034
+ Buffer8.concat([encrypted, authTag]),
1531
2035
  header,
1532
2036
  nonce2,
1533
2037
  secretKey
@@ -1546,15 +2050,20 @@ var VoiceReceiver = class {
1546
2050
  * @param mode - The encryption mode
1547
2051
  * @param nonce - The nonce buffer used by the connection for encryption
1548
2052
  * @param secretKey - The secret key used by the connection for encryption
2053
+ * @param userId - The user id that sent the packet
1549
2054
  * @returns The parsed Opus packet
1550
2055
  */
1551
- parsePacket(buffer, mode, nonce2, secretKey) {
2056
+ parsePacket(buffer, mode, nonce2, secretKey, userId) {
1552
2057
  let packet = this.decrypt(buffer, mode, nonce2, secretKey);
1553
- if (!packet) return;
2058
+ if (!packet) throw new Error("Failed to parse packet");
1554
2059
  if (buffer.subarray(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) {
1555
2060
  const headerExtensionLength = buffer.subarray(14).readUInt16BE();
1556
2061
  packet = packet.subarray(4 * headerExtensionLength);
1557
2062
  }
2063
+ if (this.voiceConnection.state.status === "ready" /* Ready */ && (this.voiceConnection.state.networking.state.code === 4 /* Ready */ || this.voiceConnection.state.networking.state.code === 5 /* Resuming */)) {
2064
+ const daveSession = this.voiceConnection.state.networking.state.dave;
2065
+ if (daveSession) packet = daveSession.decrypt(packet, userId);
2066
+ }
1558
2067
  return packet;
1559
2068
  }
1560
2069
  /**
@@ -1572,16 +2081,17 @@ var VoiceReceiver = class {
1572
2081
  const stream = this.subscriptions.get(userData.userId);
1573
2082
  if (!stream) return;
1574
2083
  if (this.connectionData.encryptionMode && this.connectionData.nonceBuffer && this.connectionData.secretKey) {
1575
- const packet = this.parsePacket(
1576
- msg,
1577
- this.connectionData.encryptionMode,
1578
- this.connectionData.nonceBuffer,
1579
- this.connectionData.secretKey
1580
- );
1581
- if (packet) {
1582
- stream.push(packet);
1583
- } else {
1584
- stream.destroy(new Error("Failed to parse packet"));
2084
+ try {
2085
+ const packet = this.parsePacket(
2086
+ msg,
2087
+ this.connectionData.encryptionMode,
2088
+ this.connectionData.nonceBuffer,
2089
+ this.connectionData.secretKey,
2090
+ userData.userId
2091
+ );
2092
+ if (packet) stream.push(packet);
2093
+ } catch (error) {
2094
+ stream.destroy(error);
1585
2095
  }
1586
2096
  }
1587
2097
  }
@@ -1620,7 +2130,7 @@ var VoiceConnectionDisconnectReason = /* @__PURE__ */ ((VoiceConnectionDisconnec
1620
2130
  VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["Manual"] = 3] = "Manual";
1621
2131
  return VoiceConnectionDisconnectReason2;
1622
2132
  })(VoiceConnectionDisconnectReason || {});
1623
- var VoiceConnection = class extends EventEmitter7 {
2133
+ var VoiceConnection = class extends EventEmitter8 {
1624
2134
  static {
1625
2135
  __name(this, "VoiceConnection");
1626
2136
  }
@@ -1653,6 +2163,10 @@ var VoiceConnection = class extends EventEmitter7 {
1653
2163
  * The debug logger function, if debugging is enabled.
1654
2164
  */
1655
2165
  debug;
2166
+ /**
2167
+ * The options used to create this voice connection.
2168
+ */
2169
+ options;
1656
2170
  /**
1657
2171
  * Creates a new voice connection.
1658
2172
  *
@@ -1668,6 +2182,7 @@ var VoiceConnection = class extends EventEmitter7 {
1668
2182
  this.onNetworkingStateChange = this.onNetworkingStateChange.bind(this);
1669
2183
  this.onNetworkingError = this.onNetworkingError.bind(this);
1670
2184
  this.onNetworkingDebug = this.onNetworkingDebug.bind(this);
2185
+ this.onNetworkingTransitioned = this.onNetworkingTransitioned.bind(this);
1671
2186
  const adapter = options.adapterCreator({
1672
2187
  onVoiceServerUpdate: /* @__PURE__ */ __name((data) => this.addServerPacket(data), "onVoiceServerUpdate"),
1673
2188
  onVoiceStateUpdate: /* @__PURE__ */ __name((data) => this.addStatePacket(data), "onVoiceStateUpdate"),
@@ -1679,16 +2194,17 @@ var VoiceConnection = class extends EventEmitter7 {
1679
2194
  state: void 0
1680
2195
  };
1681
2196
  this.joinConfig = joinConfig;
2197
+ this.options = options;
1682
2198
  }
1683
2199
  /**
1684
2200
  * The current state of the voice connection.
2201
+ *
2202
+ * @remarks
2203
+ * The setter will perform clean-up operations where necessary.
1685
2204
  */
1686
2205
  get state() {
1687
2206
  return this._state;
1688
2207
  }
1689
- /**
1690
- * Updates the state of the voice connection, performing clean-up operations where necessary.
1691
- */
1692
2208
  set state(newState) {
1693
2209
  const oldState = this._state;
1694
2210
  const oldNetworking = Reflect.get(oldState, "networking");
@@ -1702,6 +2218,7 @@ var VoiceConnection = class extends EventEmitter7 {
1702
2218
  oldNetworking.off("error", this.onNetworkingError);
1703
2219
  oldNetworking.off("close", this.onNetworkingClose);
1704
2220
  oldNetworking.off("stateChange", this.onNetworkingStateChange);
2221
+ oldNetworking.off("transitioned", this.onNetworkingTransitioned);
1705
2222
  oldNetworking.destroy();
1706
2223
  }
1707
2224
  if (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state);
@@ -1797,14 +2314,20 @@ var VoiceConnection = class extends EventEmitter7 {
1797
2314
  serverId: server.guild_id,
1798
2315
  token: server.token,
1799
2316
  sessionId: state.session_id,
1800
- userId: state.user_id
2317
+ userId: state.user_id,
2318
+ channelId: state.channel_id
1801
2319
  },
1802
- Boolean(this.debug)
2320
+ {
2321
+ debug: Boolean(this.debug),
2322
+ daveEncryption: this.options.daveEncryption ?? true,
2323
+ decryptionFailureTolerance: this.options.decryptionFailureTolerance
2324
+ }
1803
2325
  );
1804
2326
  networking.once("close", this.onNetworkingClose);
1805
2327
  networking.on("stateChange", this.onNetworkingStateChange);
1806
2328
  networking.on("error", this.onNetworkingError);
1807
2329
  networking.on("debug", this.onNetworkingDebug);
2330
+ networking.on("transitioned", this.onNetworkingTransitioned);
1808
2331
  this.state = {
1809
2332
  ...this.state,
1810
2333
  status: "connecting" /* Connecting */,
@@ -1885,6 +2408,14 @@ var VoiceConnection = class extends EventEmitter7 {
1885
2408
  onNetworkingDebug(message) {
1886
2409
  this.debug?.(`[NW] ${message}`);
1887
2410
  }
2411
+ /**
2412
+ * Propagates transitions from the underlying network instance.
2413
+ *
2414
+ * @param transitionId - The transition id
2415
+ */
2416
+ onNetworkingTransitioned(transitionId) {
2417
+ this.emit("transitioned", transitionId);
2418
+ }
1888
2419
  /**
1889
2420
  * Prepares an audio packet for dispatch.
1890
2421
  *
@@ -2040,6 +2571,30 @@ var VoiceConnection = class extends EventEmitter7 {
2040
2571
  udp: void 0
2041
2572
  };
2042
2573
  }
2574
+ /**
2575
+ * The current voice privacy code of the encrypted session.
2576
+ *
2577
+ * @remarks
2578
+ * For this data to be available, the VoiceConnection must be in the Ready state,
2579
+ * and the connection would have to be end-to-end encrypted.
2580
+ */
2581
+ get voicePrivacyCode() {
2582
+ if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */) {
2583
+ return this.state.networking.state.dave?.voicePrivacyCode ?? void 0;
2584
+ }
2585
+ return void 0;
2586
+ }
2587
+ /**
2588
+ * Gets the verification code for a user in the session.
2589
+ *
2590
+ * @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session.
2591
+ */
2592
+ async getVerificationCode(userId) {
2593
+ if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */ && this.state.networking.state.dave) {
2594
+ return this.state.networking.state.dave.getVerificationCode(userId);
2595
+ }
2596
+ throw new Error("Session not available");
2597
+ }
2043
2598
  /**
2044
2599
  * Called when a subscription of this voice connection to an audio player is removed.
2045
2600
  *
@@ -2096,13 +2651,15 @@ function joinVoiceChannel(options) {
2096
2651
  };
2097
2652
  return createVoiceConnection(joinConfig, {
2098
2653
  adapterCreator: options.adapterCreator,
2099
- debug: options.debug
2654
+ debug: options.debug,
2655
+ daveEncryption: options.daveEncryption,
2656
+ decryptionFailureTolerance: options.decryptionFailureTolerance
2100
2657
  });
2101
2658
  }
2102
2659
  __name(joinVoiceChannel, "joinVoiceChannel");
2103
2660
 
2104
2661
  // src/audio/AudioResource.ts
2105
- import { pipeline } from "node:stream";
2662
+ import { pipeline } from "stream";
2106
2663
  import prism2 from "prism-media";
2107
2664
 
2108
2665
  // src/audio/TransformerGraph.ts
@@ -2130,6 +2687,16 @@ var StreamType = /* @__PURE__ */ ((StreamType2) => {
2130
2687
  StreamType2["WebmOpus"] = "webm/opus";
2131
2688
  return StreamType2;
2132
2689
  })(StreamType || {});
2690
+ var TransformerType = /* @__PURE__ */ ((TransformerType2) => {
2691
+ TransformerType2["FFmpegOgg"] = "ffmpeg ogg";
2692
+ TransformerType2["FFmpegPCM"] = "ffmpeg pcm";
2693
+ TransformerType2["InlineVolume"] = "volume transformer";
2694
+ TransformerType2["OggOpusDemuxer"] = "ogg/opus demuxer";
2695
+ TransformerType2["OpusDecoder"] = "opus decoder";
2696
+ TransformerType2["OpusEncoder"] = "opus encoder";
2697
+ TransformerType2["WebmOpusDemuxer"] = "webm/opus demuxer";
2698
+ return TransformerType2;
2699
+ })(TransformerType || {});
2133
2700
  var Node = class {
2134
2701
  static {
2135
2702
  __name(this, "Node");
@@ -2419,7 +2986,8 @@ function createAudioResource(input, options = {}) {
2419
2986
  __name(createAudioResource, "createAudioResource");
2420
2987
 
2421
2988
  // src/util/generateDependencyReport.ts
2422
- import { resolve, dirname } from "node:path";
2989
+ import { getCiphers } from "crypto";
2990
+ import { resolve, dirname } from "path";
2423
2991
  import prism3 from "prism-media";
2424
2992
  function findPackageJSON(dir, packageName, depth) {
2425
2993
  if (depth === 0) return void 0;
@@ -2436,7 +3004,7 @@ __name(findPackageJSON, "findPackageJSON");
2436
3004
  function version(name) {
2437
3005
  try {
2438
3006
  if (name === "@discordjs/voice") {
2439
- return "0.18.1-dev.1732709130-97ffa201a";
3007
+ return "0.19.0";
2440
3008
  }
2441
3009
  const pkg = findPackageJSON(dirname(__require.resolve(name)), name, 3);
2442
3010
  return pkg?.version ?? "not found";
@@ -2457,12 +3025,16 @@ function generateDependencyReport() {
2457
3025
  addVersion("opusscript");
2458
3026
  report.push("");
2459
3027
  report.push("Encryption Libraries");
3028
+ report.push(`- native crypto support for aes-256-gcm: ${getCiphers().includes("aes-256-gcm") ? "yes" : "no"}`);
2460
3029
  addVersion("sodium-native");
2461
3030
  addVersion("sodium");
2462
3031
  addVersion("libsodium-wrappers");
2463
3032
  addVersion("@stablelib/xchacha20poly1305");
2464
3033
  addVersion("@noble/ciphers");
2465
3034
  report.push("");
3035
+ report.push("DAVE Libraries");
3036
+ addVersion("@snazzah/davey");
3037
+ report.push("");
2466
3038
  report.push("FFmpeg");
2467
3039
  try {
2468
3040
  const info = prism3.FFmpeg.getInfo();
@@ -2476,7 +3048,7 @@ function generateDependencyReport() {
2476
3048
  __name(generateDependencyReport, "generateDependencyReport");
2477
3049
 
2478
3050
  // src/util/entersState.ts
2479
- import { once } from "node:events";
3051
+ import { once } from "events";
2480
3052
 
2481
3053
  // src/util/abortAfter.ts
2482
3054
  function abortAfter(delay) {
@@ -2502,9 +3074,9 @@ async function entersState(target, status, timeoutOrSignal) {
2502
3074
  __name(entersState, "entersState");
2503
3075
 
2504
3076
  // src/util/demuxProbe.ts
2505
- import { Buffer as Buffer7 } from "node:buffer";
2506
- import process from "node:process";
2507
- import { Readable as Readable2 } from "node:stream";
3077
+ import { Buffer as Buffer9 } from "buffer";
3078
+ import process from "process";
3079
+ import { Readable as Readable2 } from "stream";
2508
3080
  import prism4 from "prism-media";
2509
3081
  function validateDiscordOpusHead(opusHead) {
2510
3082
  const channels = opusHead.readUInt8(9);
@@ -2522,7 +3094,7 @@ async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordO
2522
3094
  reject(new Error("Cannot probe a stream that has ended"));
2523
3095
  return;
2524
3096
  }
2525
- let readBuffer = Buffer7.alloc(0);
3097
+ let readBuffer = Buffer9.alloc(0);
2526
3098
  let resolved;
2527
3099
  const finish = /* @__PURE__ */ __name((type) => {
2528
3100
  stream.off("data", onData);
@@ -2562,7 +3134,7 @@ async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordO
2562
3134
  }
2563
3135
  }, "onClose");
2564
3136
  const onData = /* @__PURE__ */ __name((buffer) => {
2565
- readBuffer = Buffer7.concat([readBuffer, buffer]);
3137
+ readBuffer = Buffer9.concat([readBuffer, buffer]);
2566
3138
  webm.write(buffer);
2567
3139
  ogg.write(buffer);
2568
3140
  if (readBuffer.length >= probeSize) {
@@ -2580,23 +3152,30 @@ async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordO
2580
3152
  __name(demuxProbe, "demuxProbe");
2581
3153
 
2582
3154
  // src/index.ts
2583
- var version2 = "0.18.1-dev.1732709130-97ffa201a";
3155
+ var version2 = "0.19.0";
2584
3156
  export {
2585
3157
  AudioPlayer,
2586
3158
  AudioPlayerError,
2587
3159
  AudioPlayerStatus,
2588
3160
  AudioReceiveStream,
2589
3161
  AudioResource,
3162
+ DAVESession,
2590
3163
  EndBehaviorType,
3164
+ Networking,
3165
+ NetworkingStatusCode,
2591
3166
  NoSubscriberBehavior,
3167
+ Node,
2592
3168
  PlayerSubscription,
2593
3169
  SSRCMap,
2594
3170
  SpeakingMap,
2595
3171
  StreamType,
3172
+ TransformerType,
2596
3173
  VoiceConnection,
2597
3174
  VoiceConnectionDisconnectReason,
2598
3175
  VoiceConnectionStatus,
2599
3176
  VoiceReceiver,
3177
+ VoiceUDPSocket,
3178
+ VoiceWebSocket,
2600
3179
  createAudioPlayer,
2601
3180
  createAudioResource,
2602
3181
  createDefaultAudioReceiveStreamOptions,