@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.js CHANGED
@@ -29,23 +29,30 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
29
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
30
30
 
31
31
  // src/index.ts
32
- var src_exports = {};
33
- __export(src_exports, {
32
+ var index_exports = {};
33
+ __export(index_exports, {
34
34
  AudioPlayer: () => AudioPlayer,
35
35
  AudioPlayerError: () => AudioPlayerError,
36
36
  AudioPlayerStatus: () => AudioPlayerStatus,
37
37
  AudioReceiveStream: () => AudioReceiveStream,
38
38
  AudioResource: () => AudioResource,
39
+ DAVESession: () => DAVESession,
39
40
  EndBehaviorType: () => EndBehaviorType,
41
+ Networking: () => Networking,
42
+ NetworkingStatusCode: () => NetworkingStatusCode,
40
43
  NoSubscriberBehavior: () => NoSubscriberBehavior,
44
+ Node: () => Node,
41
45
  PlayerSubscription: () => PlayerSubscription,
42
46
  SSRCMap: () => SSRCMap,
43
47
  SpeakingMap: () => SpeakingMap,
44
48
  StreamType: () => StreamType,
49
+ TransformerType: () => TransformerType,
45
50
  VoiceConnection: () => VoiceConnection,
46
51
  VoiceConnectionDisconnectReason: () => VoiceConnectionDisconnectReason,
47
52
  VoiceConnectionStatus: () => VoiceConnectionStatus,
48
53
  VoiceReceiver: () => VoiceReceiver,
54
+ VoiceUDPSocket: () => VoiceUDPSocket,
55
+ VoiceWebSocket: () => VoiceWebSocket,
49
56
  createAudioPlayer: () => createAudioPlayer,
50
57
  createAudioResource: () => createAudioResource,
51
58
  createDefaultAudioReceiveStreamOptions: () => createDefaultAudioReceiveStreamOptions,
@@ -59,10 +66,10 @@ __export(src_exports, {
59
66
  validateDiscordOpusHead: () => validateDiscordOpusHead,
60
67
  version: () => version2
61
68
  });
62
- module.exports = __toCommonJS(src_exports);
69
+ module.exports = __toCommonJS(index_exports);
63
70
 
64
71
  // src/VoiceConnection.ts
65
- var import_node_events7 = require("events");
72
+ var import_node_events8 = require("events");
66
73
 
67
74
  // src/DataStore.ts
68
75
  var import_v10 = require("discord-api-types/v10");
@@ -161,10 +168,10 @@ function deleteAudioPlayer(player) {
161
168
  __name(deleteAudioPlayer, "deleteAudioPlayer");
162
169
 
163
170
  // src/networking/Networking.ts
164
- var import_node_buffer3 = require("buffer");
171
+ var import_node_buffer6 = require("buffer");
165
172
  var import_node_crypto = __toESM(require("crypto"));
166
- var import_node_events3 = require("events");
167
- var import_v42 = require("discord-api-types/voice/v4");
173
+ var import_node_events5 = require("events");
174
+ var import_v82 = require("discord-api-types/voice/v8");
168
175
 
169
176
  // src/util/Secretbox.ts
170
177
  var import_node_buffer = require("buffer");
@@ -182,20 +189,12 @@ var libs = {
182
189
  }, "crypto_aead_xchacha20poly1305_ietf_encrypt")
183
190
  }), "sodium-native"),
184
191
  sodium: /* @__PURE__ */ __name((sodium) => ({
185
- crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => {
186
- return sodium.api.crypto_aead_xchacha20poly1305_ietf_decrypt(cipherText, additionalData, null, nonce2, key);
187
- }, "crypto_aead_xchacha20poly1305_ietf_decrypt"),
188
- crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => {
189
- return sodium.api.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key);
190
- }, "crypto_aead_xchacha20poly1305_ietf_encrypt")
192
+ 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"),
193
+ 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")
191
194
  }), "sodium"),
192
195
  "libsodium-wrappers": /* @__PURE__ */ __name((sodium) => ({
193
- crypto_aead_xchacha20poly1305_ietf_decrypt: /* @__PURE__ */ __name((cipherText, additionalData, nonce2, key) => {
194
- return sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(null, cipherText, additionalData, nonce2, key);
195
- }, "crypto_aead_xchacha20poly1305_ietf_decrypt"),
196
- crypto_aead_xchacha20poly1305_ietf_encrypt: /* @__PURE__ */ __name((plaintext, additionalData, nonce2, key) => {
197
- return sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(plaintext, additionalData, null, nonce2, key);
198
- }, "crypto_aead_xchacha20poly1305_ietf_encrypt")
196
+ 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"),
197
+ 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")
199
198
  }), "libsodium-wrappers"),
