djs-selfbot-v13 3.7.32 → 3.7.33

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.
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+
3
+ const EventEmitter = require('events');
4
+ const Davey = require('@snazzah/davey');
5
+
6
+ const TRANSITION_EXPIRY = 10;
7
+ const TRANSITION_EXPIRY_PENDING_DOWNGRADE = 24;
8
+ const DEFAULT_DECRYPTION_FAILURE_TOLERANCE = 36;
9
+
10
+ const SILENCE_FRAME = Buffer.from([0xf8, 0xff, 0xfe]);
11
+
12
+ function getMaxProtocolVersion() {
13
+ return Davey.DAVE_PROTOCOL_VERSION ?? 0;
14
+ }
15
+
16
+ class DAVESession extends EventEmitter {
17
+ constructor(protocolVersion, userId, channelId, options = {}) {
18
+ super();
19
+ this.protocolVersion = protocolVersion;
20
+ this.userId = userId;
21
+ this.channelId = channelId;
22
+ this.failureTolerance = options.decryptionFailureTolerance ?? DEFAULT_DECRYPTION_FAILURE_TOLERANCE;
23
+ this.pendingTransitions = new Map();
24
+ this.downgraded = false;
25
+ this.consecutiveFailures = 0;
26
+ this.reinitializing = false;
27
+ this.lastTransitionId = undefined;
28
+ this.session = undefined;
29
+ }
30
+
31
+ get voicePrivacyCode() {
32
+ if (this.protocolVersion === 0 || !this.session?.voicePrivacyCode) return null;
33
+ return this.session.voicePrivacyCode;
34
+ }
35
+
36
+ async getVerificationCode(userId) {
37
+ if (!this.session) throw new Error('Session not available');
38
+ return this.session.getVerificationCode(userId);
39
+ }
40
+
41
+ reinit() {
42
+ if (this.protocolVersion > 0) {
43
+ if (this.session) {
44
+ this.session.reinit(this.protocolVersion, this.userId, this.channelId);
45
+ this.emit('debug', `Session reinitialized for protocol version ${this.protocolVersion}`);
46
+ } else {
47
+ this.session = new Davey.DAVESession(this.protocolVersion, this.userId, this.channelId);
48
+ this.emit('debug', `Session initialized for protocol version ${this.protocolVersion}`);
49
+ }
50
+ this.emit('keyPackage', this.session.getSerializedKeyPackage());
51
+ } else if (this.session) {
52
+ this.session.reset();
53
+ this.session.setPassthroughMode(true, TRANSITION_EXPIRY);
54
+ this.emit('debug', 'Session reset');
55
+ }
56
+ }
57
+
58
+ setExternalSender(externalSender) {
59
+ if (!this.session) throw new Error('No session available');
60
+ try {
61
+ this.session.setExternalSender(externalSender);
62
+ this.emit('debug', 'Set MLS external sender');
63
+ } catch (error) {
64
+ if (String(error).includes('AlreadyInGroup')) {
65
+ this.emit('debug', 'MLS external sender already set, skipping');
66
+ return;
67
+ }
68
+ throw error;
69
+ }
70
+ }
71
+
72
+ prepareTransition(data) {
73
+ this.emit('debug', `Preparing for transition (${data.transition_id}, v${data.protocol_version})`);
74
+ this.pendingTransitions.set(data.transition_id, data.protocol_version);
75
+
76
+ if (data.transition_id === 0) {
77
+ this.executeTransition(data.transition_id);
78
+ } else {
79
+ if (data.protocol_version === 0) this.session?.setPassthroughMode(true, TRANSITION_EXPIRY_PENDING_DOWNGRADE);
80
+ return true;
81
+ }
82
+
83
+ return false;
84
+ }
85
+
86
+ executeTransition(transitionId) {
87
+ this.emit('debug', `Executing transition (${transitionId})`);
88
+ if (!this.pendingTransitions.has(transitionId)) {
89
+ this.emit('debug', `Received execute transition, but we don't have a pending transition for ${transitionId}`);
90
+ return false;
91
+ }
92
+
93
+ const oldVersion = this.protocolVersion;
94
+ this.protocolVersion = this.pendingTransitions.get(transitionId);
95
+
96
+ if (oldVersion !== this.protocolVersion && this.protocolVersion === 0) {
97
+ this.downgraded = true;
98
+ this.emit('debug', 'Session downgraded');
99
+ } else if (transitionId > 0 && this.downgraded) {
100
+ this.downgraded = false;
101
+ this.session?.setPassthroughMode(true, TRANSITION_EXPIRY);
102
+ this.emit('debug', 'Session upgraded');
103
+ }
104
+
105
+ this.reinitializing = false;
106
+ this.lastTransitionId = transitionId;
107
+ this.emit('debug', `Transition executed (v${oldVersion} -> v${this.protocolVersion}, id: ${transitionId})`);
108
+ this.pendingTransitions.delete(transitionId);
109
+ return true;
110
+ }
111
+
112
+ prepareEpoch(data) {
113
+ this.emit('debug', `Preparing for epoch (${data.epoch})`);
114
+ if (data.epoch === 1) {
115
+ this.protocolVersion = data.protocol_version;
116
+ this.reinit();
117
+ }
118
+ }
119
+
120
+ recoverFromInvalidTransition(transitionId) {
121
+ if (this.reinitializing) return;
122
+ this.emit('debug', `Invalidating transition ${transitionId}`);
123
+ this.reinitializing = true;
124
+ this.consecutiveFailures = 0;
125
+ this.emit('invalidateTransition', transitionId);
126
+ this.reinit();
127
+ }
128
+
129
+ processProposals(payload, connectedClients) {
130
+ if (!this.session) throw new Error('No session available');
131
+ try {
132
+ const optype = payload.readUInt8(0);
133
+ const { commit, welcome } = this.session.processProposals(
134
+ optype,
135
+ payload.subarray(1),
136
+ Array.from(connectedClients),
137
+ );
138
+ this.emit('debug', 'MLS proposals processed');
139
+ if (!commit) return;
140
+ return welcome ? Buffer.concat([commit, welcome]) : commit;
141
+ } catch (error) {
142
+ this.emit('debug', `MLS proposals errored: ${error}`);
143
+ return null;
144
+ }
145
+ }
146
+
147
+ processCommit(payload) {
148
+ if (!this.session) throw new Error('No session available');
149
+ const transitionId = payload.readUInt16BE(0);
150
+ try {
151
+ this.session.processCommit(payload.subarray(2));
152
+ if (transitionId === 0) {
153
+ this.reinitializing = false;
154
+ this.lastTransitionId = transitionId;
155
+ } else {
156
+ this.pendingTransitions.set(transitionId, this.protocolVersion);
157
+ }
158
+ this.emit('debug', `MLS commit processed (transition id: ${transitionId})`);
159
+ return { transitionId, success: true };
160
+ } catch (error) {
161
+ this.emit('debug', `MLS commit errored from transition ${transitionId}: ${error}`);
162
+ this.recoverFromInvalidTransition(transitionId);
163
+ return { transitionId, success: false };
164
+ }
165
+ }
166
+
167
+ processWelcome(payload) {
168
+ if (!this.session) throw new Error('No session available');
169
+ const transitionId = payload.readUInt16BE(0);
170
+ try {
171
+ this.session.processWelcome(payload.subarray(2));
172
+ if (transitionId === 0) {
173
+ this.reinitializing = false;
174
+ this.lastTransitionId = transitionId;
175
+ } else {
176
+ this.pendingTransitions.set(transitionId, this.protocolVersion);
177
+ }
178
+ this.emit('debug', `MLS welcome processed (transition id: ${transitionId})`);
179
+ return { transitionId, success: true };
180
+ } catch (error) {
181
+ this.emit('debug', `MLS welcome errored from transition ${transitionId}: ${error}`);
182
+ this.recoverFromInvalidTransition(transitionId);
183
+ return { transitionId, success: false };
184
+ }
185
+ }
186
+
187
+ encrypt(packet) {
188
+ if (this.protocolVersion === 0 || !this.session?.ready || packet.equals(SILENCE_FRAME)) return packet;
189
+ return this.session.encryptOpus(packet);
190
+ }
191
+
192
+ encryptVideo(packet, codec = 'H264') {
193
+ if (this.protocolVersion === 0 || !this.session?.ready) return packet;
194
+ const codecMap = {
195
+ H264: Davey.Codec.H264,
196
+ VP8: Davey.Codec.VP8,
197
+ H265: Davey.Codec.H265,
198
+ AV1: Davey.Codec.AV1,
199
+ };
200
+ return this.session.encrypt(Davey.MediaType.VIDEO, codecMap[codec] ?? Davey.Codec.UNKNOWN, packet);
201
+ }
202
+
203
+ decrypt(packet, userId) {
204
+ const canDecrypt = this.session?.ready && (this.protocolVersion !== 0 || this.session?.canPassthrough(userId));
205
+ if (packet.equals(SILENCE_FRAME) || !canDecrypt || !this.session) return packet;
206
+ try {
207
+ const buffer = this.session.decrypt(userId, Davey.MediaType.AUDIO, packet);
208
+ this.consecutiveFailures = 0;
209
+ return buffer;
210
+ } catch (error) {
211
+ if (!this.reinitializing && this.pendingTransitions.size === 0) {
212
+ this.consecutiveFailures++;
213
+ this.emit('debug', `Failed to decrypt a packet (${this.consecutiveFailures} consecutive fails)`);
214
+ if (this.consecutiveFailures > this.failureTolerance) {
215
+ if (this.lastTransitionId) this.recoverFromInvalidTransition(this.lastTransitionId);
216
+ else throw error;
217
+ }
218
+ } else if (this.reinitializing) {
219
+ this.emit('debug', 'Failed to decrypt a packet (reinitializing session)');
220
+ } else if (this.pendingTransitions.size > 0) {
221
+ this.emit('debug', `Failed to decrypt a packet (${this.pendingTransitions.size} pending transition[s])`);
222
+ }
223
+ }
224
+ return null;
225
+ }
226
+
227
+ destroy() {
228
+ try {
229
+ this.session?.reset();
230
+ } catch {}
231
+ }
232
+ }
233
+
234
+ module.exports = { DAVESession, getMaxProtocolVersion };
@@ -4,7 +4,8 @@ const EventEmitter = require('events');
4
4
  const { setTimeout, setInterval } = require('node:timers');
