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