djs-selfbot-v13 3.7.33 → 3.7.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "djs-selfbot-v13",
3
- "version": "3.7.33",
3
+ "version": "3.7.34",
4
4
  "description": "An unofficial discord.js fork for creating selfbots",
5
5
  "main": "./src/index.js",
6
6
  "types": "./typings/index.d.ts",
@@ -51,6 +51,8 @@
51
51
  },
52
52
  "homepage": "https://djs-selfbot.vercel.app/",
53
53
  "dependencies": {
54
+ "@dank074/discord-video-stream": "^6.0.0",
55
+ "@snazzah/davey": "^0.1.11",
54
56
  "@discordjs/builders": "^1.6.3",
55
57
  "@discordjs/collection": "^2.1.1",
56
58
  "@sapphire/async-queue": "^1.5.5",
@@ -68,6 +70,11 @@
68
70
  "werift-rtp": "^0.8.4",
69
71
  "ws": "^8.16.0"
70
72
  },
73
+ "optionalDependencies": {
74
+ "@snazzah/davey-linux-x64-gnu": "0.1.11",
75
+ "@snazzah/davey-linux-x64-musl": "0.1.11",
76
+ "@snazzah/davey-win32-x64-msvc": "0.1.11"
77
+ },
71
78
  "engines": {
72
79
  "node": ">=20.18"
73
80
  },
@@ -10,6 +10,7 @@ const BaseClient = require('./BaseClient');
10
10
  const ActionsManager = require('./actions/ActionsManager');
11
11
  const ClientVoiceManager = require('./voice/ClientVoiceManager');
12
12
  const StreamSession = require('./voice/StreamSession');
13
+ const WebRtcStreamSession = require('./voice/WebRtcStreamSession');
13
14
  const WebSocketManager = require('./websocket/WebSocketManager');
14
15
  const { Error, TypeError } = require('../errors');
15
16
  const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
@@ -920,51 +921,44 @@ class Client extends BaseClient {
920
921
  guildId,
921
922
  channelId,
922
923
  url,
923
- fps = 30,
924
+ fps,
924
925
  height,
925
926
  width,
926
- bitrate = 2000,
927
+ bitrate = 5000,
928
+ bitrateMax,
927
929
  audioBitrate,
928
- videoCodec = 'H264',
929
930
  preset,
931
+ tune,
930
932
  audio,
931
- video = false,
933
+ livestream = false,
934
+ downloadHttp = true,
932
935
  } = options;
933
936
 
934
937
  if (!url) throw new Error('STREAM_URL_REQUIRED');
935
938
  if (!guildId || !channelId) throw new Error('STREAM_CHANNEL_REQUIRED');
936
939
 
