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