5
5
  const WebSocket = require('../../../WebSocket');
6
6
  const { Error } = require('../../../errors');
7
- const { Opcodes, VoiceOpcodes } = require('../../../util/Constants');
7
+ const { Opcodes, VoiceOpcodes, VoiceStatus } = require('../../../util/Constants');
8
+ const { DAVESession, getMaxProtocolVersion } = require('./DAVESession');
8
9
 
9
10
  /**
10
11
  * Represents a Voice Connection's WebSocket.
@@ -29,6 +30,7 @@ class VoiceWebSocket extends EventEmitter {
29
30
  this._sequenceNumber = -1;
30
31
 
31
32
  this.dead = false;
33
+ this._identified = false;
32
34
  this.connection.on('closing', this.shutdown.bind(this));
33
35
  }
34
36
 
@@ -57,6 +59,7 @@ class VoiceWebSocket extends EventEmitter {
57
59
  this.ws = null;
58
60
  }
59
61
  this.clearHeartbeat();
62
+ this._identified = false;
60
63
  }
61
64
 
62
65
  /**
@@ -67,7 +70,7 @@ class VoiceWebSocket extends EventEmitter {
67
70
  if (this.dead) return;
68
71
  if (this.ws) this.reset();
69
72
  if (this.attempts >= 5) {
70
- this.emit('debug', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts));
73
+ this.emit('error', new Error('VOICE_CONNECTION_ATTEMPTS_EXCEEDED', this.attempts));
71
74
  return;
72
75
  }
73
76
 
@@ -87,13 +90,14 @@ class VoiceWebSocket extends EventEmitter {
87
90
 
88
91
  /**
89
92
  * Sends data to the WebSocket if it is open.
90
- * @param {string} data The data to send to the WebSocket
91
- * @returns {Promise<string>}
93
+ * @param {string|Buffer} data The data to send to the WebSocket
94
+ * @returns {Promise<string|Buffer>}
92
95
  */