937
- const channel =
938
- this.guilds.cache.get(guildId)?.channels.cache.get(channelId) ?? this.channels.cache.get(channelId);
939
-
940
- if (!channel) throw new Error('STREAM_CHANNEL_NOT_FOUND');
941
-
942
- const voiceConnection = await this.voice.joinChannel(channel, {
943
- selfVideo: Boolean(video),
944
- videoCodec,
945
- });
946
-
947
- const streamConnection = await voiceConnection.createStreamConnection();
948
-
949
- if (!video && voiceConnection.status === VoiceStatus.CONNECTED) {
950
- voiceConnection.setVideoStatus(false);
940
+ let playUrl = url;
941
+ if (typeof url === 'string' && url.startsWith('http') && downloadHttp) {
942
+ playUrl = await WebRtcStreamSession.resolveUrl(url);
951
943
  }
952
944
 
953
- await streamConnection.waitForDaveReady();
954
- streamConnection.sendScreenshareState(false);
955
-
956
- const session = new StreamSession(this, voiceConnection, streamConnection, {
957
- url,
945
+ const session = new WebRtcStreamSession(this, {
946
+ guildId,
947
+ channelId,
948
+ url: playUrl,
958
949
  fps,
959
950
  height,
960
951
  width,
961
952
  bitrate,
953
+ bitrateMax,
962
954
  audioBitrate,
963
955
  preset,
956
+ tune,
964
957
  audio,
958
+ livestream,
965
959
  });
966
960
 
967
- session._play();
961
+ await session.start();
968
962
  return session;
969
963
  }
970
964
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const VoiceConnection = require('./VoiceConnection');
4
4
  const { Error } = require('../../errors');
5
- const { Events, VoiceStatus, Opcodes } = require('../../util/Constants');
5
+ const { Events, VoiceStatus, Opcodes } = require('../../util/Constants');
6
6
 
7
7
  /**
8
8
  * Manages voice connections for the client
@@ -60,7 +60,7 @@ class ClientVoiceManager {
60
60
 
61
61
  onVoiceStateUpdate(payload) {
62
62
  const { guild_id, session_id, channel_id } = payload;
63
- if (payload.user_id !== this.client.user?.id) return;
63
+ if (payload.user_id !== this.client.user?.id) return;
64
64
  // @discordjs/voice
65
65
  if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
66
66
  this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
@@ -72,9 +72,9 @@ class ClientVoiceManager {
72
72
  this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`);
73
73
  if (!connection) return;
74
74
  if (!channel_id) {
75
- if (connection.status === VoiceStatus.AUTHENTICATING || connection.status === VoiceStatus.CONNECTING) {
76
- return;
77
- }
75
+ if (connection.status === VoiceStatus.AUTHENTICATING || connection.status === VoiceStatus.CONNECTING) {
76
+ return;
77
+ }
78
78
  connection._disconnect();
79
79
  this.connection = null;
80
80
  return;
@@ -97,62 +97,73 @@ class ClientVoiceManager {
97
97
  * @typedef {Object} JoinChannelConfig
98
98
  */
99
99
 
100
- /**
101
- * Clears stale voice/stream state before joining a channel.
102
- * @param {VoiceChannel} channel The channel to join
103
- * @returns {Promise<void>}
104
- * @private
105
- */
106
- preJoinCleanup(channel) {
107
- return new Promise(resolve => {
108
- const userId = this.client.user?.id;
109
- const voiceState = channel.guild?.voiceStates.cache.get(userId);
110
- const inTarget = voiceState?.channelId === channel.id;
111
- const isStreaming = voiceState?.streaming || this.client.user?.voice?.streaming;
112
- const needsCleanup = inTarget && (isStreaming || !this.connection);
113
-
114
- if (!needsCleanup) {
115
- resolve();
116
- return;
117
- }
118
-
119
- const streamKey = `guild:${channel.guild.id}:${channel.id}:${userId}`;
120
- let settled = false;
121
- const done = () => {
122
- if (settled) return;
123
- settled = true;
124
- this.client.removeListener(Events.VOICE_STATE_UPDATE, onState);
125
- setTimeout(resolve, 400).unref();
126
- };
127
-
128
- const onState = (_old, newState) => {
129
- if (newState.id !== userId) return;
130
- if (!newState.channelId && !newState.streaming) done();
131
- };
132
-
133
- this.client.on(Events.VOICE_STATE_UPDATE, onState);
134
-
135
- if (isStreaming) {
136
- this.client.ws.broadcast({
137
- op: Opcodes.STREAM_DELETE,
138
- d: { stream_key: streamKey },
139
- });
140
- }
141
-
142
- this.client.ws.broadcast({
143
- op: Opcodes.VOICE_STATE_UPDATE,
144
- d: {
145
- guild_id: channel.guild.id,
146
- channel_id: null,
147
- self_mute: false,
148
- self_deaf: false,
149
- },
150
- });
151
-
152
- setTimeout(done, 4000).unref();
153
- });
154
- }
155
-
100
+ /**
101
+ * Clears stale voice/stream state before joining a channel.
102
+ * @param {VoiceChannel} channel The channel to join
103
+ * @returns {Promise<void>}
104
+ * @private
105
+ */
106
+ preJoinCleanup(channel) {
107
+ return new Promise(resolve => {
108
+ const guild = channel.guild;
109
+ if (!guild) {
110
+ resolve();
111
+ return;
112
+ }
113
+
114
+ const userId = this.client.user?.id;
115
+ const voiceState = guild.voiceStates.cache.get(userId);
116
+ const inVoice = Boolean(voiceState?.channelId);
117
+ const isStreaming = Boolean(voiceState?.streaming);
118
+
119
+ if (this.connection) {
120
+ this.connection.disconnect();
121
+ this.connection = null;
122
+ }
123
+
124
+ if (!inVoice && !isStreaming) {
125
+ resolve();
126
+ return;
127
+ }
128
+
129
+ const streamKey = `guild:${guild.id}:${voiceState.channelId}:${userId}`;
130
+ let settled = false;
131
+ const done = () => {
132
+ if (settled) return;
133
+ settled = true;
134
+ this.client.removeListener(Events.VOICE_STATE_UPDATE, onState);
135
+ setTimeout(resolve, 500).unref();
136
+ };
137
+
138
+ const onState = (_old, newState) => {
139
+ if (newState.id !== userId || newState.guild?.id !== guild.id) return;
140
+ if (!newState.channelId && !newState.streaming) done();
141
+ };
142
+
143
+ this.client.on(Events.VOICE_STATE_UPDATE, onState);
144
+ this.client.emit('debug', `[VOICE] preJoinCleanup: quitte le salon ${voiceState.channelId}`);
145
+
146
+ if (isStreaming) {
147
+ this.client.ws.broadcast({
148
+ op: Opcodes.STREAM_DELETE,
149
+ d: { stream_key: streamKey },
150
+ });
151
+ }
152
+
153
+ this.client.ws.broadcast({
154
+ op: Opcodes.VOICE_STATE_UPDATE,
155
+ d: {
156
+ guild_id: guild.id,
157
+ channel_id: null,
158
+ self_mute: false,
159
+ self_deaf: false,
160
+ },
161
+ });
162
+
163
+ setTimeout(done, 5000).unref();
164
+ });
165
+ }
166
+
156
167
  /**
157
168
  * Sets up a request to join a voice channel.
158
169
  * @param {VoiceChannel | StageChannel | DMChannel | GroupDMChannel | Snowflake} channel The voice channel to join
@@ -166,32 +177,32 @@ class ClientVoiceManager {
166
177
  throw new Error('VOICE_JOIN_CHANNEL', channel.full);
167
178
  }
168
179
 
169
- const startJoin = () => {
180
+ const startJoin = () => {
170
181
  let connection = this.connection;
171
182
 
172
- if (connection?.status === VoiceStatus.CONNECTED && connection.channel.id === channel.id) {
183
+ if (connection?.status === VoiceStatus.CONNECTED && connection.channel.id === channel.id) {
173
184
  resolve(connection);
174
185
  return;
175
186
  }
176
187
 
177
- if (connection) {
178
- connection.disconnect();
179
- this.connection = null;
180
- connection = null;
181
- }
182
-
183
- connection = new VoiceConnection(this, channel);
184
- if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
185
- connection.on('debug', msg =>
186
- this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`),
187
- );
188
- connection.authenticate({
189
- self_mute: Boolean(config.selfMute),
190
- self_deaf: Boolean(config.selfDeaf),
191
- self_video: Boolean(config.selfVideo),
192
- });
193
- this.connection = connection;
194
-
188
+ if (connection) {
189
+ connection.disconnect();
190
+ this.connection = null;
191
+ connection = null;
192
+ }
193
+
194
+ connection = new VoiceConnection(this, channel);
195
+ if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
196
+ connection.on('debug', msg =>
197
+ this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`),
198
+ );
199
+ connection.authenticate({
200
+ self_mute: Boolean(config.selfMute),
201
+ self_deaf: Boolean(config.selfDeaf),
202
+ self_video: Boolean(config.selfVideo),
203
+ });
204
+ this.connection = connection;
205
+
195
206
  connection.once('failed', reason => {
196
207
  this.connection = null;
197
208
  reject(reason);
@@ -208,9 +219,9 @@ class ClientVoiceManager {
208
219
  this.connection = null;
209
220
  });
210
221
  });
211
- };
212
-
213
- this.preJoinCleanup(channel).then(startJoin).catch(reject);
222
+ };
223
+
224
+ this.preJoinCleanup(channel).then(startJoin).catch(reject);
214
225
  });
215
226
  }
216
227
  }
@@ -1,5 +1,8 @@
1
1
  'use strict';
2
2
 
3
+ const fs = require('node:fs');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
3
6
  const { EventEmitter } = require('events');
4
7
 
5
8
  /**
@@ -7,6 +10,21 @@ const { EventEmitter } = require('events');
7
10
  * @extends {EventEmitter}
8
11
  */
9
12
  class StreamSession extends EventEmitter {
13
+ /**
14
+ * Downloads an HTTP(S) video to a temp file (recommended on VPS).
15
+ * @param {string} url Remote video URL
16
+ * @returns {Promise<string>} Local file path
17
+ */
18
+ static async resolveUrl(url) {
19
+ const response = await fetch(url);
20
+ if (!response.ok) throw new Error(`STREAM_URL_HTTP_${response.status}`);
21
+ const buffer = Buffer.from(await response.arrayBuffer());
22
+ if (buffer.length < 1024) throw new Error('STREAM_URL_INVALID');
23
+ const tmpPath = path.join(os.tmpdir(), `djs-stream-${Date.now()}.mp4`);
24
+ fs.writeFileSync(tmpPath, buffer);
25
+ console.log(`[stream] vidéo téléchargée: ${tmpPath} (${(buffer.length / 1024 / 1024).toFixed(2)} Mo)`);
26
+ return tmpPath;
27
+ }
10
28
  /**
11
29
  * @param {import('../Client')} client Discord client
12
30
  * @param {import('./VoiceConnection')} voiceConnection Voice connection
@@ -68,6 +86,8 @@ class StreamSession extends EventEmitter {
68
86
 
69
87
  if (!url.startsWith('http')) {
70
88
  videoOptions.inputFFmpegArgs = ['-re'];
89
+ } else {
90
+ videoOptions.inputFFmpegArgs = ['-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1'];
71
91
  }
72
92
 
73
93
  this.videoDispatcher = this.streamConnection.playVideo(url, videoOptions);
@@ -80,6 +100,8 @@ class StreamSession extends EventEmitter {
80
100
  };
81
101
  if (!url.startsWith('http')) {
82
102
  audioOptions.inputFFmpegArgs = ['-re'];
103
+ } else {
104
+ audioOptions.inputFFmpegArgs = ['-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1'];
83
105
  }
84
106
  this.voiceAudioDispatcher = this.voiceConnection.playAudio(url, audioOptions);
85
107
  this.audioDispatcher = this.streamConnection.playAudio(url, audioOptions);
@@ -165,4 +187,7 @@ module.exports = StreamSession;
165
187
  * @property {string} [preset='ultrafast'] x264 preset
166
188
  * @property {boolean} [audio=true] Whether to play audio
167
189
  * @property {boolean} [video=false] Enable webcam (false = screenshare only)
190
+ * @property {boolean} [downloadHttp=true] Download HTTP URLs locally before playback
191
+ * @property {number} [bitrateMax] Max video bitrate in kbps
192
+ * @property {boolean} [livestream=false] Enable readrateInitialBurst (live sources)
168
193
  */
@@ -498,6 +498,7 @@ class VoiceConnection extends EventEmitter {
498
498
  * @private
499
499
  */
500
500
  authenticate(options = {}) {
501
+ this._joinOptions = options;
501
502
  this.sendVoiceStateUpdate(options);
502
503
  this.connectTimeout = setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 30_000).unref();
503
504
  }
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ const { EventEmitter } = require('events');
4
+ const { PassThrough } = require('stream');
5
+ const ffmpeg = require('fluent-ffmpeg');
6
+ const { Streamer, playStream } = require('@dank074/discord-video-stream');
7
+ const StreamSession = require('./StreamSession');
8
+ const StageChannel = require('../../structures/StageChannel');
9
+
10
+ if (typeof globalThis.WebSocket === 'undefined') {
11
+ globalThis.WebSocket = require('ws');
12
+ }
13
+
14
+ let _ffmpegPathReady = null;
15
+
16
+ /** Utilise le ffmpeg jellyfin de node-av (libx264/libopus) si disponible. */
17
+ function ensureFfmpegPath() {
18
+ if (_ffmpegPathReady) return _ffmpegPathReady;
19
+ _ffmpegPathReady = import('node-av/ffmpeg')
20
+ .then(({ ffmpegPath, isFfmpegAvailable }) => {
21
+ if (!isFfmpegAvailable()) {
22
+ console.warn('[stream] ffmpeg node-av introuvable — ffmpeg système utilisé (vérifie libx264/libopus)');
23
+ return null;
24
+ }
25
+ const path = ffmpegPath();
26
+ ffmpeg.setFfmpegPath(path);
27
+ console.log(`[stream] ffmpeg: ${path}`);
28
+ return path;
29
+ })
30
+ .catch(() => {
31
+ console.warn('[stream] impossible de charger node-av/ffmpeg — ffmpeg système utilisé');
32
+ return null;
33
+ });
34
+ return _ffmpegPathReady;
35
+ }
36
+
37
+ /**
38
+ * Go Live via WebRTC — ffmpeg custom (sans azmq) + playStream de la librairie.
39
+ * @extends {EventEmitter}
40
+ */
41
+ class WebRtcStreamSession extends EventEmitter {
42
+ static resolveUrl = StreamSession.resolveUrl;
43
+
44
+ /**
45
+ * @param {import('../Client')} client
46
+ * @param {import('./StreamSession').StartStreamOptions} options
47
+ */
48
+ constructor(client, options) {
49
+ super();
50
+ this.client = client;
51
+ this.options = options;
52
+ this.streamer = new Streamer(client);
53
+ this._abort = new AbortController();
54
+ this._playPromise = null;
55
+ this._command = null;
56
+ this._started = false;
57
+ }
58
+
59
+ /**
60
+ * Construit la commande ffmpeg : transcode en H264 + Opus, sortie nut.
61
+ * Pas de filtre azmq (compatibilité ffmpeg sans libzmq).
62
+ */
63
+ _buildFfmpeg(url) {
64
+ const {
65
+ fps,
66
+ height = 720,
67
+ width = -2,
68
+ bitrate = 5000,
69
+ bitrateMax,
70
+ audioBitrate = 128,
71
+ preset = 'superfast',
72
+ tune = 'film',
73
+ audio,
74
+ } = this.options;
75
+
76
+ const maxRate = bitrateMax ?? Math.round(bitrate * 1.5);
77
+ const includeAudio = audio !== false;
78
+ const output = new PassThrough();
79
+ const command = ffmpeg(url);
80
+
81
+ const isHttp = typeof url === 'string' && /^https?:\/\//i.test(url);
82
+ if (isHttp) {
83
+ command.inputOptions([
84
+ '-reconnect 1',
85
+ '-reconnect_at_eof 1',
86
+ '-reconnect_streamed 1',
87
+ '-reconnect_delay_max 4294',
88
+ ]);
89
+ }
90
+
91
+ command.output(output).outputFormat('nut');
92
+
93
+ command.addOutputOption('-map 0:v');
94
+ command.videoFilter(`scale=${width}:${height}`);
95
+ // fps forcé seulement si demandé explicitement (sinon on garde celui de la source = vitesse 1x)
96
+ if (fps && fps > 0) command.fpsOutput(fps);
97
+ command.addOutputOption([
98
+ '-b:v',
99
+ `${bitrate}k`,
100
+ '-maxrate:v',
101
+ `${maxRate}k`,
102
+ '-bufsize:v',
103
+ `${Math.round(bitrate / 2)}k`,
104
+ '-bf',
105
+ '0',
106
+ '-pix_fmt',
107
+ 'yuv420p',
108
+ '-force_key_frames',
109
+ 'expr:gte(t,n_forced*1)',
110
+ ]);
111
+ command.videoCodec('libx264');
112
+ command.outputOptions(['-forced-idr 1', `-tune ${tune}`, `-preset ${preset}`]);
113
+
114
+ if (includeAudio) {
115
+ command.addOutputOption('-map 0:a:0?');
116
+ command.audioChannels(2);
117
+ command.audioFrequency(48000);
118
+ command.audioCodec('libopus');
119
+ command.audioBitrate(`${audioBitrate}k`);
120
+ command.audioFilters('volume=1');
121
+ }
122
+
123
+ return { command, output };
124
+ }
125
+
126
+ async _run(url, signal) {
127
+ await ensureFfmpegPath();
128
+
129
+ const { command, output } = this._buildFfmpeg(url);
130
+ this._command = command;
131
+
132
+ command.on('start', cmd => this.emit('debug', `[cmd] ${cmd}`));
133
+ command.on('stderr', line => this.emit('debug', String(line)));
134
+
135
+ command.on('error', (err, _stdout, stderr) => {
136
+ if (stderr) this.emit('debug', String(stderr));
137
+ if (signal.aborted) return;
138
+ const msg = err?.message || String(err);
139
+ this.emit('error', new Error(stderr ? `${msg}\n${stderr}` : msg));
140
+ });
141
+
142
+ signal.addEventListener('abort', () => command.kill('SIGTERM'), { once: true });
143
+ command.run();
144
+
145
+ const { livestream } = this.options;
146
+ const playOptions = { type: 'go-live' };
147
+ if (livestream) playOptions.readrateInitialBurst = 10;
148
+
149
+ this._playPromise = playStream(output, this.streamer, playOptions, signal);
150
+
151
+ this._playPromise
152
+ .then(() => this.emit('finish'))
153
+ .catch(err => {
154
+ if (signal.aborted || err?.name === 'AbortError') return;
155
+ this.emit('error', err);
156
+ });
157
+
158
+ return this._playPromise;
159
+ }
160
+
161
+ async start() {
162
+ const { guildId, channelId, url } = this.options;
163
+ await ensureFfmpegPath();
164
+ await this.streamer.joinVoice(guildId, channelId);
165
+
166
+ if (this.client.user?.voice?.channel instanceof StageChannel) {
167
+ await this.client.user.voice.setSuppressed(false);
168
+ }
169
+
170
+ this._started = true;
171
+ void this._run(url, this._abort.signal);
172
+ }
173
+
174
+ stop() {
175
+ this._abort.abort();
176
+ }
177
+
178
+ async replay() {
179
+ this.stop();
180
+ this._abort = new AbortController();
181
+ await this._run(this.options.url, this._abort.signal);
182
+ }
183
+
184
+ disconnect() {
185
+ this.stop();
186
+ if (this._started) this.streamer.leaveVoice();
187
+ this._started = false;
188
+ }
189
+ }
190
+
191
+ module.exports = WebRtcStreamSession;
@@ -138,13 +138,15 @@ class VoiceWebSocket extends EventEmitter {
138
138
  this._identified = true;
139
139
 
140
140
  const isStream = this.connection.constructor.name === 'StreamConnection';
141
+ const sessionId = this.connection.authentication.sessionId;
142
+ const maxDave = getMaxProtocolVersion();
141
143
  const data = {
142
144
  server_id: this.connection.serverId || this.connection.channel.guild?.id || this.connection.channel.id,
143
145
  user_id: this.client.user.id,
144
146
  token: this.connection.authentication.token,
145
- session_id: this.connection.authentication.sessionId,
146
- max_dave_protocol_version: getMaxProtocolVersion(),
147
+ session_id: sessionId,
147
148
  };
149
+ if (maxDave > 0) data.max_dave_protocol_version = maxDave;
148
150
 
149
151
  if (isStream) {
150
152
  data.channel_id = this.connection.channel.id;
@@ -253,60 +255,14 @@ class VoiceWebSocket extends EventEmitter {
253
255
  return;
254
256
  }
255
257
  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
258
  this.dead = true;
259
+ const guildId = this.connection.channel.guild?.id;
260
+ if (guildId) {
261
+ this.connection.channel.client.ws.broadcast({
262
+ op: Opcodes.VOICE_STATE_UPDATE,
263
+ d: { guild_id: guildId, channel_id: null, self_mute: false, self_deaf: false },
264
+ });
265
+ }
310
266
  this.emit('error', new Error('VOICE_SESSION_EXPIRED'));
311
267
  return;
312
268
  }
@@ -186,9 +186,11 @@ const Messages = {
186
186
  VOICE_USER_MISSING: "Couldn't resolve the user to create stream.",
187
187
  VOICE_JOIN_CHANNEL: (full = false) =>
188
188
  `You do not have permission to join this voice channel${full ? '; it is full.' : '.'}`,
189
- VOICE_CONNECTION_TIMEOUT: 'Connection not established within 30 seconds.',
189
+ VOICE_CONNECTION_TIMEOUT:
190
+ 'Connection not established within 30 seconds. Close the official Discord app (desktop/mobile) on this account, leave any voice channel, then retry.',
190
191
  VOICE_DAVE_REQUIRED: '%s',
191
- VOICE_SESSION_EXPIRED: 'Voice session is no longer valid.',
192
+ VOICE_SESSION_EXPIRED:
193
+ 'Voice session is no longer valid (4006). Quit any other Discord client on this account, leave voice channels, then retry.',
192
194
  VOICE_TOKEN_ABSENT: 'Token not provided from voice server packet.',
193
195
  VOICE_SESSION_ABSENT: 'Session ID not supplied.',
194
196
  VOICE_INVALID_ENDPOINT: 'Invalid endpoint received.',
@@ -1196,6 +1196,7 @@ export interface StartStreamOptions {
1196
1196
  url: string;
1197
1197
  fps?: number;
1198
1198
  height?: number;
1199
+ width?: number;
1199
1200
  bitrate?: number;
1200
1201
  audioBitrate?: number;
1201
1202
  videoCodec?: 'H264' | 'VP8';
@@ -1203,6 +1204,12 @@ export interface StartStreamOptions {
1203
1204
  audio?: boolean;
1204
1205
  /** Enable webcam. Default false (screenshare only). */
1205
1206
  video?: boolean;
1207
+ /** Download HTTP URLs locally before playback. Default true. */
1208
+ downloadHttp?: boolean;
1209
+ /** Max video bitrate in kbps. Default: bitrate * 1.5 */
1210
+ bitrateMax?: number;
1211
+ /** Low-latency encoding and playback. Default true. */
1212
+ lowLatency?: boolean;
1206
1213
  }
1207
1214
 
1208
1215
  export class StreamSession extends EventEmitter {