200
199
  "@stablelib/xchacha20poly1305": /* @__PURE__ */ __name((stablelib) => ({
201
200
  crypto_aead_xchacha20poly1305_ietf_decrypt(plaintext, additionalData, nonce2, key) {
@@ -253,1094 +252,1608 @@ var secretboxLoadPromise = new Promise(async (resolve2) => {
253
252
  var noop = /* @__PURE__ */ __name(() => {
254
253
  }, "noop");
255
254
 
256
- // src/networking/VoiceUDPSocket.ts
255
+ // src/networking/DAVESession.ts
256
+ var import_node_buffer3 = require("buffer");
257
+ var import_node_events2 = require("events");
258
+
259
+ // src/audio/AudioPlayer.ts
257
260
  var import_node_buffer2 = require("buffer");
258
- var import_node_dgram = require("dgram");
259
261
  var import_node_events = require("events");
260
- var import_node_net = require("net");
261
- function parseLocalPacket(message) {
262
- const packet = import_node_buffer2.Buffer.from(message);
263
- const ip = packet.slice(8, packet.indexOf(0, 8)).toString("utf8");
264
- if (!(0, import_node_net.isIPv4)(ip)) {
265
- throw new Error("Malformed IP address");
266
- }
267
- const port = packet.readUInt16BE(packet.length - 2);
268
- return { ip, port };
269
- }
270
- __name(parseLocalPacket, "parseLocalPacket");
271
- var KEEP_ALIVE_INTERVAL = 5e3;
272
- var MAX_COUNTER_VALUE = 2 ** 32 - 1;
273
- var VoiceUDPSocket = class extends import_node_events.EventEmitter {
262
+
263
+ // src/audio/AudioPlayerError.ts
264
+ var AudioPlayerError = class extends Error {
274
265
  static {
275
- __name(this, "VoiceUDPSocket");
276
- }
277
- /**
278
- * The underlying network Socket for the VoiceUDPSocket.
279
- */
280
- socket;
281
- /**
282
- * The socket details for Discord (remote)
283
- */
284
- remote;
285
- /**
286
- * The counter used in the keep alive mechanism.
287
- */
288
- keepAliveCounter = 0;
289
- /**
290
- * The buffer used to write the keep alive counter into.
291
- */
292
- keepAliveBuffer;
293
- /**
294
- * The Node.js interval for the keep-alive mechanism.
295
- */
296
- keepAliveInterval;
297
- /**
298
- * The time taken to receive a response to keep alive messages.
299
- *
300
- * @deprecated This field is no longer updated as keep alive messages are no longer tracked.
301
- */
302
- ping;
303
- /**
304
- * Creates a new VoiceUDPSocket.
305
- *
306
- * @param remote - Details of the remote socket
307
- */
308
- constructor(remote) {
309
- super();
310
- this.socket = (0, import_node_dgram.createSocket)("udp4");
311
- this.socket.on("error", (error) => this.emit("error", error));
312
- this.socket.on("message", (buffer) => this.onMessage(buffer));
313
- this.socket.on("close", () => this.emit("close"));
314
- this.remote = remote;
315
- this.keepAliveBuffer = import_node_buffer2.Buffer.alloc(8);
316
- this.keepAliveInterval = setInterval(() => this.keepAlive(), KEEP_ALIVE_INTERVAL);
317
- setImmediate(() => this.keepAlive());
266
+ __name(this, "AudioPlayerError");
318
267
  }
319
268
  /**
320
- * Called when a message is received on the UDP socket.
321
- *
322
- * @param buffer - The received buffer
269
+ * The resource associated with the audio player at the time the error was thrown.
323
270
  */
324
- onMessage(buffer) {
325
- this.emit("message", buffer);
271
+ resource;
272
+ constructor(error, resource) {
273
+ super(error.message);
274
+ this.resource = resource;
275
+ this.name = error.name;
276
+ this.stack = error.stack;
326
277
  }
327
- /**
328
- * Called at a regular interval to check whether we are still able to send datagrams to Discord.
329
- */
330
- keepAlive() {
331
- this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0);
332
- this.send(this.keepAliveBuffer);
333
- this.keepAliveCounter++;
334
- if (this.keepAliveCounter > MAX_COUNTER_VALUE) {
335
- this.keepAliveCounter = 0;
336
- }
278
+ };
279
+
280
+ // src/audio/PlayerSubscription.ts
281
+ var PlayerSubscription = class {
282
+ static {
283
+ __name(this, "PlayerSubscription");
337
284
  }
338
285
  /**
339
- * Sends a buffer to Discord.
340
- *
341
- * @param buffer - The buffer to send
286
+ * The voice connection of this subscription.
342
287
  */
343
- send(buffer) {
344
- this.socket.send(buffer, this.remote.port, this.remote.ip);
345
- }
288
+ connection;
346
289
  /**
347
- * Closes the socket, the instance will not be able to be reused.
290
+ * The audio player of this subscription.
348
291
  */
349
- destroy() {
350
- try {
351
- this.socket.close();
352
- } catch {
353
- }
354
- clearInterval(this.keepAliveInterval);
292
+ player;
293
+ constructor(connection, player) {
294
+ this.connection = connection;
295
+ this.player = player;
355
296
  }
356
297
  /**
357
- * Performs IP discovery to discover the local address and port to be used for the voice connection.
358
- *
359
- * @param ssrc - The SSRC received from Discord
298
+ * Unsubscribes the connection from the audio player, meaning that the
299
+ * audio player cannot stream audio to it until a new subscription is made.
360
300
  */
361
- async performIPDiscovery(ssrc) {
362
- return new Promise((resolve2, reject) => {
363
- const listener = /* @__PURE__ */ __name((message) => {
364
- try {
365
- if (message.readUInt16BE(0) !== 2) return;
366
- const packet = parseLocalPacket(message);
367
- this.socket.off("message", listener);
368
- resolve2(packet);
369
- } catch {
370
- }
371
- }, "listener");
372
- this.socket.on("message", listener);
373
- this.socket.once("close", () => reject(new Error("Cannot perform IP discovery - socket closed")));
374
- const discoveryBuffer = import_node_buffer2.Buffer.alloc(74);
375
- discoveryBuffer.writeUInt16BE(1, 0);
376
- discoveryBuffer.writeUInt16BE(70, 2);
377
- discoveryBuffer.writeUInt32BE(ssrc, 4);
378
- this.send(discoveryBuffer);
379
- });
301
+ unsubscribe() {
302
+ this.connection["onSubscriptionRemoved"](this);
303
+ this.player["unsubscribe"](this);
380
304
  }
381
305
  };
382
306
 
383
- // src/networking/VoiceWebSocket.ts
384
- var import_node_events2 = require("events");
385
- var import_v4 = require("discord-api-types/voice/v4");
386
- var import_ws = __toESM(require("ws"));
387
- var VoiceWebSocket = class extends import_node_events2.EventEmitter {
307
+ // src/audio/AudioPlayer.ts
308
+ var SILENCE_FRAME = import_node_buffer2.Buffer.from([248, 255, 254]);
309
+ var NoSubscriberBehavior = /* @__PURE__ */ ((NoSubscriberBehavior2) => {
310
+ NoSubscriberBehavior2["Pause"] = "pause";
311
+ NoSubscriberBehavior2["Play"] = "play";
312
+ NoSubscriberBehavior2["Stop"] = "stop";
313
+ return NoSubscriberBehavior2;
314
+ })(NoSubscriberBehavior || {});
315
+ var AudioPlayerStatus = /* @__PURE__ */ ((AudioPlayerStatus2) => {
316
+ AudioPlayerStatus2["AutoPaused"] = "autopaused";
317
+ AudioPlayerStatus2["Buffering"] = "buffering";
318
+ AudioPlayerStatus2["Idle"] = "idle";
319
+ AudioPlayerStatus2["Paused"] = "paused";
320
+ AudioPlayerStatus2["Playing"] = "playing";
321
+ return AudioPlayerStatus2;
322
+ })(AudioPlayerStatus || {});
323
+ function stringifyState(state) {
324
+ return JSON.stringify({
325
+ ...state,
326
+ resource: Reflect.has(state, "resource"),
327
+ stepTimeout: Reflect.has(state, "stepTimeout")
328
+ });
329
+ }
330
+ __name(stringifyState, "stringifyState");
331
+ var AudioPlayer = class extends import_node_events.EventEmitter {
388
332
  static {
389
- __name(this, "VoiceWebSocket");
333
+ __name(this, "AudioPlayer");
390
334
  }
391
335
  /**
392
- * The current heartbeat interval, if any.
393
- */
394
- heartbeatInterval;
395
- /**
396
- * The time (milliseconds since UNIX epoch) that the last heartbeat acknowledgement packet was received.
397
- * This is set to 0 if an acknowledgement packet hasn't been received yet.
398
- */
399
- lastHeartbeatAck;
400
- /**
401
- * The time (milliseconds since UNIX epoch) that the last heartbeat was sent. This is set to 0 if a heartbeat
402
- * hasn't been sent yet.
336
+ * The state that the AudioPlayer is in.
403
337
  */
404
- lastHeartbeatSend;
338
+ _state;
405
339
  /**
406
- * The number of consecutively missed heartbeats.
340
+ * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio
341
+ * to the streams in this list.
407
342
  */
408
- missedHeartbeats = 0;
343
+ subscribers = [];
409
344
  /**
410
- * The last recorded ping.
345
+ * The behavior that the player should follow when it enters certain situations.
411
346
  */
412
- ping;
347
+ behaviors;
413
348
  /**
414
349
  * The debug logger function, if debugging is enabled.
415
350
  */
416
351
  debug;
417
352
  /**
418
- * The underlying WebSocket of this wrapper.
419
- */
420
- ws;
421
- /**
422
- * Creates a new VoiceWebSocket.
423
- *
424
- * @param address - The address to connect to
353
+ * Creates a new AudioPlayer.
425
354
  */
426
- constructor(address, debug) {
355
+ constructor(options = {}) {
427
356
  super();
428
- this.ws = new import_ws.default(address);
429
- this.ws.onmessage = (err) => this.onMessage(err);
430
- this.ws.onopen = (err) => this.emit("open", err);
431
- this.ws.onerror = (err) => this.emit("error", err instanceof Error ? err : err.error);
432
- this.ws.onclose = (err) => this.emit("close", err);
433
- this.lastHeartbeatAck = 0;
434
- this.lastHeartbeatSend = 0;
435
- this.debug = debug ? (message) => this.emit("debug", message) : null;
357
+ this._state = { status: "idle" /* Idle */ };
358
+ this.behaviors = {
359
+ noSubscriber: "pause" /* Pause */,
360
+ maxMissedFrames: 5,
361
+ ...options.behaviors
362
+ };
363
+ this.debug = options.debug === false ? null : (message) => this.emit("debug", message);
436
364
  }
437
365
  /**
438
- * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed.
366
+ * A list of subscribed voice connections that can currently receive audio to play.
439
367
  */
440
- destroy() {
441
- try {
442
- this.debug?.("destroyed");
443
- this.setHeartbeatInterval(-1);
444
- this.ws.close(1e3);
445
- } catch (error) {
446
- const err = error;
447
- this.emit("error", err);
448
- }
368
+ get playable() {
369
+ return this.subscribers.filter(({ connection }) => connection.state.status === "ready" /* Ready */).map(({ connection }) => connection);
449
370
  }
450
371
  /**
451
- * Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them
452
- * as packets.
372
+ * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed,
373
+ * then the existing subscription is used.
453
374
  *
454
- * @param event - The message event
375
+ * @remarks
376
+ * This method should not be directly called. Instead, use VoiceConnection#subscribe.
377
+ * @param connection - The connection to subscribe
378
+ * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription
455
379
  */
456
- onMessage(event) {
457
- if (typeof event.data !== "string") return;
458
- this.debug?.(`<< ${event.data}`);
459
- let packet;
460
- try {
461
- packet = JSON.parse(event.data);
462
- } catch (error) {
463
- const err = error;
464
- this.emit("error", err);
380
+ // @ts-ignore
381
+ subscribe(connection) {
382
+ const existingSubscription = this.subscribers.find((subscription) => subscription.connection === connection);
383
+ if (!existingSubscription) {
384
+ const subscription = new PlayerSubscription(connection, this);
385
+ this.subscribers.push(subscription);
386
+ setImmediate(() => this.emit("subscribe", subscription));
387
+ return subscription;
388
+ }
389
+ return existingSubscription;
390
+ }
391
+ /**
392
+ * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player.
393
+ *
394
+ * @remarks
395
+ * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe.
396
+ * @param subscription - The subscription to remove
397
+ * @returns Whether or not the subscription existed on the player and was removed
398
+ */
399
+ // @ts-ignore
400
+ unsubscribe(subscription) {
401
+ const index = this.subscribers.indexOf(subscription);
402
+ const exists = index !== -1;
403
+ if (exists) {
404
+ this.subscribers.splice(index, 1);
405
+ subscription.connection.setSpeaking(false);
406
+ this.emit("unsubscribe", subscription);
407
+ }
408
+ return exists;
409
+ }
410
+ /**
411
+ * The state that the player is in.
412
+ *
413
+ * @remarks
414
+ * The setter will perform clean-up operations where necessary.
415
+ */
416
+ get state() {
417
+ return this._state;
418
+ }
419
+ set state(newState) {
420
+ const oldState = this._state;
421
+ const newResource = Reflect.get(newState, "resource");
422
+ if (oldState.status !== "idle" /* Idle */ && oldState.resource !== newResource) {
423
+ oldState.resource.playStream.on("error", noop);
424
+ oldState.resource.playStream.off("error", oldState.onStreamError);
425
+ oldState.resource.audioPlayer = void 0;
426
+ oldState.resource.playStream.destroy();
427
+ oldState.resource.playStream.read();
428
+ }
429
+ if (oldState.status === "buffering" /* Buffering */ && (newState.status !== "buffering" /* Buffering */ || newState.resource !== oldState.resource)) {
430
+ oldState.resource.playStream.off("end", oldState.onFailureCallback);
431
+ oldState.resource.playStream.off("close", oldState.onFailureCallback);
432
+ oldState.resource.playStream.off("finish", oldState.onFailureCallback);
433
+ oldState.resource.playStream.off("readable", oldState.onReadableCallback);
434
+ }
435
+ if (newState.status === "idle" /* Idle */) {
436
+ this._signalStopSpeaking();
437
+ deleteAudioPlayer(this);
438
+ }
439
+ if (newResource) {
440
+ addAudioPlayer(this);
441
+ }
442
+ const didChangeResources = oldState.status !== "idle" /* Idle */ && newState.status === "playing" /* Playing */ && oldState.resource !== newState.resource;
443
+ this._state = newState;
444
+ this.emit("stateChange", oldState, this._state);
445
+ if (oldState.status !== newState.status || didChangeResources) {
446
+ this.emit(newState.status, oldState, this._state);
447
+ }
448
+ this.debug?.(`state change:
449
+ from ${stringifyState(oldState)}
450
+ to ${stringifyState(newState)}`);
451
+ }
452
+ /**
453
+ * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed
454
+ * (it cannot be reused, even in another player) and is replaced with the new resource.
455
+ *
456
+ * @remarks
457
+ * The player will transition to the Playing state once playback begins, and will return to the Idle state once
458
+ * playback is ended.
459
+ *
460
+ * If the player was previously playing a resource and this method is called, the player will not transition to the
461
+ * Idle state during the swap over.
462
+ * @param resource - The resource to play
463
+ * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player
464
+ */
465
+ play(resource) {
466
+ if (resource.ended) {
467
+ throw new Error("Cannot play a resource that has already ended.");
468
+ }
469
+ if (resource.audioPlayer) {
470
+ if (resource.audioPlayer === this) {
471
+ return;
472
+ }
473
+ throw new Error("Resource is already being played by another audio player.");
474
+ }
475
+ resource.audioPlayer = this;
476
+ const onStreamError = /* @__PURE__ */ __name((error) => {
477
+ if (this.state.status !== "idle" /* Idle */) {
478
+ this.emit("error", new AudioPlayerError(error, this.state.resource));
479
+ }
480
+ if (this.state.status !== "idle" /* Idle */ && this.state.resource === resource) {
481
+ this.state = {
482
+ status: "idle" /* Idle */
483
+ };
484
+ }
485
+ }, "onStreamError");
486
+ resource.playStream.once("error", onStreamError);
487
+ if (resource.started) {
488
+ this.state = {
489
+ status: "playing" /* Playing */,
490
+ missedFrames: 0,
491
+ playbackDuration: 0,
492
+ resource,
493
+ onStreamError
494
+ };
495
+ } else {
496
+ const onReadableCallback = /* @__PURE__ */ __name(() => {
497
+ if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) {
498
+ this.state = {
499
+ status: "playing" /* Playing */,
500
+ missedFrames: 0,
501
+ playbackDuration: 0,
502
+ resource,
503
+ onStreamError
504
+ };
505
+ }
506
+ }, "onReadableCallback");
507
+ const onFailureCallback = /* @__PURE__ */ __name(() => {
508
+ if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) {
509
+ this.state = {
510
+ status: "idle" /* Idle */
511
+ };
512
+ }
513
+ }, "onFailureCallback");
514
+ resource.playStream.once("readable", onReadableCallback);
515
+ resource.playStream.once("end", onFailureCallback);
516
+ resource.playStream.once("close", onFailureCallback);
517
+ resource.playStream.once("finish", onFailureCallback);
518
+ this.state = {
519
+ status: "buffering" /* Buffering */,
520
+ resource,
521
+ onReadableCallback,
522
+ onFailureCallback,
523
+ onStreamError
524
+ };
525
+ }
526
+ }
527
+ /**
528
+ * Pauses playback of the current resource, if any.
529
+ *
530
+ * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches
531
+ * @returns `true` if the player was successfully paused, otherwise `false`
532
+ */
533
+ pause(interpolateSilence = true) {
534
+ if (this.state.status !== "playing" /* Playing */) return false;
535
+ this.state = {
536
+ ...this.state,
537
+ status: "paused" /* Paused */,
538
+ silencePacketsRemaining: interpolateSilence ? 5 : 0
539
+ };
540
+ return true;
541
+ }
542
+ /**
543
+ * Unpauses playback of the current resource, if any.
544
+ *
545
+ * @returns `true` if the player was successfully unpaused, otherwise `false`
546
+ */
547
+ unpause() {
548
+ if (this.state.status !== "paused" /* Paused */) return false;
549
+ this.state = {
550
+ ...this.state,
551
+ status: "playing" /* Playing */,
552
+ missedFrames: 0
553
+ };
554
+ return true;
555
+ }
556
+ /**
557
+ * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state,
558
+ * or remain in its current state until the silence padding frames of the resource have been played.
559
+ *
560
+ * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames
561
+ * @returns `true` if the player will come to a stop, otherwise `false`
562
+ */
563
+ stop(force = false) {
564
+ if (this.state.status === "idle" /* Idle */) return false;
565
+ if (force || this.state.resource.silencePaddingFrames === 0) {
566
+ this.state = {
567
+ status: "idle" /* Idle */
568
+ };
569
+ } else if (this.state.resource.silenceRemaining === -1) {
570
+ this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;
571
+ }
572
+ return true;
573
+ }
574
+ /**
575
+ * Checks whether the underlying resource (if any) is playable (readable)
576
+ *
577
+ * @returns `true` if the resource is playable, otherwise `false`
578
+ */
579
+ checkPlayable() {
580
+ const state = this._state;
581
+ if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return false;
582
+ if (!state.resource.readable) {
583
+ this.state = {
584
+ status: "idle" /* Idle */
585
+ };
586
+ return false;
587
+ }
588
+ return true;
589
+ }
590
+ /**
591
+ * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered
592
+ * by the active connections of this audio player.
593
+ */
594
+ // @ts-ignore
595
+ _stepDispatch() {
596
+ const state = this._state;
597
+ if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return;
598
+ for (const connection of this.playable) {
599
+ connection.dispatchAudio();
600
+ }
601
+ }
602
+ /**
603
+ * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the
604
+ * underlying resource of the stream, and then has all the active connections of the audio player prepare it
605
+ * (encrypt it, append header data) so that it is ready to play at the start of the next cycle.
606
+ */
607
+ // @ts-ignore
608
+ _stepPrepare() {
609
+ const state = this._state;
610
+ if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return;
611
+ const playable = this.playable;
612
+ if (state.status === "autopaused" /* AutoPaused */ && playable.length > 0) {
613
+ this.state = {
614
+ ...state,
615
+ status: "playing" /* Playing */,
616
+ missedFrames: 0
617
+ };
618
+ }
619
+ if (state.status === "paused" /* Paused */ || state.status === "autopaused" /* AutoPaused */) {
620
+ if (state.silencePacketsRemaining > 0) {
621
+ state.silencePacketsRemaining--;
622
+ this._preparePacket(SILENCE_FRAME, playable, state);
623
+ if (state.silencePacketsRemaining === 0) {
624
+ this._signalStopSpeaking();
625
+ }
626
+ }
627
+ return;
628
+ }
629
+ if (playable.length === 0) {
630
+ if (this.behaviors.noSubscriber === "pause" /* Pause */) {
631
+ this.state = {
632
+ ...state,
633
+ status: "autopaused" /* AutoPaused */,
634
+ silencePacketsRemaining: 5
635
+ };
636
+ return;
637
+ } else if (this.behaviors.noSubscriber === "stop" /* Stop */) {
638
+ this.stop(true);
639
+ }
640
+ }
641
+ const packet = state.resource.read();
642
+ if (state.status === "playing" /* Playing */) {
643
+ if (packet) {
644
+ this._preparePacket(packet, playable, state);
645
+ state.missedFrames = 0;
646
+ } else {
647
+ this._preparePacket(SILENCE_FRAME, playable, state);
648
+ state.missedFrames++;
649
+ if (state.missedFrames >= this.behaviors.maxMissedFrames) {
650
+ this.stop();
651
+ }
652
+ }
653
+ }
654
+ }
655
+ /**
656
+ * Signals to all the subscribed connections that they should send a packet to Discord indicating
657
+ * they are no longer speaking. Called once playback of a resource ends.
658
+ */
659
+ _signalStopSpeaking() {
660
+ for (const { connection } of this.subscribers) {
661
+ connection.setSpeaking(false);
662
+ }
663
+ }
664
+ /**
665
+ * Instructs the given connections to each prepare this packet to be played at the start of the
666
+ * next cycle.
667
+ *
668
+ * @param packet - The Opus packet to be prepared by each receiver
669
+ * @param receivers - The connections that should play this packet
670
+ */
671
+ _preparePacket(packet, receivers, state) {
672
+ state.playbackDuration += 20;
673
+ for (const connection of receivers) {
674
+ connection.prepareAudioPacket(packet);
675
+ }
676
+ }
677
+ };
678
+ function createAudioPlayer(options) {
679
+ return new AudioPlayer(options);
680
+ }
681
+ __name(createAudioPlayer, "createAudioPlayer");
682
+
683
+ // src/networking/DAVESession.ts
684
+ var Davey = null;
685
+ var TRANSITION_EXPIRY = 10;
686
+ var TRANSITION_EXPIRY_PENDING_DOWNGRADE = 24;
687
+ var DEFAULT_DECRYPTION_FAILURE_TOLERANCE = 36;
688
+ var daveLoadPromise = new Promise(async (resolve2) => {
689
+ try {
690
+ const lib = await import("@snazzah/davey");
691
+ Davey = lib;
692
+ } catch {
693
+ }
694
+ resolve2();
695
+ });
696
+ function getMaxProtocolVersion() {
697
+ return Davey?.DAVE_PROTOCOL_VERSION;
698
+ }
699
+ __name(getMaxProtocolVersion, "getMaxProtocolVersion");
700
+ var DAVESession = class extends import_node_events2.EventEmitter {
701
+ static {
702
+ __name(this, "DAVESession");
703
+ }
704
+ /**
705
+ * The channel id represented by this session.
706
+ */
707
+ channelId;
708
+ /**
709
+ * The user id represented by this session.
710
+ */
711
+ userId;
712
+ /**
713
+ * The protocol version being used.
714
+ */
715
+ protocolVersion;
716
+ /**
717
+ * The last transition id executed.
718
+ */
719
+ lastTransitionId;
720
+ /**
721
+ * The pending transition.
722
+ */
723
+ pendingTransition;
724
+ /**
725
+ * Whether this session was downgraded previously.
726
+ */
727
+ downgraded = false;
728
+ /**
729
+ * The amount of consecutive failures encountered when decrypting.
730
+ */
731
+ consecutiveFailures = 0;
732
+ /**
733
+ * The amount of consecutive failures needed to attempt to recover.
734
+ */
735
+ failureTolerance;
736
+ /**
737
+ * Whether this session is currently re-initializing due to an invalid transition.
738
+ */
739
+ reinitializing = false;
740
+ /**
741
+ * The underlying DAVE Session of this wrapper.
742
+ */
743
+ session;
744
+ constructor(protocolVersion, userId, channelId, options) {
745
+ if (Davey === null)
746
+ throw new Error(
747
+ `Cannot utilize the DAVE protocol as the @snazzah/davey package has not been installed.
748
+ - Use the generateDependencyReport() function for more information.
749
+ `
750
+ );
751
+ super();
752
+ this.protocolVersion = protocolVersion;
753
+ this.userId = userId;
754
+ this.channelId = channelId;
755
+ this.failureTolerance = options.decryptionFailureTolerance ?? DEFAULT_DECRYPTION_FAILURE_TOLERANCE;
756
+ }
757
+ /**
758
+ * The current voice privacy code of the session. Will be `null` if there is no session.
759
+ */
760
+ get voicePrivacyCode() {
761
+ if (this.protocolVersion === 0 || !this.session?.voicePrivacyCode) {
762
+ return null;
763
+ }
764
+ return this.session.voicePrivacyCode;
765
+ }
766
+ /**
767
+ * Gets the verification code for a user in the session.
768
+ *
769
+ * @throws Will throw if there is not an active session or the user id provided is invalid or not in the session.
770
+ */
771
+ async getVerificationCode(userId) {
772
+ if (!this.session) throw new Error("Session not available");
773
+ return this.session.getVerificationCode(userId);
774
+ }
775
+ /**
776
+ * Re-initializes (or initializes) the underlying session.
777
+ */
778
+ reinit() {
779
+ if (this.protocolVersion > 0) {
780
+ if (this.session) {
781
+ this.session.reinit(this.protocolVersion, this.userId, this.channelId);
782
+ this.emit("debug", `Session reinitialized for protocol version ${this.protocolVersion}`);
783
+ } else {
784
+ this.session = new Davey.DAVESession(this.protocolVersion, this.userId, this.channelId);
785
+ this.emit("debug", `Session initialized for protocol version ${this.protocolVersion}`);
786
+ }
787
+ this.emit("keyPackage", this.session.getSerializedKeyPackage());
788
+ } else if (this.session) {
789
+ this.session.reset();
790
+ this.session.setPassthroughMode(true, TRANSITION_EXPIRY);
791
+ this.emit("debug", "Session reset");
792
+ }
793
+ }
794
+ /**
795
+ * Set the external sender for this session.
796
+ *
797
+ * @param externalSender - The external sender
798
+ */
799
+ setExternalSender(externalSender) {
800
+ if (!this.session) throw new Error("No session available");
801
+ this.session.setExternalSender(externalSender);
802
+ this.emit("debug", "Set MLS external sender");
803
+ }
804
+ /**
805
+ * Prepare for a transition.
806
+ *
807
+ * @param data - The transition data
808
+ * @returns Whether we should signal to the voice server that we are ready
809
+ */
810
+ prepareTransition(data) {
811
+ this.emit("debug", `Preparing for transition (${data.transition_id}, v${data.protocol_version})`);
812
+ this.pendingTransition = data;
813
+ if (data.transition_id === 0) {
814
+ this.executeTransition(data.transition_id);
815
+ } else {
816
+ if (data.protocol_version === 0) this.session?.setPassthroughMode(true, TRANSITION_EXPIRY_PENDING_DOWNGRADE);
817
+ return true;
818
+ }
819
+ return false;
820
+ }
821
+ /**
822
+ * Execute a transition.
823
+ *
824
+ * @param transitionId - The transition id to execute on
825
+ */
826
+ executeTransition(transitionId) {
827
+ this.emit("debug", `Executing transition (${transitionId})`);
828
+ if (!this.pendingTransition) {
829
+ this.emit("debug", `Received execute transition, but we don't have a pending transition for ${transitionId}`);
465
830
  return;
466
831
  }
467
- if (packet.op === import_v4.VoiceOpcodes.HeartbeatAck) {
468
- this.lastHeartbeatAck = Date.now();
469
- this.missedHeartbeats = 0;
470
- this.ping = this.lastHeartbeatAck - this.lastHeartbeatSend;
832
+ let transitioned = false;
833
+ if (transitionId === this.pendingTransition.transition_id) {
834
+ const oldVersion = this.protocolVersion;
835
+ this.protocolVersion = this.pendingTransition.protocol_version;
836
+ if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
837
+ this.downgraded = true;
838
+ this.emit("debug", "Session downgraded");
839
+ } else if (transitionId > 0 && this.downgraded) {
840
+ this.downgraded = false;
841
+ this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
842
+ this.emit("debug", "Session upgraded");
843
+ }
844
+ transitioned = true;
845
+ this.reinitializing = false;
846
+ this.lastTransitionId = transitionId;
847
+ this.emit("debug", `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
848
+ } else {
849
+ this.emit(
850
+ "debug",
851
+ `Received execute transition for an unexpected transition id (expected: ${this.pendingTransition.transition_id}, actual: ${transitionId})`
852
+ );
471
853
  }
472
- this.emit("packet", packet);
854
+ this.pendingTransition = void 0;
855
+ return transitioned;
473
856
  }
474
857
  /**
475
- * Sends a JSON-stringifiable packet over the WebSocket.
858
+ * Prepare for a new epoch.
476
859
  *
477
- * @param packet - The packet to send
860
+ * @param data - The epoch data
478
861
  */
479
- sendPacket(packet) {
862
+ prepareEpoch(data) {
863
+ this.emit("debug", `Preparing for epoch (${data.epoch})`);
864
+ if (data.epoch === 1) {
865
+ this.protocolVersion = data.protocol_version;
866
+ this.reinit();
867
+ }
868
+ }
869
+ /**
870
+ * Recover from an invalid transition by re-initializing.
871
+ *
872
+ * @param transitionId - The transition id to invalidate
873
+ */
874
+ recoverFromInvalidTransition(transitionId) {
875
+ if (this.reinitializing) return;
876
+ this.emit("debug", `Invalidating transition ${transitionId}`);
877
+ this.reinitializing = true;
878
+ this.consecutiveFailures = 0;
879
+ this.emit("invalidateTransition", transitionId);
880
+ this.reinit();
881
+ }
882
+ /**
883
+ * Processes proposals from the MLS group.
884
+ *
885
+ * @param payload - The binary message payload
886
+ * @param connectedClients - The set of connected client IDs
887
+ * @returns The payload to send back to the voice server, if there is one
888
+ */
889
+ processProposals(payload, connectedClients) {
890
+ if (!this.session) throw new Error("No session available");
891
+ const optype = payload.readUInt8(0);
892
+ const { commit, welcome } = this.session.processProposals(
893
+ optype,
894
+ payload.subarray(1),
895
+ Array.from(connectedClients)
896
+ );
897
+ this.emit("debug", "MLS proposals processed");
898
+ if (!commit) return;
899
+ return welcome ? import_node_buffer3.Buffer.concat([commit, welcome]) : commit;
900
+ }
901
+ /**
902
+ * Processes a commit from the MLS group.
903
+ *
904
+ * @param payload - The payload
905
+ * @returns The transaction id and whether it was successful
906
+ */
907
+ processCommit(payload) {
908
+ if (!this.session) throw new Error("No session available");
909
+ const transitionId = payload.readUInt16BE(0);
480
910
  try {
481
- const stringified = JSON.stringify(packet);
482
- this.debug?.(`>> ${stringified}`);
483
- this.ws.send(stringified);
911
+ this.session.processCommit(payload.subarray(2));
912
+ if (transitionId === 0) {
913
+ this.reinitializing = false;
914
+ this.lastTransitionId = transitionId;
915
+ } else {
916
+ this.pendingTransition = { transition_id: transitionId, protocol_version: this.protocolVersion };
917
+ }
918
+ this.emit("debug", `MLS commit processed (transition id: ${transitionId})`);
919
+ return { transitionId, success: true };
484
920
  } catch (error) {
485
- const err = error;
486
- this.emit("error", err);
921
+ this.emit("debug", `MLS commit errored from transition ${transitionId}: ${error}`);
922
+ this.recoverFromInvalidTransition(transitionId);
923
+ return { transitionId, success: false };
487
924
  }
488
925
  }
489
926
  /**
490
- * Sends a heartbeat over the WebSocket.
927
+ * Processes a welcome from the MLS group.
928
+ *
929
+ * @param payload - The payload
930
+ * @returns The transaction id and whether it was successful
491
931
  */
492
- sendHeartbeat() {
493
- this.lastHeartbeatSend = Date.now();
494
- this.missedHeartbeats++;
495
- const nonce2 = this.lastHeartbeatSend;
496
- this.sendPacket({
497
- op: import_v4.VoiceOpcodes.Heartbeat,
498
- // eslint-disable-next-line id-length
499
- d: nonce2
500
- });
932
+ processWelcome(payload) {
933
+ if (!this.session) throw new Error("No session available");
934
+ const transitionId = payload.readUInt16BE(0);
935
+ try {
936
+ this.session.processWelcome(payload.subarray(2));
937
+ if (transitionId === 0) {
938
+ this.reinitializing = false;
939
+ this.lastTransitionId = transitionId;
940
+ } else {
941
+ this.pendingTransition = { transition_id: transitionId, protocol_version: this.protocolVersion };
942
+ }
943
+ this.emit("debug", `MLS welcome processed (transition id: ${transitionId})`);
944
+ return { transitionId, success: true };
945
+ } catch (error) {
946
+ this.emit("debug", `MLS welcome errored from transition ${transitionId}: ${error}`);
947
+ this.recoverFromInvalidTransition(transitionId);
948
+ return { transitionId, success: false };
949
+ }
501
950
  }
502
951
  /**
503
- * Sets/clears an interval to send heartbeats over the WebSocket.
952
+ * Encrypt a packet using end-to-end encryption.
504
953
  *
505
- * @param ms - The interval in milliseconds. If negative, the interval will be unset
954
+ * @param packet - The packet to encrypt
506
955
  */
507
- setHeartbeatInterval(ms) {
508
- if (this.heartbeatInterval !== void 0) clearInterval(this.heartbeatInterval);
509
- if (ms > 0) {
510
- this.heartbeatInterval = setInterval(() => {
511
- if (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) {
512
- this.ws.close();
513
- this.setHeartbeatInterval(-1);
956
+ encrypt(packet) {
957
+ if (this.protocolVersion === 0 || !this.session?.ready || packet.equals(SILENCE_FRAME)) return packet;
958
+ return this.session.encryptOpus(packet);
959
+ }
960
+ /**
961
+ * Decrypt a packet using end-to-end encryption.
962
+ *
963
+ * @param packet - The packet to decrypt
964
+ * @param userId - The user id that sent the packet
965
+ * @returns The decrypted packet, or `null` if the decryption failed but should be ignored
966
+ */
967
+ decrypt(packet, userId) {
968
+ const canDecrypt = this.session?.ready && (this.protocolVersion !== 0 || this.session?.canPassthrough(userId));
969
+ if (packet.equals(SILENCE_FRAME) || !canDecrypt || !this.session) return packet;
970
+ try {
971
+ const buffer = this.session.decrypt(userId, Davey.MediaType.AUDIO, packet);
972
+ this.consecutiveFailures = 0;
973
+ return buffer;
974
+ } catch (error) {
975
+ if (!this.reinitializing && !this.pendingTransition) {
976
+ this.consecutiveFailures++;
977
+ this.emit("debug", `Failed to decrypt a packet (${this.consecutiveFailures} consecutive fails)`);
978
+ if (this.consecutiveFailures > this.failureTolerance) {
979
+ if (this.lastTransitionId) this.recoverFromInvalidTransition(this.lastTransitionId);
980
+ else throw error;
514
981
  }
515
- this.sendHeartbeat();
516
- }, ms);
982
+ } else if (this.reinitializing) {
983
+ this.emit("debug", "Failed to decrypt a packet (reinitializing session)");
984
+ } else if (this.pendingTransition) {
985
+ this.emit(
986
+ "debug",
987
+ `Failed to decrypt a packet (pending transition ${this.pendingTransition.transition_id} to v${this.pendingTransition.protocol_version})`
988
+ );
989
+ }
990
+ }
991
+ return null;
992
+ }
993
+ /**
994
+ * Resets the session.
995
+ */
996
+ destroy() {
997
+ try {
998
+ this.session?.reset();
999
+ } catch {
517
1000
  }
518
1001
  }
519
1002
  };
520
1003
 
521
- // src/networking/Networking.ts
522
- var CHANNELS = 2;
523
- var TIMESTAMP_INC = 48e3 / 100 * CHANNELS;
524
- var MAX_NONCE_SIZE = 2 ** 32 - 1;
525
- var SUPPORTED_ENCRYPTION_MODES = ["aead_xchacha20_poly1305_rtpsize"];
526
- if (import_node_crypto.default.getCiphers().includes("aes-256-gcm")) {
527
- SUPPORTED_ENCRYPTION_MODES.unshift("aead_aes256_gcm_rtpsize");
528
- }
529
- var nonce = import_node_buffer3.Buffer.alloc(24);
530
- function stringifyState(state) {
531
- return JSON.stringify({
532
- ...state,
533
- ws: Reflect.has(state, "ws"),
534
- udp: Reflect.has(state, "udp")
535
- });
536
- }
537
- __name(stringifyState, "stringifyState");
538
- function chooseEncryptionMode(options) {
539
- const option = options.find((option2) => SUPPORTED_ENCRYPTION_MODES.includes(option2));
540
- if (!option) {
541
- throw new Error(`No compatible encryption modes. Available include: ${options.join(", ")}`);
1004
+ // src/networking/VoiceUDPSocket.ts
1005
+ var import_node_buffer4 = require("buffer");
1006
+ var import_node_dgram = require("dgram");
1007
+ var import_node_events3 = require("events");
1008
+ var import_node_net = require("net");
1009
+ function parseLocalPacket(message) {
1010
+ const packet = import_node_buffer4.Buffer.from(message);
1011
+ const ip = packet.slice(8, packet.indexOf(0, 8)).toString("utf8");
1012
+ if (!(0, import_node_net.isIPv4)(ip)) {
1013
+ throw new Error("Malformed IP address");
542
1014
  }
543
- return option;
544
- }
545
- __name(chooseEncryptionMode, "chooseEncryptionMode");
546
- function randomNBit(numberOfBits) {
547
- return Math.floor(Math.random() * 2 ** numberOfBits);
1015
+ const port = packet.readUInt16BE(packet.length - 2);
1016
+ return { ip, port };
548
1017
  }
549
- __name(randomNBit, "randomNBit");
550
- var Networking = class extends import_node_events3.EventEmitter {
1018
+ __name(parseLocalPacket, "parseLocalPacket");
1019
+ var KEEP_ALIVE_INTERVAL = 5e3;
1020
+ var MAX_COUNTER_VALUE = 2 ** 32 - 1;
1021
+ var VoiceUDPSocket = class extends import_node_events3.EventEmitter {
551
1022
  static {
552
- __name(this, "Networking");
1023
+ __name(this, "VoiceUDPSocket");
553
1024
  }
554
- _state;
555
1025
  /**
556
- * The debug logger function, if debugging is enabled.
1026
+ * The underlying network Socket for the VoiceUDPSocket.
557
1027
  */
558
- debug;
1028
+ socket;
559
1029
  /**
560
- * Creates a new Networking instance.
1030
+ * The socket details for Discord (remote)
561
1031
  */
562
- constructor(options, debug) {
563
- super();
564
- this.onWsOpen = this.onWsOpen.bind(this);
565
- this.onChildError = this.onChildError.bind(this);
566
- this.onWsPacket = this.onWsPacket.bind(this);
567
- this.onWsClose = this.onWsClose.bind(this);
568
- this.onWsDebug = this.onWsDebug.bind(this);
569
- this.onUdpDebug = this.onUdpDebug.bind(this);
570
- this.onUdpClose = this.onUdpClose.bind(this);
571
- this.debug = debug ? (message) => this.emit("debug", message) : null;
572
- this._state = {
573
- code: 0 /* OpeningWs */,
574
- ws: this.createWebSocket(options.endpoint),
575
- connectionOptions: options
576
- };
577
- }
1032
+ remote;
578
1033
  /**
579
- * Destroys the Networking instance, transitioning it into the Closed state.
1034
+ * The counter used in the keep alive mechanism.
580
1035
  */
581
- destroy() {
582
- this.state = {
583
- code: 6 /* Closed */
584
- };
585
- }
1036
+ keepAliveCounter = 0;
586
1037
  /**
587
- * The current state of the networking instance.
1038
+ * The buffer used to write the keep alive counter into.
588
1039
  */
589
- get state() {
590
- return this._state;
591
- }
1040
+ keepAliveBuffer;
592
1041
  /**
593
- * Sets a new state for the networking instance, performing clean-up operations where necessary.
1042
+ * The Node.js interval for the keep-alive mechanism.
594
1043
  */
595
- set state(newState) {
596
- const oldWs = Reflect.get(this._state, "ws");
597
- const newWs = Reflect.get(newState, "ws");
598
- if (oldWs && oldWs !== newWs) {
599
- oldWs.off("debug", this.onWsDebug);
600
- oldWs.on("error", noop);
601
- oldWs.off("error", this.onChildError);
602
- oldWs.off("open", this.onWsOpen);
603
- oldWs.off("packet", this.onWsPacket);
604
- oldWs.off("close", this.onWsClose);
605
- oldWs.destroy();
606
- }
607
- const oldUdp = Reflect.get(this._state, "udp");
608
- const newUdp = Reflect.get(newState, "udp");
609
- if (oldUdp && oldUdp !== newUdp) {
610
- oldUdp.on("error", noop);
611
- oldUdp.off("error", this.onChildError);
612
- oldUdp.off("close", this.onUdpClose);
613
- oldUdp.off("debug", this.onUdpDebug);
614
- oldUdp.destroy();
615
- }
616
- const oldState = this._state;
617
- this._state = newState;
618
- this.emit("stateChange", oldState, newState);
619
- this.debug?.(`state change:
620
- from ${stringifyState(oldState)}
621
- to ${stringifyState(newState)}`);
622
- }
1044
+ keepAliveInterval;
623
1045
  /**
624
- * Creates a new WebSocket to a Discord Voice gateway.
1046
+ * The time taken to receive a response to keep alive messages.
625
1047
  *
626
- * @param endpoint - The endpoint to connect to
1048
+ * @deprecated This field is no longer updated as keep alive messages are no longer tracked.
627
1049
  */
628
- createWebSocket(endpoint) {
629
- const ws = new VoiceWebSocket(`wss://${endpoint}?v=4`, Boolean(this.debug));
630
- ws.on("error", this.onChildError);
631
- ws.once("open", this.onWsOpen);
632
- ws.on("packet", this.onWsPacket);
633
- ws.once("close", this.onWsClose);
634
- ws.on("debug", this.onWsDebug);
635
- return ws;
636
- }
1050
+ ping;
637
1051
  /**
638
- * Propagates errors from the children VoiceWebSocket and VoiceUDPSocket.
1052
+ * Creates a new VoiceUDPSocket.
639
1053
  *
640
- * @param error - The error that was emitted by a child
1054
+ * @param remote - Details of the remote socket
641
1055
  */
642
- onChildError(error) {
643
- this.emit("error", error);
1056
+ constructor(remote) {
1057
+ super();
1058
+ this.socket = (0, import_node_dgram.createSocket)("udp4");
1059
+ this.socket.on("error", (error) => this.emit("error", error));
1060
+ this.socket.on("message", (buffer) => this.onMessage(buffer));
1061
+ this.socket.on("close", () => this.emit("close"));
1062
+ this.remote = remote;
1063
+ this.keepAliveBuffer = import_node_buffer4.Buffer.alloc(8);
1064
+ this.keepAliveInterval = setInterval(() => this.keepAlive(), KEEP_ALIVE_INTERVAL);
1065
+ setImmediate(() => this.keepAlive());
644
1066
  }
645
1067
  /**
646
- * Called when the WebSocket opens. Depending on the state that the instance is in,
647
- * it will either identify with a new session, or it will attempt to resume an existing session.
648
- */
649
- onWsOpen() {
650
- if (this.state.code === 0 /* OpeningWs */) {
651
- const packet = {
652
- op: import_v42.VoiceOpcodes.Identify,
653
- d: {
654
- server_id: this.state.connectionOptions.serverId,
655
- user_id: this.state.connectionOptions.userId,
656
- session_id: this.state.connectionOptions.sessionId,
657
- token: this.state.connectionOptions.token
658
- }
659
- };
660
- this.state.ws.sendPacket(packet);
661
- this.state = {
662
- ...this.state,
663
- code: 1 /* Identifying */
664
- };
665
- } else if (this.state.code === 5 /* Resuming */) {
666
- const packet = {
667
- op: import_v42.VoiceOpcodes.Resume,
668
- d: {
669
- server_id: this.state.connectionOptions.serverId,
670
- session_id: this.state.connectionOptions.sessionId,
671
- token: this.state.connectionOptions.token
672
- }
673
- };
674
- this.state.ws.sendPacket(packet);
1068
+ * Called when a message is received on the UDP socket.
1069
+ *
1070
+ * @param buffer - The received buffer
1071
+ */
1072
+ onMessage(buffer) {
1073
+ this.emit("message", buffer);
1074
+ }
1075
+ /**
1076
+ * Called at a regular interval to check whether we are still able to send datagrams to Discord.
1077
+ */
1078
+ keepAlive() {
1079
+ this.keepAliveBuffer.writeUInt32LE(this.keepAliveCounter, 0);
1080
+ this.send(this.keepAliveBuffer);
1081
+ this.keepAliveCounter++;
1082
+ if (this.keepAliveCounter > MAX_COUNTER_VALUE) {
1083
+ this.keepAliveCounter = 0;
675
1084
  }
676
1085
  }
677
1086
  /**
678
- * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter),
679
- * the instance will either attempt to resume, or enter the closed state and emit a 'close' event
680
- * with the close code, allowing the user to decide whether or not they would like to reconnect.
1087
+ * Sends a buffer to Discord.
681
1088
  *
682
- * @param code - The close code
1089
+ * @param buffer - The buffer to send
683
1090
  */
684
- onWsClose({ code }) {
685
- const canResume = code === 4015 || code < 4e3;
686
- if (canResume && this.state.code === 4 /* Ready */) {
687
- this.state = {
688
- ...this.state,
689
- code: 5 /* Resuming */,
690
- ws: this.createWebSocket(this.state.connectionOptions.endpoint)
691
- };
692
- } else if (this.state.code !== 6 /* Closed */) {
693
- this.destroy();
694
- this.emit("close", code);
695
- }
1091
+ send(buffer) {
1092
+ this.socket.send(buffer, this.remote.port, this.remote.ip);
696
1093
  }
697
1094
  /**
698
- * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord.
1095
+ * Closes the socket, the instance will not be able to be reused.
699
1096
  */
700
- onUdpClose() {
701
- if (this.state.code === 4 /* Ready */) {
702
- this.state = {
703
- ...this.state,
704
- code: 5 /* Resuming */,
705
- ws: this.createWebSocket(this.state.connectionOptions.endpoint)
706
- };
1097
+ destroy() {
1098
+ try {
1099
+ this.socket.close();
1100
+ } catch {
707
1101
  }
1102
+ clearInterval(this.keepAliveInterval);
708
1103
  }
709
1104
  /**
710
- * Called when a packet is received on the connection's WebSocket.
1105
+ * Performs IP discovery to discover the local address and port to be used for the voice connection.
711
1106
  *
712
- * @param packet - The received packet
1107
+ * @param ssrc - The SSRC received from Discord
713
1108
  */
714
- onWsPacket(packet) {
715
- if (packet.op === import_v42.VoiceOpcodes.Hello && this.state.code !== 6 /* Closed */) {
716
- this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval);
717
- } else if (packet.op === import_v42.VoiceOpcodes.Ready && this.state.code === 1 /* Identifying */) {
718
- const { ip, port, ssrc, modes } = packet.d;
719
- const udp = new VoiceUDPSocket({ ip, port });
720
- udp.on("error", this.onChildError);
721
- udp.on("debug", this.onUdpDebug);
722
- udp.once("close", this.onUdpClose);
723
- udp.performIPDiscovery(ssrc).then((localConfig) => {
724
- if (this.state.code !== 2 /* UdpHandshaking */) return;
725
- this.state.ws.sendPacket({
726
- op: import_v42.VoiceOpcodes.SelectProtocol,
727
- d: {
728
- protocol: "udp",
729
- data: {
730
- address: localConfig.ip,
731
- port: localConfig.port,
732
- mode: chooseEncryptionMode(modes)
733
- }
734
- }
735
- });
736
- this.state = {
737
- ...this.state,
738
- code: 3 /* SelectingProtocol */
739
- };
740
- }).catch((error) => this.emit("error", error));
741
- this.state = {
742
- ...this.state,
743
- code: 2 /* UdpHandshaking */,
744
- udp,
745
- connectionData: {
746
- ssrc
747
- }
748
- };
749
- } else if (packet.op === import_v42.VoiceOpcodes.SessionDescription && this.state.code === 3 /* SelectingProtocol */) {
750
- const { mode: encryptionMode, secret_key: secretKey } = packet.d;
751
- this.state = {
752
- ...this.state,
753
- code: 4 /* Ready */,
754
- connectionData: {
755
- ...this.state.connectionData,
756
- encryptionMode,
757
- secretKey: new Uint8Array(secretKey),
758
- sequence: randomNBit(16),
759
- timestamp: randomNBit(32),
760
- nonce: 0,
761
- nonceBuffer: encryptionMode === "aead_aes256_gcm_rtpsize" ? import_node_buffer3.Buffer.alloc(12) : import_node_buffer3.Buffer.alloc(24),
762
- speaking: false,
763
- packetsPlayed: 0
1109
+ async performIPDiscovery(ssrc) {
1110
+ return new Promise((resolve2, reject) => {
1111
+ const listener = /* @__PURE__ */ __name((message) => {
1112
+ try {
1113
+ if (message.readUInt16BE(0) !== 2) return;
1114
+ const packet = parseLocalPacket(message);
1115
+ this.socket.off("message", listener);
1116
+ resolve2(packet);
1117
+ } catch {
764
1118
  }
765
- };
766
- } else if (packet.op === import_v42.VoiceOpcodes.Resumed && this.state.code === 5 /* Resuming */) {
767
- this.state = {
768
- ...this.state,
769
- code: 4 /* Ready */
770
- };
771
- this.state.connectionData.speaking = false;
772
- }
1119
+ }, "listener");
1120
+ this.socket.on("message", listener);
1121
+ this.socket.once("close", () => reject(new Error("Cannot perform IP discovery - socket closed")));
1122
+ const discoveryBuffer = import_node_buffer4.Buffer.alloc(74);
1123
+ discoveryBuffer.writeUInt16BE(1, 0);
1124
+ discoveryBuffer.writeUInt16BE(70, 2);
1125
+ discoveryBuffer.writeUInt32BE(ssrc, 4);
1126
+ this.send(discoveryBuffer);
1127
+ });
1128
+ }
1129
+ };
1130
+
1131
+ // src/networking/VoiceWebSocket.ts
1132
+ var import_node_buffer5 = require("buffer");
1133
+ var import_node_events4 = require("events");
1134
+ var import_v8 = require("discord-api-types/voice/v8");
1135
+ var import_ws = __toESM(require("ws"));
1136
+ var VoiceWebSocket = class extends import_node_events4.EventEmitter {
1137
+ static {
1138
+ __name(this, "VoiceWebSocket");
773
1139
  }
774
1140
  /**
775
- * Propagates debug messages from the child WebSocket.
776
- *
777
- * @param message - The emitted debug message
1141
+ * The current heartbeat interval, if any.
778
1142
  */
779
- onWsDebug(message) {
780
- this.debug?.(`[WS] ${message}`);
781
- }
1143
+ heartbeatInterval;
782
1144
  /**
783
- * Propagates debug messages from the child UDPSocket.
784
- *
785
- * @param message - The emitted debug message
1145
+ * The time (milliseconds since UNIX epoch) that the last heartbeat acknowledgement packet was received.
1146
+ * This is set to 0 if an acknowledgement packet hasn't been received yet.
786
1147
  */
787
- onUdpDebug(message) {
788
- this.debug?.(`[UDP] ${message}`);
789
- }
1148
+ lastHeartbeatAck;
790
1149
  /**
791
- * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it.
792
- * It will be stored within the instance, and can be played by dispatchAudio()
793
- *
794
- * @remarks
795
- * Calling this method while there is already a prepared audio packet that has not yet been dispatched
796
- * will overwrite the existing audio packet. This should be avoided.
797
- * @param opusPacket - The Opus packet to encrypt
798
- * @returns The audio packet that was prepared
1150
+ * The time (milliseconds since UNIX epoch) that the last heartbeat was sent. This is set to 0 if a heartbeat
1151
+ * hasn't been sent yet.
799
1152
  */
800
- prepareAudioPacket(opusPacket) {
801
- const state = this.state;
802
- if (state.code !== 4 /* Ready */) return;
803
- state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData);
804
- return state.preparedPacket;
805
- }
1153
+ lastHeartbeatSend;
806
1154
  /**
807
- * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet
808
- * is consumed and cannot be dispatched again.
1155
+ * The number of consecutively missed heartbeats.
809
1156
  */
810
- dispatchAudio() {
811
- const state = this.state;
812
- if (state.code !== 4 /* Ready */) return false;
813
- if (state.preparedPacket !== void 0) {
814
- this.playAudioPacket(state.preparedPacket);
815
- state.preparedPacket = void 0;
816
- return true;
817
- }
818
- return false;
819
- }
1157
+ missedHeartbeats = 0;
820
1158
  /**
821
- * Plays an audio packet, updating timing metadata used for playback.
1159
+ * The last recorded ping.
1160
+ */
1161
+ ping;
1162
+ /**
1163
+ * The last sequence number acknowledged from Discord. Will be `-1` if no sequence numbered messages have been received.
1164
+ */
1165
+ sequence = -1;
1166
+ /**
1167
+ * The debug logger function, if debugging is enabled.
1168
+ */
1169
+ debug;
1170
+ /**
1171
+ * The underlying WebSocket of this wrapper.
1172
+ */
1173
+ ws;
1174
+ /**
1175
+ * Creates a new VoiceWebSocket.
822
1176
  *
823
- * @param audioPacket - The audio packet to play
1177
+ * @param address - The address to connect to
824
1178
  */
825
- playAudioPacket(audioPacket) {
826
- const state = this.state;
827
- if (state.code !== 4 /* Ready */) return;
828
- const { connectionData } = state;
829
- connectionData.packetsPlayed++;
830
- connectionData.sequence++;
831
- connectionData.timestamp += TIMESTAMP_INC;
832
- if (connectionData.sequence >= 2 ** 16) connectionData.sequence = 0;
833
- if (connectionData.timestamp >= 2 ** 32) connectionData.timestamp = 0;
834
- this.setSpeaking(true);
835
- state.udp.send(audioPacket);
1179
+ constructor(address, debug) {
1180
+ super();
1181
+ this.ws = new import_ws.default(address);
1182
+ this.ws.onmessage = (err) => this.onMessage(err);
1183
+ this.ws.onopen = (err) => this.emit("open", err);
1184
+ this.ws.onerror = (err) => this.emit("error", err instanceof Error ? err : err.error);
1185
+ this.ws.onclose = (err) => this.emit("close", err);
1186
+ this.lastHeartbeatAck = 0;
1187
+ this.lastHeartbeatSend = 0;
1188
+ this.debug = debug ? (message) => this.emit("debug", message) : null;
836
1189
  }
837
1190
  /**
838
- * Sends a packet to the voice gateway indicating that the client has start/stopped sending
839
- * audio.
1191
+ * Destroys the VoiceWebSocket. The heartbeat interval is cleared, and the connection is closed.
1192
+ */
1193
+ destroy() {
1194
+ try {
1195
+ this.debug?.("destroyed");
1196
+ this.setHeartbeatInterval(-1);
1197
+ this.ws.close(1e3);
1198
+ } catch (error) {
1199
+ const err = error;
1200
+ this.emit("error", err);
1201
+ }
1202
+ }
1203
+ /**
1204
+ * Handles message events on the WebSocket. Attempts to JSON parse the messages and emit them
1205
+ * as packets. Binary messages will be parsed and emitted.
840
1206
  *
841
- * @param speaking - Whether or not the client should be shown as speaking
1207
+ * @param event - The message event
842
1208
  */
843
- setSpeaking(speaking) {
844
- const state = this.state;
845
- if (state.code !== 4 /* Ready */) return;
846
- if (state.connectionData.speaking === speaking) return;
847
- state.connectionData.speaking = speaking;
848
- state.ws.sendPacket({
849
- op: import_v42.VoiceOpcodes.Speaking,
850
- d: {
851
- speaking: speaking ? 1 : 0,
852
- delay: 0,
853
- ssrc: state.connectionData.ssrc
854
- }
855
- });
1209
+ onMessage(event) {
1210
+ if (event.data instanceof import_node_buffer5.Buffer || event.data instanceof ArrayBuffer) {
1211
+ const buffer = event.data instanceof ArrayBuffer ? import_node_buffer5.Buffer.from(event.data) : event.data;
1212
+ const seq = buffer.readUInt16BE(0);
1213
+ const op = buffer.readUInt8(2);
1214
+ const payload = buffer.subarray(3);
1215
+ this.sequence = seq;
1216
+ this.debug?.(`<< [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes`);
1217
+ this.emit("binary", { op, seq, payload });
1218
+ return;
1219
+ } else if (typeof event.data !== "string") {
1220
+ return;
1221
+ }
1222
+ this.debug?.(`<< ${event.data}`);
1223
+ let packet;
1224
+ try {
1225
+ packet = JSON.parse(event.data);
1226
+ } catch (error) {
1227
+ const err = error;
1228
+ this.emit("error", err);
1229
+ return;
1230
+ }
1231
+ if (packet.seq) {
1232
+ this.sequence = packet.seq;
1233
+ }
1234
+ if (packet.op === import_v8.VoiceOpcodes.HeartbeatAck) {
1235
+ this.lastHeartbeatAck = Date.now();
1236
+ this.missedHeartbeats = 0;
1237
+ this.ping = this.lastHeartbeatAck - this.lastHeartbeatSend;
1238
+ }
1239
+ this.emit("packet", packet);
856
1240
  }
857
1241
  /**
858
- * Creates a new audio packet from an Opus packet. This involves encrypting the packet,
859
- * then prepending a header that includes metadata.
1242
+ * Sends a JSON-stringifiable packet over the WebSocket.
860
1243
  *
861
- * @param opusPacket - The Opus packet to prepare
862
- * @param connectionData - The current connection data of the instance
1244
+ * @param packet - The packet to send
863
1245
  */
864
- createAudioPacket(opusPacket, connectionData) {
865
- const rtpHeader = import_node_buffer3.Buffer.alloc(12);
866
- rtpHeader[0] = 128;
867
- rtpHeader[1] = 120;
868
- const { sequence, timestamp, ssrc } = connectionData;
869
- rtpHeader.writeUIntBE(sequence, 2, 2);
870
- rtpHeader.writeUIntBE(timestamp, 4, 4);
871
- rtpHeader.writeUIntBE(ssrc, 8, 4);
872
- rtpHeader.copy(nonce, 0, 0, 12);
873
- return import_node_buffer3.Buffer.concat([rtpHeader, ...this.encryptOpusPacket(opusPacket, connectionData, rtpHeader)]);
1246
+ sendPacket(packet) {
1247
+ try {
1248
+ const stringified = JSON.stringify(packet);
1249
+ this.debug?.(`>> ${stringified}`);
1250
+ this.ws.send(stringified);
1251
+ } catch (error) {
1252
+ const err = error;
1253
+ this.emit("error", err);
1254
+ }
874
1255
  }
875
1256
  /**
876
- * Encrypts an Opus packet using the format agreed upon by the instance and Discord.
1257
+ * Sends a binary message over the WebSocket.
877
1258
  *
878
- * @param opusPacket - The Opus packet to encrypt
879
- * @param connectionData - The current connection data of the instance
1259
+ * @param opcode - The opcode to use
1260
+ * @param payload - The payload to send
880
1261
  */
881
- encryptOpusPacket(opusPacket, connectionData, additionalData) {
882
- const { secretKey, encryptionMode } = connectionData;
883
- connectionData.nonce++;
884
- if (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0;
885
- connectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0);
886
- const noncePadding = connectionData.nonceBuffer.subarray(0, 4);
887
- let encrypted;
888
- switch (encryptionMode) {
889
- case "aead_aes256_gcm_rtpsize": {
890
- const cipher = import_node_crypto.default.createCipheriv("aes-256-gcm", secretKey, connectionData.nonceBuffer);
891
- cipher.setAAD(additionalData);
892
- encrypted = import_node_buffer3.Buffer.concat([cipher.update(opusPacket), cipher.final(), cipher.getAuthTag()]);
893
- return [encrypted, noncePadding];
894
- }
895
- case "aead_xchacha20_poly1305_rtpsize": {
896
- encrypted = methods.crypto_aead_xchacha20poly1305_ietf_encrypt(
897
- opusPacket,
898
- additionalData,
899
- connectionData.nonceBuffer,
900
- secretKey
901
- );
902
- return [encrypted, noncePadding];
903
- }
904
- default: {
905
- throw new RangeError(`Unsupported encryption method: ${encryptionMode}`);
906
- }
1262
+ sendBinaryMessage(opcode, payload) {
1263
+ try {
1264
+ const message = import_node_buffer5.Buffer.concat([new Uint8Array([opcode]), payload]);
1265
+ this.debug?.(`>> [bin] opcode ${opcode}, ${payload.byteLength} bytes`);
1266
+ this.ws.send(message);
1267
+ } catch (error) {
1268
+ const err = error;
1269
+ this.emit("error", err);
907
1270
  }
908
1271
  }
909
- };
910
-
911
- // src/receive/VoiceReceiver.ts
912
- var import_node_buffer5 = require("buffer");
913
- var import_node_crypto2 = __toESM(require("crypto"));
914
- var import_v43 = require("discord-api-types/voice/v4");
915
-
916
- // src/receive/AudioReceiveStream.ts
917
- var import_node_stream = require("stream");
918
-
919
- // src/audio/AudioPlayer.ts
920
- var import_node_buffer4 = require("buffer");
921
- var import_node_events4 = require("events");
922
-
923
- // src/audio/AudioPlayerError.ts
924
- var AudioPlayerError = class extends Error {
925
- static {
926
- __name(this, "AudioPlayerError");
927
- }
928
- /**
929
- * The resource associated with the audio player at the time the error was thrown.
930
- */
931
- resource;
932
- constructor(error, resource) {
933
- super(error.message);
934
- this.resource = resource;
935
- this.name = error.name;
936
- this.stack = error.stack;
937
- }
938
- };
939
-
940
- // src/audio/PlayerSubscription.ts
941
- var PlayerSubscription = class {
942
- static {
943
- __name(this, "PlayerSubscription");
944
- }
945
- /**
946
- * The voice connection of this subscription.
947
- */
948
- connection;
949
1272
  /**
950
- * The audio player of this subscription.
1273
+ * Sends a heartbeat over the WebSocket.
951
1274
  */
952
- player;
953
- constructor(connection, player) {
954
- this.connection = connection;
955
- this.player = player;
1275
+ sendHeartbeat() {
1276
+ this.lastHeartbeatSend = Date.now();
1277
+ this.missedHeartbeats++;
1278
+ const nonce2 = this.lastHeartbeatSend;
1279
+ this.sendPacket({
1280
+ op: import_v8.VoiceOpcodes.Heartbeat,
1281
+ // eslint-disable-next-line id-length
1282
+ d: {
1283
+ // eslint-disable-next-line id-length
1284
+ t: nonce2,
1285
+ seq_ack: this.sequence
1286
+ }
1287
+ });
956
1288
  }
957
1289
  /**
958
- * Unsubscribes the connection from the audio player, meaning that the
959
- * audio player cannot stream audio to it until a new subscription is made.
1290
+ * Sets/clears an interval to send heartbeats over the WebSocket.
1291
+ *
1292
+ * @param ms - The interval in milliseconds. If negative, the interval will be unset
960
1293
  */
961
- unsubscribe() {
962
- this.connection["onSubscriptionRemoved"](this);
963
- this.player["unsubscribe"](this);
1294
+ setHeartbeatInterval(ms) {
1295
+ if (this.heartbeatInterval !== void 0) clearInterval(this.heartbeatInterval);
1296
+ if (ms > 0) {
1297
+ this.heartbeatInterval = setInterval(() => {
1298
+ if (this.lastHeartbeatSend !== 0 && this.missedHeartbeats >= 3) {
1299
+ this.ws.close();
1300
+ this.setHeartbeatInterval(-1);
1301
+ }
1302
+ this.sendHeartbeat();
1303
+ }, ms);
1304
+ }
964
1305
  }
965
1306
  };
966
1307
 
967
- // src/audio/AudioPlayer.ts
968
- var SILENCE_FRAME = import_node_buffer4.Buffer.from([248, 255, 254]);
969
- var NoSubscriberBehavior = /* @__PURE__ */ ((NoSubscriberBehavior2) => {
970
- NoSubscriberBehavior2["Pause"] = "pause";
971
- NoSubscriberBehavior2["Play"] = "play";
972
- NoSubscriberBehavior2["Stop"] = "stop";
973
- return NoSubscriberBehavior2;
974
- })(NoSubscriberBehavior || {});
975
- var AudioPlayerStatus = /* @__PURE__ */ ((AudioPlayerStatus2) => {
976
- AudioPlayerStatus2["AutoPaused"] = "autopaused";
977
- AudioPlayerStatus2["Buffering"] = "buffering";
978
- AudioPlayerStatus2["Idle"] = "idle";
979
- AudioPlayerStatus2["Paused"] = "paused";
980
- AudioPlayerStatus2["Playing"] = "playing";
981
- return AudioPlayerStatus2;
982
- })(AudioPlayerStatus || {});
1308
+ // src/networking/Networking.ts
1309
+ var CHANNELS = 2;
1310
+ var TIMESTAMP_INC = 48e3 / 100 * CHANNELS;
1311
+ var MAX_NONCE_SIZE = 2 ** 32 - 1;
1312
+ var SUPPORTED_ENCRYPTION_MODES = [import_v82.VoiceEncryptionMode.AeadXChaCha20Poly1305RtpSize];
1313
+ if (import_node_crypto.default.getCiphers().includes("aes-256-gcm")) {
1314
+ SUPPORTED_ENCRYPTION_MODES.unshift(import_v82.VoiceEncryptionMode.AeadAes256GcmRtpSize);
1315
+ }
1316
+ var NetworkingStatusCode = /* @__PURE__ */ ((NetworkingStatusCode2) => {
1317
+ NetworkingStatusCode2[NetworkingStatusCode2["OpeningWs"] = 0] = "OpeningWs";
1318
+ NetworkingStatusCode2[NetworkingStatusCode2["Identifying"] = 1] = "Identifying";
1319
+ NetworkingStatusCode2[NetworkingStatusCode2["UdpHandshaking"] = 2] = "UdpHandshaking";
1320
+ NetworkingStatusCode2[NetworkingStatusCode2["SelectingProtocol"] = 3] = "SelectingProtocol";
1321
+ NetworkingStatusCode2[NetworkingStatusCode2["Ready"] = 4] = "Ready";
1322
+ NetworkingStatusCode2[NetworkingStatusCode2["Resuming"] = 5] = "Resuming";
1323
+ NetworkingStatusCode2[NetworkingStatusCode2["Closed"] = 6] = "Closed";
1324
+ return NetworkingStatusCode2;
1325
+ })(NetworkingStatusCode || {});
1326
+ var nonce = import_node_buffer6.Buffer.alloc(24);
983
1327
  function stringifyState2(state) {
984
1328
  return JSON.stringify({
985
1329
  ...state,
986
- resource: Reflect.has(state, "resource"),
987
- stepTimeout: Reflect.has(state, "stepTimeout")
1330
+ ws: Reflect.has(state, "ws"),
1331
+ udp: Reflect.has(state, "udp")
988
1332
  });
989
1333
  }
990
1334
  __name(stringifyState2, "stringifyState");
991
- var AudioPlayer = class extends import_node_events4.EventEmitter {
1335
+ function chooseEncryptionMode(options) {
1336
+ const option = options.find((option2) => SUPPORTED_ENCRYPTION_MODES.includes(option2));
1337
+ if (!option) {
1338
+ throw new Error(`No compatible encryption modes. Available include: ${options.join(", ")}`);
1339
+ }
1340
+ return option;
1341
+ }
1342
+ __name(chooseEncryptionMode, "chooseEncryptionMode");
1343
+ function randomNBit(numberOfBits) {
1344
+ return Math.floor(Math.random() * 2 ** numberOfBits);
1345
+ }
1346
+ __name(randomNBit, "randomNBit");
1347
+ var Networking = class extends import_node_events5.EventEmitter {
992
1348
  static {
993
- __name(this, "AudioPlayer");
1349
+ __name(this, "Networking");
994
1350
  }
995
- /**
996
- * The state that the AudioPlayer is in.
997
- */
998
1351
  _state;
999
1352
  /**
1000
- * A list of VoiceConnections that are registered to this AudioPlayer. The player will attempt to play audio
1001
- * to the streams in this list.
1353
+ * The debug logger function, if debugging is enabled.
1002
1354
  */
1003
- subscribers = [];
1355
+ debug;
1004
1356
  /**
1005
- * The behavior that the player should follow when it enters certain situations.
1357
+ * The options used to create this Networking instance.
1006
1358
  */
1007
- behaviors;
1359
+ options;
1008
1360
  /**
1009
- * The debug logger function, if debugging is enabled.
1361
+ * Creates a new Networking instance.
1010
1362
  */
1011
- debug;
1363
+ constructor(connectionOptions, options) {
1364
+ super();
1365
+ this.onWsOpen = this.onWsOpen.bind(this);
1366
+ this.onChildError = this.onChildError.bind(this);
1367
+ this.onWsPacket = this.onWsPacket.bind(this);
1368
+ this.onWsBinary = this.onWsBinary.bind(this);
1369
+ this.onWsClose = this.onWsClose.bind(this);
1370
+ this.onWsDebug = this.onWsDebug.bind(this);
1371
+ this.onUdpDebug = this.onUdpDebug.bind(this);
1372
+ this.onUdpClose = this.onUdpClose.bind(this);
1373
+ this.onDaveDebug = this.onDaveDebug.bind(this);
1374
+ this.onDaveKeyPackage = this.onDaveKeyPackage.bind(this);
1375
+ this.onDaveInvalidateTransition = this.onDaveInvalidateTransition.bind(this);
1376
+ this.debug = options?.debug ? (message) => this.emit("debug", message) : null;
1377
+ this._state = {
1378
+ code: 0 /* OpeningWs */,
1379
+ ws: this.createWebSocket(connectionOptions.endpoint),
1380
+ connectionOptions
1381
+ };
1382
+ this.options = options;
1383
+ }
1012
1384
  /**
1013
- * Creates a new AudioPlayer.
1385
+ * Destroys the Networking instance, transitioning it into the Closed state.
1014
1386
  */
1015
- constructor(options = {}) {
1016
- super();
1017
- this._state = { status: "idle" /* Idle */ };
1018
- this.behaviors = {
1019
- noSubscriber: "pause" /* Pause */,
1020
- maxMissedFrames: 5,
1021
- ...options.behaviors
1387
+ destroy() {
1388
+ this.state = {
1389
+ code: 6 /* Closed */
1022
1390
  };
1023
- this.debug = options.debug === false ? null : (message) => this.emit("debug", message);
1024
1391
  }
1025
1392
  /**
1026
- * A list of subscribed voice connections that can currently receive audio to play.
1393
+ * The current state of the networking instance.
1394
+ *
1395
+ * @remarks
1396
+ * The setter will perform clean-up operations where necessary.
1397
+ */
1398
+ get state() {
1399
+ return this._state;
1400
+ }
1401
+ set state(newState) {
1402
+ const oldWs = Reflect.get(this._state, "ws");
1403
+ const newWs = Reflect.get(newState, "ws");
1404
+ if (oldWs && oldWs !== newWs) {
1405
+ oldWs.off("debug", this.onWsDebug);
1406
+ oldWs.on("error", noop);
1407
+ oldWs.off("error", this.onChildError);
1408
+ oldWs.off("open", this.onWsOpen);
1409
+ oldWs.off("packet", this.onWsPacket);
1410
+ oldWs.off("binary", this.onWsBinary);
1411
+ oldWs.off("close", this.onWsClose);
1412
+ oldWs.destroy();
1413
+ }
1414
+ const oldUdp = Reflect.get(this._state, "udp");
1415
+ const newUdp = Reflect.get(newState, "udp");
1416
+ if (oldUdp && oldUdp !== newUdp) {
1417
+ oldUdp.on("error", noop);
1418
+ oldUdp.off("error", this.onChildError);
1419
+ oldUdp.off("close", this.onUdpClose);
1420
+ oldUdp.off("debug", this.onUdpDebug);
1421
+ oldUdp.destroy();
1422
+ }
1423
+ const oldDave = Reflect.get(this._state, "dave");
1424
+ const newDave = Reflect.get(newState, "dave");
1425
+ if (oldDave && oldDave !== newDave) {
1426
+ oldDave.off("error", this.onChildError);
1427
+ oldDave.off("debug", this.onDaveDebug);
1428
+ oldDave.off("keyPackage", this.onDaveKeyPackage);
1429
+ oldDave.off("invalidateTransition", this.onDaveInvalidateTransition);
1430
+ oldDave.destroy();
1431
+ }
1432
+ const oldState = this._state;
1433
+ this._state = newState;
1434
+ this.emit("stateChange", oldState, newState);
1435
+ this.debug?.(`state change:
1436
+ from ${stringifyState2(oldState)}
1437
+ to ${stringifyState2(newState)}`);
1438
+ }
1439
+ /**
1440
+ * Creates a new WebSocket to a Discord Voice gateway.
1441
+ *
1442
+ * @param endpoint - The endpoint to connect to
1443
+ * @param lastSequence - The last sequence to set for this WebSocket
1444
+ */
1445
+ createWebSocket(endpoint, lastSequence) {
1446
+ const ws = new VoiceWebSocket(`wss://${endpoint}?v=8`, Boolean(this.debug));
1447
+ if (lastSequence !== void 0) {
1448
+ ws.sequence = lastSequence;
1449
+ }
1450
+ ws.on("error", this.onChildError);
1451
+ ws.once("open", this.onWsOpen);
1452
+ ws.on("packet", this.onWsPacket);
1453
+ ws.on("binary", this.onWsBinary);
1454
+ ws.once("close", this.onWsClose);
1455
+ ws.on("debug", this.onWsDebug);
1456
+ return ws;
1457
+ }
1458
+ /**
1459
+ * Creates a new DAVE session for this voice connection if we can create one.
1460
+ *
1461
+ * @param protocolVersion - The protocol version to use
1027
1462
  */
1028
- get playable() {
1029
- return this.subscribers.filter(({ connection }) => connection.state.status === "ready" /* Ready */).map(({ connection }) => connection);
1463
+ createDaveSession(protocolVersion) {
1464
+ if (getMaxProtocolVersion() === null || this.options.daveEncryption === false || this.state.code !== 3 /* SelectingProtocol */ && this.state.code !== 4 /* Ready */ && this.state.code !== 5 /* Resuming */) {
1465
+ return;
1466
+ }
1467
+ const session = new DAVESession(
1468
+ protocolVersion,
1469
+ this.state.connectionOptions.userId,
1470
+ this.state.connectionOptions.channelId,
1471
+ {
1472
+ decryptionFailureTolerance: this.options.decryptionFailureTolerance
1473
+ }
1474
+ );
1475
+ session.on("error", this.onChildError);
1476
+ session.on("debug", this.onDaveDebug);
1477
+ session.on("keyPackage", this.onDaveKeyPackage);
1478
+ session.on("invalidateTransition", this.onDaveInvalidateTransition);
1479
+ session.reinit();
1480
+ return session;
1030
1481
  }
1031
1482
  /**
1032
- * Subscribes a VoiceConnection to the audio player's play list. If the VoiceConnection is already subscribed,
1033
- * then the existing subscription is used.
1483
+ * Propagates errors from the children VoiceWebSocket, VoiceUDPSocket and DAVESession.
1034
1484
  *
1035
- * @remarks
1036
- * This method should not be directly called. Instead, use VoiceConnection#subscribe.
1037
- * @param connection - The connection to subscribe
1038
- * @returns The new subscription if the voice connection is not yet subscribed, otherwise the existing subscription
1485
+ * @param error - The error that was emitted by a child
1039
1486
  */
1040
- // @ts-ignore
1041
- subscribe(connection) {
1042
- const existingSubscription = this.subscribers.find((subscription) => subscription.connection === connection);
1043
- if (!existingSubscription) {
1044
- const subscription = new PlayerSubscription(connection, this);
1045
- this.subscribers.push(subscription);
1046
- setImmediate(() => this.emit("subscribe", subscription));
1047
- return subscription;
1048
- }
1049
- return existingSubscription;
1487
+ onChildError(error) {
1488
+ this.emit("error", error);
1050
1489
  }
1051
1490
  /**
1052
- * Unsubscribes a subscription - i.e. removes a voice connection from the play list of the audio player.
1053
- *
1054
- * @remarks
1055
- * This method should not be directly called. Instead, use PlayerSubscription#unsubscribe.
1056
- * @param subscription - The subscription to remove
1057
- * @returns Whether or not the subscription existed on the player and was removed
1491
+ * Called when the WebSocket opens. Depending on the state that the instance is in,
1492
+ * it will either identify with a new session, or it will attempt to resume an existing session.
1058
1493
  */
1059
- // @ts-ignore
1060
- unsubscribe(subscription) {
1061
- const index = this.subscribers.indexOf(subscription);
1062
- const exists = index !== -1;
1063
- if (exists) {
1064
- this.subscribers.splice(index, 1);
1065
- subscription.connection.setSpeaking(false);
1066
- this.emit("unsubscribe", subscription);
1494
+ onWsOpen() {
1495
+ if (this.state.code === 0 /* OpeningWs */) {
1496
+ this.state.ws.sendPacket({
1497
+ op: import_v82.VoiceOpcodes.Identify,
1498
+ d: {
1499
+ server_id: this.state.connectionOptions.serverId,
1500
+ user_id: this.state.connectionOptions.userId,
1501
+ session_id: this.state.connectionOptions.sessionId,
1502
+ token: this.state.connectionOptions.token,
1503
+ max_dave_protocol_version: this.options.daveEncryption === false ? 0 : getMaxProtocolVersion() ?? 0
1504
+ }
1505
+ });
1506
+ this.state = {
1507
+ ...this.state,
1508
+ code: 1 /* Identifying */
1509
+ };
1510
+ } else if (this.state.code === 5 /* Resuming */) {
1511
+ this.state.ws.sendPacket({
1512
+ op: import_v82.VoiceOpcodes.Resume,
1513
+ d: {
1514
+ server_id: this.state.connectionOptions.serverId,
1515
+ session_id: this.state.connectionOptions.sessionId,
1516
+ token: this.state.connectionOptions.token,
1517
+ seq_ack: this.state.ws.sequence
1518
+ }
1519
+ });
1067
1520
  }
1068
- return exists;
1069
1521
  }
1070
1522
  /**
1071
- * The state that the player is in.
1523
+ * Called when the WebSocket closes. Based on the reason for closing (given by the code parameter),
1524
+ * the instance will either attempt to resume, or enter the closed state and emit a 'close' event
1525
+ * with the close code, allowing the user to decide whether or not they would like to reconnect.
1526
+ *
1527
+ * @param code - The close code
1072
1528
  */
1073
- get state() {
1074
- return this._state;
1529
+ onWsClose({ code }) {
1530
+ const canResume = code === 4015 || code < 4e3;
1531
+ if (canResume && this.state.code === 4 /* Ready */) {
1532
+ const lastSequence = this.state.ws.sequence;
1533
+ this.state = {
1534
+ ...this.state,
1535
+ code: 5 /* Resuming */,
1536
+ ws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence)
1537
+ };
1538
+ } else if (this.state.code !== 6 /* Closed */) {
1539
+ this.destroy();
1540
+ this.emit("close", code);
1541
+ }
1075
1542
  }
1076
1543
  /**
1077
- * Sets a new state for the player, performing clean-up operations where necessary.
1544
+ * Called when the UDP socket has closed itself if it has stopped receiving replies from Discord.
1078
1545
  */
1079
- set state(newState) {
1080
- const oldState = this._state;
1081
- const newResource = Reflect.get(newState, "resource");
1082
- if (oldState.status !== "idle" /* Idle */ && oldState.resource !== newResource) {
1083
- oldState.resource.playStream.on("error", noop);
1084
- oldState.resource.playStream.off("error", oldState.onStreamError);
1085
- oldState.resource.audioPlayer = void 0;
1086
- oldState.resource.playStream.destroy();
1087
- oldState.resource.playStream.read();
1088
- }
1089
- if (oldState.status === "buffering" /* Buffering */ && (newState.status !== "buffering" /* Buffering */ || newState.resource !== oldState.resource)) {
1090
- oldState.resource.playStream.off("end", oldState.onFailureCallback);
1091
- oldState.resource.playStream.off("close", oldState.onFailureCallback);
1092
- oldState.resource.playStream.off("finish", oldState.onFailureCallback);
1093
- oldState.resource.playStream.off("readable", oldState.onReadableCallback);
1094
- }
1095
- if (newState.status === "idle" /* Idle */) {
1096
- this._signalStopSpeaking();
1097
- deleteAudioPlayer(this);
1098
- }
1099
- if (newResource) {
1100
- addAudioPlayer(this);
1101
- }
1102
- const didChangeResources = oldState.status !== "idle" /* Idle */ && newState.status === "playing" /* Playing */ && oldState.resource !== newState.resource;
1103
- this._state = newState;
1104
- this.emit("stateChange", oldState, this._state);
1105
- if (oldState.status !== newState.status || didChangeResources) {
1106
- this.emit(newState.status, oldState, this._state);
1546
+ onUdpClose() {
1547
+ if (this.state.code === 4 /* Ready */) {
1548
+ const lastSequence = this.state.ws.sequence;
1549
+ this.state = {
1550
+ ...this.state,
1551
+ code: 5 /* Resuming */,
1552
+ ws: this.createWebSocket(this.state.connectionOptions.endpoint, lastSequence)
1553
+ };
1107
1554
  }
1108
- this.debug?.(`state change:
1109
- from ${stringifyState2(oldState)}
1110
- to ${stringifyState2(newState)}`);
1111
1555
  }
1112
1556
  /**
1113
- * Plays a new resource on the player. If the player is already playing a resource, the existing resource is destroyed
1114
- * (it cannot be reused, even in another player) and is replaced with the new resource.
1115
- *
1116
- * @remarks
1117
- * The player will transition to the Playing state once playback begins, and will return to the Idle state once
1118
- * playback is ended.
1557
+ * Called when a packet is received on the connection's WebSocket.
1119
1558
  *
1120
- * If the player was previously playing a resource and this method is called, the player will not transition to the
1121
- * Idle state during the swap over.
1122
- * @param resource - The resource to play
1123
- * @throws Will throw if attempting to play an audio resource that has already ended, or is being played by another player
1559
+ * @param packet - The received packet
1124
1560
  */
1125
- play(resource) {
1126
- if (resource.ended) {
1127
- throw new Error("Cannot play a resource that has already ended.");
1128
- }
1129
- if (resource.audioPlayer) {
1130
- if (resource.audioPlayer === this) {
1131
- return;
1132
- }
1133
- throw new Error("Resource is already being played by another audio player.");
1134
- }
1135
- resource.audioPlayer = this;
1136
- const onStreamError = /* @__PURE__ */ __name((error) => {
1137
- if (this.state.status !== "idle" /* Idle */) {
1138
- this.emit("error", new AudioPlayerError(error, this.state.resource));
1139
- }
1140
- if (this.state.status !== "idle" /* Idle */ && this.state.resource === resource) {
1561
+ onWsPacket(packet) {
1562
+ if (packet.op === import_v82.VoiceOpcodes.Hello && this.state.code !== 6 /* Closed */) {
1563
+ this.state.ws.setHeartbeatInterval(packet.d.heartbeat_interval);
1564
+ } else if (packet.op === import_v82.VoiceOpcodes.Ready && this.state.code === 1 /* Identifying */) {
1565
+ const { ip, port, ssrc, modes } = packet.d;
1566
+ const udp = new VoiceUDPSocket({ ip, port });
1567
+ udp.on("error", this.onChildError);
1568
+ udp.on("debug", this.onUdpDebug);
1569
+ udp.once("close", this.onUdpClose);
1570
+ udp.performIPDiscovery(ssrc).then((localConfig) => {
1571
+ if (this.state.code !== 2 /* UdpHandshaking */) return;
1572
+ this.state.ws.sendPacket({
1573
+ op: import_v82.VoiceOpcodes.SelectProtocol,
1574
+ d: {
1575
+ protocol: "udp",
1576
+ data: {
1577
+ address: localConfig.ip,
1578
+ port: localConfig.port,
1579
+ mode: chooseEncryptionMode(modes)
1580
+ }
1581
+ }
1582
+ });
1141
1583
  this.state = {
1142
- status: "idle" /* Idle */
1584
+ ...this.state,
1585
+ code: 3 /* SelectingProtocol */
1143
1586
  };
1144
- }
1145
- }, "onStreamError");
1146
- resource.playStream.once("error", onStreamError);
1147
- if (resource.started) {
1587
+ }).catch((error) => this.emit("error", error));
1148
1588
  this.state = {
1149
- status: "playing" /* Playing */,
1150
- missedFrames: 0,
1151
- playbackDuration: 0,
1152
- resource,
1153
- onStreamError
1154
- };
1155
- } else {
1156
- const onReadableCallback = /* @__PURE__ */ __name(() => {
1157
- if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) {
1158
- this.state = {
1159
- status: "playing" /* Playing */,
1160
- missedFrames: 0,
1161
- playbackDuration: 0,
1162
- resource,
1163
- onStreamError
1164
- };
1589
+ ...this.state,
1590
+ code: 2 /* UdpHandshaking */,
1591
+ udp,
1592
+ connectionData: {
1593
+ ssrc,
1594
+ connectedClients: /* @__PURE__ */ new Set()
1165
1595
  }
1166
- }, "onReadableCallback");
1167
- const onFailureCallback = /* @__PURE__ */ __name(() => {
1168
- if (this.state.status === "buffering" /* Buffering */ && this.state.resource === resource) {
1169
- this.state = {
1170
- status: "idle" /* Idle */
1171
- };
1596
+ };
1597
+ } else if (packet.op === import_v82.VoiceOpcodes.SessionDescription && this.state.code === 3 /* SelectingProtocol */) {
1598
+ const { mode: encryptionMode, secret_key: secretKey, dave_protocol_version: daveProtocolVersion } = packet.d;
1599
+ this.state = {
1600
+ ...this.state,
1601
+ code: 4 /* Ready */,
1602
+ dave: this.createDaveSession(daveProtocolVersion),
1603
+ connectionData: {
1604
+ ...this.state.connectionData,
1605
+ encryptionMode,
1606
+ secretKey: new Uint8Array(secretKey),
1607
+ sequence: randomNBit(16),
1608
+ timestamp: randomNBit(32),
1609
+ nonce: 0,
1610
+ nonceBuffer: encryptionMode === "aead_aes256_gcm_rtpsize" ? import_node_buffer6.Buffer.alloc(12) : import_node_buffer6.Buffer.alloc(24),
1611
+ speaking: false,
1612
+ packetsPlayed: 0
1172
1613
  }
1173
- }, "onFailureCallback");
1174
- resource.playStream.once("readable", onReadableCallback);
1175
- resource.playStream.once("end", onFailureCallback);
1176
- resource.playStream.once("close", onFailureCallback);
1177
- resource.playStream.once("finish", onFailureCallback);
1614
+ };
1615
+ } else if (packet.op === import_v82.VoiceOpcodes.Resumed && this.state.code === 5 /* Resuming */) {
1178
1616
  this.state = {
1179
- status: "buffering" /* Buffering */,
1180
- resource,
1181
- onReadableCallback,
1182
- onFailureCallback,
1183
- onStreamError
1617
+ ...this.state,
1618
+ code: 4 /* Ready */
1184
1619
  };
1620
+ this.state.connectionData.speaking = false;
1621
+ } else if ((packet.op === import_v82.VoiceOpcodes.ClientsConnect || packet.op === import_v82.VoiceOpcodes.ClientDisconnect) && (this.state.code === 4 /* Ready */ || this.state.code === 2 /* UdpHandshaking */ || this.state.code === 3 /* SelectingProtocol */ || this.state.code === 5 /* Resuming */)) {
1622
+ const { connectionData } = this.state;
1623
+ if (packet.op === import_v82.VoiceOpcodes.ClientsConnect)
1624
+ for (const id of packet.d.user_ids) connectionData.connectedClients.add(id);
1625
+ else {
1626
+ connectionData.connectedClients.delete(packet.d.user_id);
1627
+ }
1628
+ } else if ((this.state.code === 4 /* Ready */ || this.state.code === 5 /* Resuming */) && this.state.dave) {
1629
+ if (packet.op === import_v82.VoiceOpcodes.DavePrepareTransition) {
1630
+ const sendReady = this.state.dave.prepareTransition(packet.d);
1631
+ if (sendReady)
1632
+ this.state.ws.sendPacket({
1633
+ op: import_v82.VoiceOpcodes.DaveTransitionReady,
1634
+ d: { transition_id: packet.d.transition_id }
1635
+ });
1636
+ if (packet.d.transition_id === 0) {
1637
+ this.emit("transitioned", 0);
1638
+ }
1639
+ } else if (packet.op === import_v82.VoiceOpcodes.DaveExecuteTransition) {
1640
+ const transitioned = this.state.dave.executeTransition(packet.d.transition_id);
1641
+ if (transitioned) this.emit("transitioned", packet.d.transition_id);
1642
+ } else if (packet.op === import_v82.VoiceOpcodes.DavePrepareEpoch) this.state.dave.prepareEpoch(packet.d);
1185
1643
  }
1186
1644
  }
1187
1645
  /**
1188
- * Pauses playback of the current resource, if any.
1646
+ * Called when a binary message is received on the connection's WebSocket.
1189
1647
  *
1190
- * @param interpolateSilence - If true, the player will play 5 packets of silence after pausing to prevent audio glitches
1191
- * @returns `true` if the player was successfully paused, otherwise `false`
1648
+ * @param message - The received message
1649
+ */
1650
+ onWsBinary(message) {
1651
+ if (this.state.code === 4 /* Ready */ && this.state.dave) {
1652
+ if (message.op === import_v82.VoiceOpcodes.DaveMlsExternalSender) {
1653
+ this.state.dave.setExternalSender(message.payload);
1654
+ } else if (message.op === import_v82.VoiceOpcodes.DaveMlsProposals) {
1655
+ const payload = this.state.dave.processProposals(message.payload, this.state.connectionData.connectedClients);
1656
+ if (payload) this.state.ws.sendBinaryMessage(import_v82.VoiceOpcodes.DaveMlsCommitWelcome, payload);
1657
+ } else if (message.op === import_v82.VoiceOpcodes.DaveMlsAnnounceCommitTransition) {
1658
+ const { transitionId, success } = this.state.dave.processCommit(message.payload);
1659
+ if (success) {
1660
+ if (transitionId === 0) this.emit("transitioned", transitionId);
1661
+ else
1662
+ this.state.ws.sendPacket({
1663
+ op: import_v82.VoiceOpcodes.DaveTransitionReady,
1664
+ d: { transition_id: transitionId }
1665
+ });
1666
+ }
1667
+ } else if (message.op === import_v82.VoiceOpcodes.DaveMlsWelcome) {
1668
+ const { transitionId, success } = this.state.dave.processWelcome(message.payload);
1669
+ if (success) {
1670
+ if (transitionId === 0) this.emit("transitioned", transitionId);
1671
+ else
1672
+ this.state.ws.sendPacket({
1673
+ op: import_v82.VoiceOpcodes.DaveTransitionReady,
1674
+ d: { transition_id: transitionId }
1675
+ });
1676
+ }
1677
+ }
1678
+ }
1679
+ }
1680
+ /**
1681
+ * Called when a new key package is ready to be sent to the voice server.
1682
+ *
1683
+ * @param keyPackage - The new key package
1192
1684
  */
1193
- pause(interpolateSilence = true) {
1194
- if (this.state.status !== "playing" /* Playing */) return false;
1195
- this.state = {
1196
- ...this.state,
1197
- status: "paused" /* Paused */,
1198
- silencePacketsRemaining: interpolateSilence ? 5 : 0
1199
- };
1200
- return true;
1685
+ onDaveKeyPackage(keyPackage) {
1686
+ if (this.state.code === 3 /* SelectingProtocol */ || this.state.code === 4 /* Ready */)
1687
+ this.state.ws.sendBinaryMessage(import_v82.VoiceOpcodes.DaveMlsKeyPackage, keyPackage);
1688
+ }
1689
+ /**
1690
+ * Called when the DAVE session wants to invalidate their transition and re-initialize.
1691
+ *
1692
+ * @param transitionId - The transition to invalidate
1693
+ */
1694
+ onDaveInvalidateTransition(transitionId) {
1695
+ if (this.state.code === 3 /* SelectingProtocol */ || this.state.code === 4 /* Ready */)
1696
+ this.state.ws.sendPacket({
1697
+ op: import_v82.VoiceOpcodes.DaveMlsInvalidCommitWelcome,
1698
+ d: { transition_id: transitionId }
1699
+ });
1700
+ }
1701
+ /**
1702
+ * Propagates debug messages from the child WebSocket.
1703
+ *
1704
+ * @param message - The emitted debug message
1705
+ */
1706
+ onWsDebug(message) {
1707
+ this.debug?.(`[WS] ${message}`);
1201
1708
  }
1202
1709
  /**
1203
- * Unpauses playback of the current resource, if any.
1710
+ * Propagates debug messages from the child UDPSocket.
1204
1711
  *
1205
- * @returns `true` if the player was successfully unpaused, otherwise `false`
1712
+ * @param message - The emitted debug message
1206
1713
  */
1207
- unpause() {
1208
- if (this.state.status !== "paused" /* Paused */) return false;
1209
- this.state = {
1210
- ...this.state,
1211
- status: "playing" /* Playing */,
1212
- missedFrames: 0
1213
- };
1214
- return true;
1714
+ onUdpDebug(message) {
1715
+ this.debug?.(`[UDP] ${message}`);
1215
1716
  }
1216
1717
  /**
1217
- * Stops playback of the current resource and destroys the resource. The player will either transition to the Idle state,
1218
- * or remain in its current state until the silence padding frames of the resource have been played.
1718
+ * Propagates debug messages from the child DAVESession.
1219
1719
  *
1220
- * @param force - If true, will force the player to enter the Idle state even if the resource has silence padding frames
1221
- * @returns `true` if the player will come to a stop, otherwise `false`
1720
+ * @param message - The emitted debug message
1222
1721
  */
1223
- stop(force = false) {
1224
- if (this.state.status === "idle" /* Idle */) return false;
1225
- if (force || this.state.resource.silencePaddingFrames === 0) {
1226
- this.state = {
1227
- status: "idle" /* Idle */
1228
- };
1229
- } else if (this.state.resource.silenceRemaining === -1) {
1230
- this.state.resource.silenceRemaining = this.state.resource.silencePaddingFrames;
1231
- }
1232
- return true;
1722
+ onDaveDebug(message) {
1723
+ this.debug?.(`[DAVE] ${message}`);
1233
1724
  }
1234
1725
  /**
1235
- * Checks whether the underlying resource (if any) is playable (readable)
1726
+ * Prepares an Opus packet for playback. This includes attaching metadata to it and encrypting it.
1727
+ * It will be stored within the instance, and can be played by dispatchAudio()
1236
1728
  *
1237
- * @returns `true` if the resource is playable, otherwise `false`
1729
+ * @remarks
1730
+ * Calling this method while there is already a prepared audio packet that has not yet been dispatched
1731
+ * will overwrite the existing audio packet. This should be avoided.
1732
+ * @param opusPacket - The Opus packet to encrypt
1733
+ * @returns The audio packet that was prepared
1238
1734
  */
1239
- checkPlayable() {
1240
- const state = this._state;
1241
- if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return false;
1242
- if (!state.resource.readable) {
1243
- this.state = {
1244
- status: "idle" /* Idle */
1245
- };
1246
- return false;
1247
- }
1248
- return true;
1735
+ prepareAudioPacket(opusPacket) {
1736
+ const state = this.state;
1737
+ if (state.code !== 4 /* Ready */) return;
1738
+ state.preparedPacket = this.createAudioPacket(opusPacket, state.connectionData, state.dave);
1739
+ return state.preparedPacket;
1249
1740
  }
1250
1741
  /**
1251
- * Called roughly every 20ms by the global audio player timer. Dispatches any audio packets that are buffered
1252
- * by the active connections of this audio player.
1742
+ * Dispatches the audio packet previously prepared by prepareAudioPacket(opusPacket). The audio packet
1743
+ * is consumed and cannot be dispatched again.
1253
1744
  */
1254
- // @ts-ignore
1255
- _stepDispatch() {
1256
- const state = this._state;
1257
- if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return;
1258
- for (const connection of this.playable) {
1259
- connection.dispatchAudio();
1745
+ dispatchAudio() {
1746
+ const state = this.state;
1747
+ if (state.code !== 4 /* Ready */) return false;
1748
+ if (state.preparedPacket !== void 0) {
1749
+ this.playAudioPacket(state.preparedPacket);
1750
+ state.preparedPacket = void 0;
1751
+ return true;
1260
1752
  }
1753
+ return false;
1261
1754
  }
1262
1755
  /**
1263
- * Called roughly every 20ms by the global audio player timer. Attempts to read an audio packet from the
1264
- * underlying resource of the stream, and then has all the active connections of the audio player prepare it
1265
- * (encrypt it, append header data) so that it is ready to play at the start of the next cycle.
1756
+ * Plays an audio packet, updating timing metadata used for playback.
1757
+ *
1758
+ * @param audioPacket - The audio packet to play
1266
1759
  */
1267
- // @ts-ignore
1268
- _stepPrepare() {
1269
- const state = this._state;
1270
- if (state.status === "idle" /* Idle */ || state.status === "buffering" /* Buffering */) return;
1271
- const playable = this.playable;
1272
- if (state.status === "autopaused" /* AutoPaused */ && playable.length > 0) {
1273
- this.state = {
1274
- ...state,
1275
- status: "playing" /* Playing */,
1276
- missedFrames: 0
1277
- };
1278
- }
1279
- if (state.status === "paused" /* Paused */ || state.status === "autopaused" /* AutoPaused */) {
1280
- if (state.silencePacketsRemaining > 0) {
1281
- state.silencePacketsRemaining--;
1282
- this._preparePacket(SILENCE_FRAME, playable, state);
1283
- if (state.silencePacketsRemaining === 0) {
1284
- this._signalStopSpeaking();
1285
- }
1286
- }
1287
- return;
1288
- }
1289
- if (playable.length === 0) {
1290
- if (this.behaviors.noSubscriber === "pause" /* Pause */) {
1291
- this.state = {
1292
- ...state,
1293
- status: "autopaused" /* AutoPaused */,
1294
- silencePacketsRemaining: 5
1295
- };
1296
- return;
1297
- } else if (this.behaviors.noSubscriber === "stop" /* Stop */) {
1298
- this.stop(true);
1299
- }
1300
- }
1301
- const packet = state.resource.read();
1302
- if (state.status === "playing" /* Playing */) {
1303
- if (packet) {
1304
- this._preparePacket(packet, playable, state);
1305
- state.missedFrames = 0;
1306
- } else {
1307
- this._preparePacket(SILENCE_FRAME, playable, state);
1308
- state.missedFrames++;
1309
- if (state.missedFrames >= this.behaviors.maxMissedFrames) {
1310
- this.stop();
1311
- }
1760
+ playAudioPacket(audioPacket) {
1761
+ const state = this.state;
1762
+ if (state.code !== 4 /* Ready */) return;
1763
+ const { connectionData } = state;
1764
+ connectionData.packetsPlayed++;
1765
+ connectionData.sequence++;
1766
+ connectionData.timestamp += TIMESTAMP_INC;
1767
+ if (connectionData.sequence >= 2 ** 16) connectionData.sequence = 0;
1768
+ if (connectionData.timestamp >= 2 ** 32) connectionData.timestamp = 0;
1769
+ this.setSpeaking(true);
1770
+ state.udp.send(audioPacket);
1771
+ }
1772
+ /**
1773
+ * Sends a packet to the voice gateway indicating that the client has start/stopped sending
1774
+ * audio.
1775
+ *
1776
+ * @param speaking - Whether or not the client should be shown as speaking
1777
+ */
1778
+ setSpeaking(speaking) {
1779
+ const state = this.state;
1780
+ if (state.code !== 4 /* Ready */) return;
1781
+ if (state.connectionData.speaking === speaking) return;
1782
+ state.connectionData.speaking = speaking;
1783
+ state.ws.sendPacket({
1784
+ op: import_v82.VoiceOpcodes.Speaking,
1785
+ d: {
1786
+ speaking: speaking ? 1 : 0,
1787
+ delay: 0,
1788
+ ssrc: state.connectionData.ssrc
1312
1789
  }
1313
- }
1790
+ });
1314
1791
  }
1315
1792
  /**
1316
- * Signals to all the subscribed connections that they should send a packet to Discord indicating
1317
- * they are no longer speaking. Called once playback of a resource ends.
1793
+ * Creates a new audio packet from an Opus packet. This involves encrypting the packet,
1794
+ * then prepending a header that includes metadata.
1795
+ *
1796
+ * @param opusPacket - The Opus packet to prepare
1797
+ * @param connectionData - The current connection data of the instance
1798
+ * @param daveSession - The DAVE session to use for encryption
1318
1799
  */
1319
- _signalStopSpeaking() {
1320
- for (const { connection } of this.subscribers) {
1321
- connection.setSpeaking(false);
1322
- }
1800
+ createAudioPacket(opusPacket, connectionData, daveSession) {
1801
+ const rtpHeader = import_node_buffer6.Buffer.alloc(12);
1802
+ rtpHeader[0] = 128;
1803
+ rtpHeader[1] = 120;
1804
+ const { sequence, timestamp, ssrc } = connectionData;
1805
+ rtpHeader.writeUIntBE(sequence, 2, 2);
1806
+ rtpHeader.writeUIntBE(timestamp, 4, 4);
1807
+ rtpHeader.writeUIntBE(ssrc, 8, 4);
1808
+ rtpHeader.copy(nonce, 0, 0, 12);
1809
+ return import_node_buffer6.Buffer.concat([rtpHeader, ...this.encryptOpusPacket(opusPacket, connectionData, rtpHeader, daveSession)]);
1323
1810
  }
1324
1811
  /**
1325
- * Instructs the given connections to each prepare this packet to be played at the start of the
1326
- * next cycle.
1812
+ * Encrypts an Opus packet using the format agreed upon by the instance and Discord.
1327
1813
  *
1328
- * @param packet - The Opus packet to be prepared by each receiver
1329
- * @param receivers - The connections that should play this packet
1814
+ * @param opusPacket - The Opus packet to encrypt
1815
+ * @param connectionData - The current connection data of the instance
1816
+ * @param daveSession - The DAVE session to use for encryption
1330
1817
  */
1331
- _preparePacket(packet, receivers, state) {
1332
- state.playbackDuration += 20;
1333
- for (const connection of receivers) {
1334
- connection.prepareAudioPacket(packet);
1818
+ encryptOpusPacket(opusPacket, connectionData, additionalData, daveSession) {
1819
+ const { secretKey, encryptionMode } = connectionData;
1820
+ const packet = daveSession?.encrypt(opusPacket) ?? opusPacket;
1821
+ connectionData.nonce++;
1822
+ if (connectionData.nonce > MAX_NONCE_SIZE) connectionData.nonce = 0;
1823
+ connectionData.nonceBuffer.writeUInt32BE(connectionData.nonce, 0);
1824
+ const noncePadding = connectionData.nonceBuffer.subarray(0, 4);
1825
+ let encrypted;
1826
+ switch (encryptionMode) {
1827
+ case "aead_aes256_gcm_rtpsize": {
1828
+ const cipher = import_node_crypto.default.createCipheriv("aes-256-gcm", secretKey, connectionData.nonceBuffer);
1829
+ cipher.setAAD(additionalData);
1830
+ encrypted = import_node_buffer6.Buffer.concat([cipher.update(packet), cipher.final(), cipher.getAuthTag()]);
1831
+ return [encrypted, noncePadding];
1832
+ }
1833
+ case "aead_xchacha20_poly1305_rtpsize": {
1834
+ encrypted = methods.crypto_aead_xchacha20poly1305_ietf_encrypt(
1835
+ packet,
1836
+ additionalData,
1837
+ connectionData.nonceBuffer,
1838
+ secretKey
1839
+ );
1840
+ return [encrypted, noncePadding];
1841
+ }
1842
+ default: {
1843
+ throw new RangeError(`Unsupported encryption method: ${encryptionMode}`);
1844
+ }
1335
1845
  }
1336
1846
  }
1337
1847
  };
1338
- function createAudioPlayer(options) {
1339
- return new AudioPlayer(options);
1340
- }
1341
- __name(createAudioPlayer, "createAudioPlayer");
1848
+
1849
+ // src/receive/VoiceReceiver.ts
1850
+ var import_node_buffer7 = require("buffer");
1851
+ var import_node_crypto2 = __toESM(require("crypto"));
1852
+ var import_v83 = require("discord-api-types/voice/v8");
1342
1853
 
1343
1854
  // src/receive/AudioReceiveStream.ts
1855
+ var import_node_process = require("process");
1856
+ var import_node_stream = require("stream");
1344
1857
  var EndBehaviorType = /* @__PURE__ */ ((EndBehaviorType2) => {
1345
1858
  EndBehaviorType2[EndBehaviorType2["Manual"] = 0] = "Manual";
1346
1859
  EndBehaviorType2[EndBehaviorType2["AfterSilence"] = 1] = "AfterSilence";
@@ -1364,9 +1877,10 @@ var AudioReceiveStream = class extends import_node_stream.Readable {
1364
1877
  */
1365
1878
  end;
1366
1879
  endTimeout;
1367
- constructor({ end, ...options }) {
1880
+ constructor(options) {
1881
+ const { end, ...rest } = options;
1368
1882
  super({
1369
- ...options,
1883
+ ...rest,
1370
1884
  objectMode: true
1371
1885
  });
1372
1886
  this.end = end;
@@ -1375,6 +1889,9 @@ var AudioReceiveStream = class extends import_node_stream.Readable {
1375
1889
  if (buffer && (this.end.behavior === 2 /* AfterInactivity */ || this.end.behavior === 1 /* AfterSilence */ && (buffer.compare(SILENCE_FRAME) !== 0 || this.endTimeout === void 0))) {
1376
1890
  this.renewEndTimeout(this.end);
1377
1891
  }
1892
+ if (buffer === null) {
1893
+ (0, import_node_process.nextTick)(() => this.destroy());
1894
+ }
1378
1895
  return super.push(buffer);
1379
1896
  }
1380
1897
  renewEndTimeout(end) {
@@ -1388,8 +1905,8 @@ var AudioReceiveStream = class extends import_node_stream.Readable {
1388
1905
  };
1389
1906
 
1390
1907
  // src/receive/SSRCMap.ts
1391
- var import_node_events5 = require("events");
1392
- var SSRCMap = class extends import_node_events5.EventEmitter {
1908
+ var import_node_events6 = require("events");
1909
+ var SSRCMap = class extends import_node_events6.EventEmitter {
1393
1910
  static {
1394
1911
  __name(this, "SSRCMap");
1395
1912
  }
@@ -1459,8 +1976,8 @@ var SSRCMap = class extends import_node_events5.EventEmitter {
1459
1976
  };
1460
1977
 
1461
1978
  // src/receive/SpeakingMap.ts
1462
- var import_node_events6 = require("events");
1463
- var SpeakingMap = class _SpeakingMap extends import_node_events6.EventEmitter {
1979
+ var import_node_events7 = require("events");
1980
+ var SpeakingMap = class _SpeakingMap extends import_node_events7.EventEmitter {
1464
1981
  static {
1465
1982
  __name(this, "SpeakingMap");
1466
1983
  }
@@ -1501,7 +2018,7 @@ var SpeakingMap = class _SpeakingMap extends import_node_events6.EventEmitter {
1501
2018
  };
1502
2019
 
1503
2020
  // src/receive/VoiceReceiver.ts
1504
- var HEADER_EXTENSION_BYTE = import_node_buffer5.Buffer.from([190, 222]);
2021
+ var HEADER_EXTENSION_BYTE = import_node_buffer7.Buffer.from([190, 222]);
1505
2022
  var UNPADDED_NONCE_LENGTH = 4;
1506
2023
  var AUTH_TAG_LENGTH = 16;
1507
2024
  var VoiceReceiver = class {
@@ -1546,16 +2063,10 @@ var VoiceReceiver = class {
1546
2063
  * @internal
1547
2064
  */
1548
2065
  onWsPacket(packet) {
1549
- if (packet.op === import_v43.VoiceOpcodes.ClientDisconnect && typeof packet.d?.user_id === "string") {
2066
+ if (packet.op === import_v83.VoiceOpcodes.ClientDisconnect) {
1550
2067
  this.ssrcMap.delete(packet.d.user_id);
1551
- } else if (packet.op === import_v43.VoiceOpcodes.Speaking && typeof packet.d?.user_id === "string" && typeof packet.d?.ssrc === "number") {
2068
+ } else if (packet.op === import_v83.VoiceOpcodes.Speaking) {
1552
2069
  this.ssrcMap.update({ userId: packet.d.user_id, audioSSRC: packet.d.ssrc });
1553
- } else if (packet.op === import_v43.VoiceOpcodes.ClientConnect && typeof packet.d?.user_id === "string" && typeof packet.d?.audio_ssrc === "number") {
1554
- this.ssrcMap.update({
1555
- userId: packet.d.user_id,
1556
- audioSSRC: packet.d.audio_ssrc,
1557
- videoSSRC: packet.d.video_ssrc === 0 ? void 0 : packet.d.video_ssrc
1558
- });
1559
2070
  }
1560
2071
  }
1561
2072
  decrypt(buffer, mode, nonce2, secretKey) {
@@ -1574,12 +2085,12 @@ var VoiceReceiver = class {
1574
2085
  const decipheriv = import_node_crypto2.default.createDecipheriv("aes-256-gcm", secretKey, nonce2);
1575
2086
  decipheriv.setAAD(header);
1576
2087
  decipheriv.setAuthTag(authTag);
1577
- return import_node_buffer5.Buffer.concat([decipheriv.update(encrypted), decipheriv.final()]);
2088
+ return import_node_buffer7.Buffer.concat([decipheriv.update(encrypted), decipheriv.final()]);
1578
2089
  }
1579
2090
  case "aead_xchacha20_poly1305_rtpsize": {
1580
- return import_node_buffer5.Buffer.from(
2091
+ return import_node_buffer7.Buffer.from(
1581
2092
  methods.crypto_aead_xchacha20poly1305_ietf_decrypt(
1582
- import_node_buffer5.Buffer.concat([encrypted, authTag]),
2093
+ import_node_buffer7.Buffer.concat([encrypted, authTag]),
1583
2094
  header,
1584
2095
  nonce2,
1585
2096
  secretKey
@@ -1598,15 +2109,20 @@ var VoiceReceiver = class {
1598
2109
  * @param mode - The encryption mode
1599
2110
  * @param nonce - The nonce buffer used by the connection for encryption
1600
2111
  * @param secretKey - The secret key used by the connection for encryption
2112
+ * @param userId - The user id that sent the packet
1601
2113
  * @returns The parsed Opus packet
1602
2114
  */
1603
- parsePacket(buffer, mode, nonce2, secretKey) {
2115
+ parsePacket(buffer, mode, nonce2, secretKey, userId) {
1604
2116
  let packet = this.decrypt(buffer, mode, nonce2, secretKey);
1605
- if (!packet) return;
2117
+ if (!packet) throw new Error("Failed to parse packet");
1606
2118
  if (buffer.subarray(12, 14).compare(HEADER_EXTENSION_BYTE) === 0) {
1607
2119
  const headerExtensionLength = buffer.subarray(14).readUInt16BE();
1608
2120
  packet = packet.subarray(4 * headerExtensionLength);
1609
2121
  }
2122
+ if (this.voiceConnection.state.status === "ready" /* Ready */ && (this.voiceConnection.state.networking.state.code === 4 /* Ready */ || this.voiceConnection.state.networking.state.code === 5 /* Resuming */)) {
2123
+ const daveSession = this.voiceConnection.state.networking.state.dave;
2124
+ if (daveSession) packet = daveSession.decrypt(packet, userId);
2125
+ }
1610
2126
  return packet;
1611
2127
  }
1612
2128
  /**
@@ -1624,16 +2140,17 @@ var VoiceReceiver = class {
1624
2140
  const stream = this.subscriptions.get(userData.userId);
1625
2141
  if (!stream) return;
1626
2142
  if (this.connectionData.encryptionMode && this.connectionData.nonceBuffer && this.connectionData.secretKey) {
1627
- const packet = this.parsePacket(
1628
- msg,
1629
- this.connectionData.encryptionMode,
1630
- this.connectionData.nonceBuffer,
1631
- this.connectionData.secretKey
1632
- );
1633
- if (packet) {
1634
- stream.push(packet);
1635
- } else {
1636
- stream.destroy(new Error("Failed to parse packet"));
2143
+ try {
2144
+ const packet = this.parsePacket(
2145
+ msg,
2146
+ this.connectionData.encryptionMode,
2147
+ this.connectionData.nonceBuffer,
2148
+ this.connectionData.secretKey,
2149
+ userData.userId
2150
+ );
2151
+ if (packet) stream.push(packet);
2152
+ } catch (error) {
2153
+ stream.destroy(error);
1637
2154
  }
1638
2155
  }
1639
2156
  }
@@ -1672,7 +2189,7 @@ var VoiceConnectionDisconnectReason = /* @__PURE__ */ ((VoiceConnectionDisconnec
1672
2189
  VoiceConnectionDisconnectReason2[VoiceConnectionDisconnectReason2["Manual"] = 3] = "Manual";
1673
2190
  return VoiceConnectionDisconnectReason2;
1674
2191
  })(VoiceConnectionDisconnectReason || {});
1675
- var VoiceConnection = class extends import_node_events7.EventEmitter {
2192
+ var VoiceConnection = class extends import_node_events8.EventEmitter {
1676
2193
  static {
1677
2194
  __name(this, "VoiceConnection");
1678
2195
  }
@@ -1705,6 +2222,10 @@ var VoiceConnection = class extends import_node_events7.EventEmitter {
1705
2222
  * The debug logger function, if debugging is enabled.
1706
2223
  */
1707
2224
  debug;
2225
+ /**
2226
+ * The options used to create this voice connection.
2227
+ */
2228
+ options;
1708
2229
  /**
1709
2230
  * Creates a new voice connection.
1710
2231
  *
@@ -1720,6 +2241,7 @@ var VoiceConnection = class extends import_node_events7.EventEmitter {
1720
2241
  this.onNetworkingStateChange = this.onNetworkingStateChange.bind(this);
1721
2242
  this.onNetworkingError = this.onNetworkingError.bind(this);
1722
2243
  this.onNetworkingDebug = this.onNetworkingDebug.bind(this);
2244
+ this.onNetworkingTransitioned = this.onNetworkingTransitioned.bind(this);
1723
2245
  const adapter = options.adapterCreator({
1724
2246
  onVoiceServerUpdate: /* @__PURE__ */ __name((data) => this.addServerPacket(data), "onVoiceServerUpdate"),
1725
2247
  onVoiceStateUpdate: /* @__PURE__ */ __name((data) => this.addStatePacket(data), "onVoiceStateUpdate"),
@@ -1731,16 +2253,17 @@ var VoiceConnection = class extends import_node_events7.EventEmitter {
1731
2253
  state: void 0
1732
2254
  };
1733
2255
  this.joinConfig = joinConfig;
2256
+ this.options = options;
1734
2257
  }
1735
2258
  /**
1736
2259
  * The current state of the voice connection.
2260
+ *
2261
+ * @remarks
2262
+ * The setter will perform clean-up operations where necessary.
1737
2263
  */
1738
2264
  get state() {
1739
2265
  return this._state;
1740
2266
  }
1741
- /**
1742
- * Updates the state of the voice connection, performing clean-up operations where necessary.
1743
- */
1744
2267
  set state(newState) {
1745
2268
  const oldState = this._state;
1746
2269
  const oldNetworking = Reflect.get(oldState, "networking");
@@ -1754,6 +2277,7 @@ var VoiceConnection = class extends import_node_events7.EventEmitter {
1754
2277
  oldNetworking.off("error", this.onNetworkingError);
1755
2278
  oldNetworking.off("close", this.onNetworkingClose);
1756
2279
  oldNetworking.off("stateChange", this.onNetworkingStateChange);
2280
+ oldNetworking.off("transitioned", this.onNetworkingTransitioned);
1757
2281
  oldNetworking.destroy();
1758
2282
  }
1759
2283
  if (newNetworking) this.updateReceiveBindings(newNetworking.state, oldNetworking?.state);
@@ -1849,14 +2373,20 @@ var VoiceConnection = class extends import_node_events7.EventEmitter {
1849
2373
  serverId: server.guild_id,
1850
2374
  token: server.token,
1851
2375
  sessionId: state.session_id,
1852
- userId: state.user_id
2376
+ userId: state.user_id,
2377
+ channelId: state.channel_id
1853
2378
  },
1854
- Boolean(this.debug)
2379
+ {
2380
+ debug: Boolean(this.debug),
2381
+ daveEncryption: this.options.daveEncryption ?? true,
2382
+ decryptionFailureTolerance: this.options.decryptionFailureTolerance
2383
+ }
1855
2384
  );
1856
2385
  networking.once("close", this.onNetworkingClose);
1857
2386
  networking.on("stateChange", this.onNetworkingStateChange);
1858
2387
  networking.on("error", this.onNetworkingError);
1859
2388
  networking.on("debug", this.onNetworkingDebug);
2389
+ networking.on("transitioned", this.onNetworkingTransitioned);
1860
2390
  this.state = {
1861
2391
  ...this.state,
1862
2392
  status: "connecting" /* Connecting */,
@@ -1937,6 +2467,14 @@ var VoiceConnection = class extends import_node_events7.EventEmitter {
1937
2467
  onNetworkingDebug(message) {
1938
2468
  this.debug?.(`[NW] ${message}`);
1939
2469
  }
2470
+ /**
2471
+ * Propagates transitions from the underlying network instance.
2472
+ *
2473
+ * @param transitionId - The transition id
2474
+ */
2475
+ onNetworkingTransitioned(transitionId) {
2476
+ this.emit("transitioned", transitionId);
2477
+ }
1940
2478
  /**
1941
2479
  * Prepares an audio packet for dispatch.
1942
2480
  *
@@ -2092,6 +2630,30 @@ var VoiceConnection = class extends import_node_events7.EventEmitter {
2092
2630
  udp: void 0
2093
2631
  };
2094
2632
  }
2633
+ /**
2634
+ * The current voice privacy code of the encrypted session.
2635
+ *
2636
+ * @remarks
2637
+ * For this data to be available, the VoiceConnection must be in the Ready state,
2638
+ * and the connection would have to be end-to-end encrypted.
2639
+ */
2640
+ get voicePrivacyCode() {
2641
+ if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */) {
2642
+ return this.state.networking.state.dave?.voicePrivacyCode ?? void 0;
2643
+ }
2644
+ return void 0;
2645
+ }
2646
+ /**
2647
+ * Gets the verification code for a user in the session.
2648
+ *
2649
+ * @throws Will throw if end-to-end encryption is not on or if the user id provided is not in the session.
2650
+ */
2651
+ async getVerificationCode(userId) {
2652
+ if (this.state.status === "ready" /* Ready */ && this.state.networking.state.code === 4 /* Ready */ && this.state.networking.state.dave) {
2653
+ return this.state.networking.state.dave.getVerificationCode(userId);
2654
+ }
2655
+ throw new Error("Session not available");
2656
+ }
2095
2657
  /**
2096
2658
  * Called when a subscription of this voice connection to an audio player is removed.
2097
2659
  *
@@ -2148,7 +2710,9 @@ function joinVoiceChannel(options) {
2148
2710
  };
2149
2711
  return createVoiceConnection(joinConfig, {
2150
2712
  adapterCreator: options.adapterCreator,
2151
- debug: options.debug
2713
+ debug: options.debug,
2714
+ daveEncryption: options.daveEncryption,
2715
+ decryptionFailureTolerance: options.decryptionFailureTolerance
2152
2716
  });
2153
2717
  }
2154
2718
  __name(joinVoiceChannel, "joinVoiceChannel");
@@ -2182,6 +2746,16 @@ var StreamType = /* @__PURE__ */ ((StreamType2) => {
2182
2746
  StreamType2["WebmOpus"] = "webm/opus";
2183
2747
  return StreamType2;
2184
2748
  })(StreamType || {});
2749
+ var TransformerType = /* @__PURE__ */ ((TransformerType2) => {
2750
+ TransformerType2["FFmpegOgg"] = "ffmpeg ogg";
2751
+ TransformerType2["FFmpegPCM"] = "ffmpeg pcm";
2752
+ TransformerType2["InlineVolume"] = "volume transformer";
2753
+ TransformerType2["OggOpusDemuxer"] = "ogg/opus demuxer";
2754
+ TransformerType2["OpusDecoder"] = "opus decoder";
2755
+ TransformerType2["OpusEncoder"] = "opus encoder";
2756
+ TransformerType2["WebmOpusDemuxer"] = "webm/opus demuxer";
2757
+ return TransformerType2;
2758
+ })(TransformerType || {});
2185
2759
  var Node = class {
2186
2760
  static {
2187
2761
  __name(this, "Node");
@@ -2471,6 +3045,7 @@ function createAudioResource(input, options = {}) {
2471
3045
  __name(createAudioResource, "createAudioResource");
2472
3046
 
2473
3047
  // src/util/generateDependencyReport.ts
3048
+ var import_node_crypto3 = require("crypto");
2474
3049
  var import_node_path = require("path");
2475
3050
  var import_prism_media3 = __toESM(require("prism-media"));
2476
3051
  function findPackageJSON(dir, packageName, depth) {
@@ -2488,7 +3063,7 @@ __name(findPackageJSON, "findPackageJSON");
2488
3063
  function version(name) {
2489
3064
  try {
2490
3065
  if (name === "@discordjs/voice") {
2491
- return "0.18.1-dev.1732709130-97ffa201a";
3066
+ return "0.19.0";
2492
3067
  }
2493
3068
  const pkg = findPackageJSON((0, import_node_path.dirname)(require.resolve(name)), name, 3);
2494
3069
  return pkg?.version ?? "not found";
@@ -2509,12 +3084,16 @@ function generateDependencyReport() {
2509
3084
  addVersion("opusscript");
2510
3085
  report.push("");
2511
3086
  report.push("Encryption Libraries");
3087
+ report.push(`- native crypto support for aes-256-gcm: ${(0, import_node_crypto3.getCiphers)().includes("aes-256-gcm") ? "yes" : "no"}`);
2512
3088
  addVersion("sodium-native");
2513
3089
  addVersion("sodium");
2514
3090
  addVersion("libsodium-wrappers");
2515
3091
  addVersion("@stablelib/xchacha20poly1305");
2516
3092
  addVersion("@noble/ciphers");
2517
3093
  report.push("");
3094
+ report.push("DAVE Libraries");
3095
+ addVersion("@snazzah/davey");
3096
+ report.push("");
2518
3097
  report.push("FFmpeg");
2519
3098
  try {
2520
3099
  const info = import_prism_media3.default.FFmpeg.getInfo();
@@ -2528,7 +3107,7 @@ function generateDependencyReport() {
2528
3107
  __name(generateDependencyReport, "generateDependencyReport");
2529
3108
 
2530
3109
  // src/util/entersState.ts
2531
- var import_node_events8 = require("events");
3110
+ var import_node_events9 = require("events");
2532
3111
 
2533
3112
  // src/util/abortAfter.ts
2534
3113
  function abortAfter(delay) {
@@ -2544,7 +3123,7 @@ async function entersState(target, status, timeoutOrSignal) {
2544
3123
  if (target.state.status !== status) {
2545
3124
  const [ac, signal] = typeof timeoutOrSignal === "number" ? abortAfter(timeoutOrSignal) : [void 0, timeoutOrSignal];
2546
3125
  try {
2547
- await (0, import_node_events8.once)(target, status, { signal });
3126
+ await (0, import_node_events9.once)(target, status, { signal });
2548
3127
  } finally {
2549
3128
  ac?.abort();
2550
3129
  }
@@ -2554,8 +3133,8 @@ async function entersState(target, status, timeoutOrSignal) {
2554
3133
  __name(entersState, "entersState");
2555
3134
 
2556
3135
  // src/util/demuxProbe.ts
2557
- var import_node_buffer6 = require("buffer");
2558
- var import_node_process = __toESM(require("process"));
3136
+ var import_node_buffer8 = require("buffer");
3137
+ var import_node_process2 = __toESM(require("process"));
2559
3138
  var import_node_stream3 = require("stream");
2560
3139
  var import_prism_media4 = __toESM(require("prism-media"));
2561
3140
  function validateDiscordOpusHead(opusHead) {
@@ -2574,7 +3153,7 @@ async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordO
2574
3153
  reject(new Error("Cannot probe a stream that has ended"));
2575
3154
  return;
2576
3155
  }
2577
- let readBuffer = import_node_buffer6.Buffer.alloc(0);
3156
+ let readBuffer = import_node_buffer8.Buffer.alloc(0);
2578
3157
  let resolved;
2579
3158
  const finish = /* @__PURE__ */ __name((type) => {
2580
3159
  stream.off("data", onData);
@@ -2614,13 +3193,13 @@ async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordO
2614
3193
  }
2615
3194
  }, "onClose");
2616
3195
  const onData = /* @__PURE__ */ __name((buffer) => {
2617
- readBuffer = import_node_buffer6.Buffer.concat([readBuffer, buffer]);
3196
+ readBuffer = import_node_buffer8.Buffer.concat([readBuffer, buffer]);
2618
3197
  webm.write(buffer);
2619
3198
  ogg.write(buffer);
2620
3199
  if (readBuffer.length >= probeSize) {
2621
3200
  stream.off("data", onData);
2622
3201
  stream.pause();
2623
- import_node_process.default.nextTick(onClose);
3202
+ import_node_process2.default.nextTick(onClose);
2624
3203
  }
2625
3204
  }, "onData");
2626
3205
  stream.once("error", reject);
@@ -2632,7 +3211,7 @@ async function demuxProbe(stream, probeSize = 1024, validator = validateDiscordO
2632
3211
  __name(demuxProbe, "demuxProbe");
2633
3212
 
2634
3213
  // src/index.ts
2635
- var version2 = "0.18.1-dev.1732709130-97ffa201a";
3214
+ var version2 = "0.19.0";
2636
3215
  // Annotate the CommonJS export names for ESM import in node:
2637
3216
  0 && (module.exports = {
2638
3217
  AudioPlayer,
@@ -2640,16 +3219,23 @@ var version2 = "0.18.1-dev.1732709130-97ffa201a";
2640
3219
  AudioPlayerStatus,
2641
3220
  AudioReceiveStream,
2642
3221
  AudioResource,
3222
+ DAVESession,
2643
3223
  EndBehaviorType,
3224
+ Networking,
3225
+ NetworkingStatusCode,
2644
3226
  NoSubscriberBehavior,
3227
+ Node,
2645
3228
  PlayerSubscription,
2646
3229
  SSRCMap,
2647
3230
  SpeakingMap,
2648
3231
  StreamType,
3232
+ TransformerType,
2649
3233
  VoiceConnection,
2650
3234
  VoiceConnectionDisconnectReason,
2651
3235
  VoiceConnectionStatus,
2652
3236
  VoiceReceiver,
3237
+ VoiceUDPSocket,
3238
+ VoiceWebSocket,
2653
3239
  createAudioPlayer,
2654
3240
  createAudioResource,
2655
3241
  createDefaultAudioReceiveStreamOptions,