93
96
  send(data) {
94
- this.emit('debug', `[WS] >> ${data}`);
97
+ const preview = typeof data === 'string' ? data : `[bin] ${data.byteLength} bytes`;
98
+ this.emit('debug', `[WS] >> ${preview}`);
95
99
  return new Promise((resolve, reject) => {
96
- if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', data);
100
+ if (!this.ws || this.ws.readyState !== WebSocket.OPEN) throw new Error('WS_NOT_OPEN', preview);
97
101
  this.ws.send(data, null, error => {
98
102
  if (error) reject(error);
99
103
  else resolve(data);
@@ -111,23 +115,46 @@ class VoiceWebSocket extends EventEmitter {
111
115
  return this.send(packet);
112
116
  }
113
117
 
118
+ /**
119
+ * Sends a binary message over the WebSocket.
120
+ * @param {number} opcode The opcode to use
121
+ * @param {Buffer} payload The payload to send
122
+ * @returns {Promise<Buffer>}
123
+ */
124
+ sendBinaryMessage(opcode, payload) {
125
+ const message = Buffer.concat([Buffer.from([opcode]), payload]);
126
+ return this.send(message);
127
+ }
128
+
114
129
  /**
115
130
  * Called whenever the WebSocket opens.
116
131
  */
117
132
  onOpen() {
118
133
  this.emit('debug', `[WS] opened at gateway ${this.connection.authentication.endpoint}`);
119
- this.sendPacket({
120
- op: Opcodes.DISPATCH,
121
- d: {
122
- server_id: this.connection.serverId || this.connection.channel.guild?.id || this.connection.channel.id,
123
- user_id: this.client.user.id,
124
- token: this.connection.authentication.token,
125
- session_id: this.connection.authentication.sessionId,
126
- streams: [{ type: 'screen', rid: '100', quality: 100 }],
127
- video: true,
128
- },
129
- }).catch(() => {
130
- this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED'));
134
+ }
135
+
136
+ sendIdentify() {
137
+ if (this._identified) return Promise.resolve();
138
+ this._identified = true;
139
+
140
+ const isStream = this.connection.constructor.name === 'StreamConnection';
141
+ const data = {
142
+ server_id: this.connection.serverId || this.connection.channel.guild?.id || this.connection.channel.id,
143
+ user_id: this.client.user.id,
144
+ token: this.connection.authentication.token,
145
+ session_id: this.connection.authentication.sessionId,
146
+ max_dave_protocol_version: getMaxProtocolVersion(),
147
+ };
148
+
149
+ if (isStream) {
150
+ data.channel_id = this.connection.channel.id;
151
+ data.streams = [{ type: 'screen', rid: '100', quality: 100 }];
152
+ data.video = true;
153
+ }
154
+
155
+ return this.sendPacket({
156
+ op: VoiceOpcodes.IDENTIFY,
157
+ d: data,
131
158
  });
132
159
  }
133
160
 
@@ -138,18 +165,151 @@ class VoiceWebSocket extends EventEmitter {
138
165
  */
139
166
  onMessage(event) {
140
167
  try {
141
- return this.onPacket(WebSocket.unpack(event.data, 'json'));
168
+ const { data } = event;
169
+ if (data instanceof ArrayBuffer || Buffer.isBuffer(data)) {
170
+ const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data);
171
+ const seq = buffer.readUInt16BE(0);
172
+ const op = buffer.readUInt8(2);
173
+ const payload = buffer.subarray(3);
174
+ this._sequenceNumber = seq;
175
+ this.emit('debug', `[WS] << [bin] opcode ${op}, seq ${seq}, ${payload.byteLength} bytes`);
176
+ return this.onBinaryMessage({ op, seq, payload });
177
+ }
178
+ return this.onPacket(WebSocket.unpack(data, 'json'));
142
179
  } catch (error) {
143
180
  return this.onError(error);
144
181
  }
145
182
  }
146
183
 
184
+ usesSharedDaveSession() {
185
+ return (
186
+ this.connection.constructor.name === 'StreamConnection' &&
187
+ this.connection.voiceConnection?.dave === this.connection.dave
188
+ );
189
+ }
190
+
191
+ onBinaryMessage(message) {
192
+ const { dave } = this.connection;
193
+ if (!dave) return;
194
+ if (this.usesSharedDaveSession()) {
195
+ this.emit('debug', `[WS] << [bin] opcode ${message.op} ignored (shared MLS session)`);
196
+ return;
197
+ }
198
+
199
+ switch (message.op) {
200
+ case VoiceOpcodes.DAVE_MLS_EXTERNAL_SENDER:
201
+ dave.setExternalSender(message.payload);
202
+ break;
203
+ case VoiceOpcodes.DAVE_MLS_PROPOSALS: {
204
+ const payload = dave.processProposals(message.payload, this.connection.connectedClients);
205
+ if (payload) this.sendBinaryMessage(VoiceOpcodes.DAVE_MLS_COMMIT_WELCOME, payload);
206
+ break;
207
+ }
208
+ case VoiceOpcodes.DAVE_MLS_ANNOUNCE_COMMIT_TRANSITION: {
209
+ const { transitionId, success } = dave.processCommit(message.payload);
210
+ if (success) {
211
+ if (transitionId === 0) {
212
+ this.emit('transitioned', transitionId);
213
+ } else {
214
+ this.sendPacket({
215
+ op: VoiceOpcodes.DAVE_TRANSITION_READY,
216
+ d: { transition_id: transitionId },
217
+ });
218
+ }
219
+ }
220
+ break;
221
+ }
222
+ case VoiceOpcodes.DAVE_MLS_WELCOME: {
223
+ const { transitionId, success } = dave.processWelcome(message.payload);
224
+ if (success) {
225
+ if (transitionId === 0) {
226
+ this.emit('transitioned', transitionId);
227
+ } else {
228
+ this.sendPacket({
229
+ op: VoiceOpcodes.DAVE_TRANSITION_READY,
230
+ d: { transition_id: transitionId },
231
+ });
232
+ }
233
+ }
234
+ break;
235
+ }
236
+ default:
237
+ this.emit('unknownPacket', message);
238
+ }
239
+ }
240
+
147
241
  /**
148
242
  * Called whenever the connection to the WebSocket server is lost.
149
243
  * @param {CloseEvent} event The WebSocket close event
150
244
  */
151
245
  onClose(event) {
152
246
  this.emit('debug', `[WS] closed with code ${event.code} and reason: ${event.reason}`);
247
+ if (event.code === 4017) {
248
+ this.dead = true;
249
+ this.emit(
250
+ 'error',
251
+ new Error('VOICE_DAVE_REQUIRED', 'Discord requires DAVE/E2EE protocol support. Ensure @snazzah/davey is installed.'),
252
+ );
253
+ return;
254
+ }
255
+ if (event.code === 4006) {
256
+ const isStream = this.connection.constructor.name === 'StreamConnection';
257
+ this.connection._sessionRetries ??= 0;
258
+ if (this.connection._sessionRetries < 3) {
259
+ this.connection._sessionRetries++;
260
+ this._identified = false;
261
+ this.reset();
262
+ this.connection.authentication = {};
263
+ this.connection.status = VoiceStatus.AUTHENTICATING;
264
+ if (isStream) {
265
+ this.connection.serverId = null;
266
+ if (this.connection.sockets.ws) {
267
+ this.connection.sockets.ws.shutdown();
268
+ this.connection.sockets.ws = null;
269
+ }
270
+ if (this.connection.sockets.udp) {
271
+ this.connection.sockets.udp.shutdown();
272
+ this.connection.sockets.udp = null;
273
+ }
274
+ if (this.connection.voiceConnection) {
275
+ this.connection.voiceConnection.streamConnection = this.connection;
276
+ }
277
+ clearTimeout(this.connection.connectTimeout);
278
+ this.connection.connectTimeout = setTimeout(
279
+ () => this.connection.authenticateFailed('VOICE_CONNECTION_TIMEOUT'),
280
+ 30_000,
281
+ ).unref();
282
+ this.connection.channel.client.ws.broadcast({
283
+ op: Opcodes.STREAM_DELETE,
284
+ d: { stream_key: this.connection.streamKey },
285
+ });
286
+ setTimeout(() => {
287
+ this.connection.sendSignalScreenshare();
288
+ }, 1500).unref();
289
+ } else {
290
+ if (this.connection.sockets.ws) {
291
+ this.connection.sockets.ws.shutdown();
292
+ this.connection.sockets.ws = null;
293
+ }
294
+ if (this.connection.sockets.udp) {
295
+ this.connection.sockets.udp.shutdown();
296
+ this.connection.sockets.udp = null;
297
+ }
298
+ this.connection.status = VoiceStatus.RECONNECTING;
299
+ setTimeout(() => {
300
+ if (this.connection.authentication.token && this.connection.authentication.endpoint) {
301
+ this.connection.connect();
302
+ } else {
303
+ this.connection.sendVoiceStateUpdate();
304
+ }
305
+ }, 500).unref();
306
+ }
307
+ return;
308
+ }
309
+ this.dead = true;
310
+ this.emit('error', new Error('VOICE_SESSION_EXPIRED'));
311
+ return;
312
+ }
153
313
  if (!this.dead) setTimeout(this.connect.bind(this), this.attempts * 1000).unref();
154
314
  }
155
315
 
@@ -162,6 +322,90 @@ class VoiceWebSocket extends EventEmitter {
162
322
  this.emit('error', error);
163
323
  }
164
324
 
325
+ setupDaveSession(protocolVersion) {
326
+ const isStream = this.connection.constructor.name === 'StreamConnection';
327
+ const parentDave = this.connection.voiceConnection?.dave;
328
+
329
+ if (isStream && parentDave?.session) {
330
+ this.connection.dave = parentDave;
331
+ this.connection.connectedClients.add(this.client.user.id);
332
+ for (const id of this.connection.voiceConnection.connectedClients) {
333
+ this.connection.connectedClients.add(id);
334
+ }
335
+ this.emit('debug', '[DAVE] Reusing parent voice connection MLS session');
336
+ return;
337
+ }
338
+
339
+ if (this.connection.dave) {
340
+ const isSharedDave = isStream && this.connection.dave === parentDave;
341
+ if (!isSharedDave) {
342
+ this.connection.dave.destroy();
343
+ this.connection.dave.removeAllListeners();
344
+ }
345
+ }
346
+
347
+ if (!protocolVersion || getMaxProtocolVersion() === 0) {
348
+ this.connection.dave = null;
349
+ return;
350
+ }
351
+
352
+ const session = new DAVESession(
353
+ protocolVersion,
354
+ this.client.user.id,
355
+ this.connection.channel.id,
356
+ );
357
+
358
+ session.on('debug', msg => this.emit('debug', `[DAVE] ${msg}`));
359
+ session.on('keyPackage', keyPackage => {
360
+ this.sendBinaryMessage(VoiceOpcodes.DAVE_MLS_KEY_PACKAGE, keyPackage).catch(e => this.emit('error', e));
361
+ });
362
+ session.on('invalidateTransition', transitionId => {
363
+ this.sendPacket({
364
+ op: VoiceOpcodes.DAVE_MLS_INVALID_COMMIT_WELCOME,
365
+ d: { transition_id: transitionId },
366
+ }).catch(e => this.emit('error', e));
367
+ });
368
+ session.on('error', err => this.emit('error', err));
369
+
370
+ this.connection.dave = session;
371
+ this.connection.connectedClients.add(this.client.user.id);
372
+ if (this.connection.voiceConnection?.connectedClients) {
373
+ for (const id of this.connection.voiceConnection.connectedClients) {
374
+ this.connection.connectedClients.add(id);
375
+ }
376
+ }
377
+ session.reinit();
378
+ }
379
+
380
+ handleDavePacket(packet) {
381
+ const { dave } = this.connection;
382
+ if (!dave) return;
383
+
384
+ switch (packet.op) {
385
+ case VoiceOpcodes.DAVE_PREPARE_TRANSITION: {
386
+ const sendReady = dave.prepareTransition(packet.d);
387
+ if (sendReady) {
388
+ this.sendPacket({
389
+ op: VoiceOpcodes.DAVE_TRANSITION_READY,
390
+ d: { transition_id: packet.d.transition_id },
391
+ });
392
+ }
393
+ if (packet.d.transition_id === 0) this.emit('transitioned', 0);
394
+ break;
395
+ }
396
+ case VoiceOpcodes.DAVE_EXECUTE_TRANSITION: {
397
+ const transitioned = dave.executeTransition(packet.d.transition_id);
398
+ if (transitioned) this.emit('transitioned', packet.d.transition_id);
399
+ break;
400
+ }
401
+ case VoiceOpcodes.DAVE_PREPARE_EPOCH:
402
+ dave.prepareEpoch(packet.d);
403
+ break;
404
+ default:
405
+ break;
406
+ }
407
+ }
408
+
165
409
  /**
166
410
  * Called whenever a valid packet is received from the WebSocket.
167
411
  * @param {Object} packet The received packet
@@ -169,9 +413,24 @@ class VoiceWebSocket extends EventEmitter {
169
413
  onPacket(packet) {
170
414
  this.emit('debug', `[WS] << ${JSON.stringify(packet)}`);
171
415
  if (packet.seq) this._sequenceNumber = packet.seq;
416
+
417
+ if (
418
+ [
419
+ VoiceOpcodes.DAVE_PREPARE_TRANSITION,
420
+ VoiceOpcodes.DAVE_EXECUTE_TRANSITION,
421
+ VoiceOpcodes.DAVE_PREPARE_EPOCH,
422
+ ].includes(packet.op)
423
+ ) {
424
+ if (!this.usesSharedDaveSession()) this.handleDavePacket(packet);
425
+ return;
426
+ }
427
+
172
428
  switch (packet.op) {
173
429
  case VoiceOpcodes.HELLO:
174
430
  this.setHeartbeat(packet.d.heartbeat_interval);
431
+ this.sendIdentify().catch(() => {
432
+ this.emit('error', new Error('VOICE_JOIN_SOCKET_CLOSED'));
433
+ });
175
434
  break;
176
435
  case VoiceOpcodes.READY:
177
436
  /**
@@ -180,11 +439,11 @@ class VoiceWebSocket extends EventEmitter {
180
439
  * @event VoiceWebSocket#ready
181
440
  */
182
441
  this.emit('ready', packet.d);
183
- this.connection.setVideoStatus(false);
184
442
  break;
185
443
  /* eslint-disable no-case-declarations */
186
444
  case VoiceOpcodes.SESSION_DESCRIPTION:
187
445
  packet.d.secret_key = new Uint8Array(packet.d.secret_key);
446
+ this.setupDaveSession(packet.d.dave_protocol_version ?? 0);
188
447
  /**
189
448
  * Emitted once the Voice Websocket receives a description of this voice session.
190
449
  * @param {Object} packet The received packet
@@ -192,6 +451,9 @@ class VoiceWebSocket extends EventEmitter {
192
451
  */
193
452
  this.emit('sessionDescription', packet.d);
194
453
  break;
454
+ case VoiceOpcodes.CLIENTS_CONNECT:
455
+ for (const id of packet.d.user_ids) this.connection.connectedClients.add(id);
456
+ break;
195
457
  case VoiceOpcodes.CLIENT_CONNECT:
196
458
  this.connection.ssrcMap.set(+packet.d.audio_ssrc, {
197
459
  userId: packet.d.user_id,
@@ -200,6 +462,7 @@ class VoiceWebSocket extends EventEmitter {
200
462
  });
201
463
  break;
202
464
  case VoiceOpcodes.CLIENT_DISCONNECT:
465
+ this.connection.connectedClients.delete(packet.d.user_id);
203
466
  const streamInfo = this.connection.receiver && this.connection.receiver.packets.streams.get(packet.d.user_id);
204
467
  if (streamInfo) {
205
468
  this.connection.receiver.packets.streams.delete(packet.d.user_id);
@@ -258,10 +521,7 @@ class VoiceWebSocket extends EventEmitter {
258
521
  * Clears a heartbeat interval, if one exists.
259
522
  */
260
523
  clearHeartbeat() {
261
- if (!this.heartbeatInterval) {
262
- this.emit('warn', 'Tried to clear a heartbeat interval that does not exist');
263
- return;
264
- }
524
+ if (!this.heartbeatInterval) return;
265
525
  clearInterval(this.heartbeatInterval);
266
526
  this.heartbeatInterval = null;
267
527
  }
@@ -214,7 +214,9 @@ class MediaPlayer extends EventEmitter {
214
214
  args.push(...FFMPEG_H265_ARGUMENTS(options));
215
215
  }
216
216
 
217
- args.push('-force_key_frames', '00:02');
217
+ if (options?.fps) {
218
+ args.push('-g', String(options.fps), '-keyint_min', String(options.fps), '-sc_threshold', '0');
219
+ }
218
220
 
219
221
  if (options?.inputFFmpegArgs) {
220
222
  args.unshift(...options.inputFFmpegArgs);