djs-selfbot-v13 3.7.30 → 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.
- package/package.json +1 -1
- package/src/client/Client.js +74 -1
- package/src/client/voice/ClientVoiceManager.js +84 -17
- package/src/client/voice/StreamSession.js +168 -0
- package/src/client/voice/VoiceConnection.js +265 -19
- package/src/client/voice/dispatcher/AnnexBDispatcher.js +64 -11
- package/src/client/voice/dispatcher/BaseDispatcher.js +13 -9
- package/src/client/voice/dispatcher/VideoDispatcher.js +33 -0
- package/src/client/voice/networking/DAVESession.js +234 -0
- package/src/client/voice/networking/VoiceWebSocket.js +284 -24
- package/src/client/voice/player/MediaPlayer.js +3 -1
- package/src/client/voice/player/processing/AnnexBBitstreamReaderWriter.js +137 -0
- package/src/client/voice/player/processing/SPSVUIRewriter.js +203 -0
- package/src/client/voice/receiver/PacketHandler.js +11 -4
- package/src/errors/Messages.js +6 -1
- package/src/managers/BackupManager.js +1 -1
- package/src/{util → managers}/backup/index.js +1 -1
- package/src/{util → managers}/backup/load.js +1 -1
- package/src/util/Constants.js +12 -0
- package/typings/index.d.ts +29 -0
- /package/src/{util → managers}/backup/create.js +0 -0
- /package/src/{util → managers}/backup/util.js +0 -0
package/package.json
CHANGED
package/src/client/Client.js
CHANGED
|
@@ -9,6 +9,7 @@ const { authenticator } = require('otplib');
|
|
|
9
9
|
const BaseClient = require('./BaseClient');
|
|
10
10
|
const ActionsManager = require('./actions/ActionsManager');
|
|
11
11
|
const ClientVoiceManager = require('./voice/ClientVoiceManager');
|
|
12
|
+
const StreamSession = require('./voice/StreamSession');
|
|
12
13
|
const WebSocketManager = require('./websocket/WebSocketManager');
|
|
13
14
|
const { Error, TypeError } = require('../errors');
|
|
14
15
|
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
|
|
@@ -36,7 +37,7 @@ const VoiceRegion = require('../structures/VoiceRegion');
|
|
|
36
37
|
const Webhook = require('../structures/Webhook');
|
|
37
38
|
const Widget = require('../structures/Widget');
|
|
38
39
|
const Application = require('../structures/interfaces/Application');
|
|
39
|
-
const { Events, Status } = require('../util/Constants');
|
|
40
|
+
const { Events, Status, VoiceStatus } = require('../util/Constants');
|
|
40
41
|
const DataResolver = require('../util/DataResolver');
|
|
41
42
|
const Intents = require('../util/Intents');
|
|
42
43
|
const DiscordAuthWebsocket = require('../util/RemoteAuth');
|
|
@@ -895,6 +896,78 @@ class Client extends BaseClient {
|
|
|
895
896
|
});
|
|
896
897
|
}
|
|
897
898
|
|
|
899
|
+
/**
|
|
900
|
+
* Joins a voice channel, starts a screenshare stream and plays a video.
|
|
901
|
+
* @param {import('./voice/StreamSession').StartStreamOptions} options Stream options
|
|
902
|
+
* @returns {Promise<import('./voice/StreamSession')>}
|
|
903
|
+
* @example
|
|
904
|
+
* const stream = await client.startStream({
|
|
905
|
+
* guildId: '123',
|
|
906
|
+
* channelId: '456',
|
|
907
|
+
* url: 'video.mp4',
|
|
908
|
+
* fps: 60,
|
|
909
|
+
* height: 720,
|
|
910
|
+
* bitrate: 4500,
|
|
911
|
+
* });
|
|
912
|
+
* stream.pause();
|
|
913
|
+
* stream.resume();
|
|
914
|
+
* stream.stop();
|
|
915
|
+
* stream.replay();
|
|
916
|
+
* stream.disconnect();
|
|
917
|
+
*/
|
|
918
|
+
async startStream(options = {}) {
|
|
919
|
+
const {
|
|
920
|
+
guildId,
|
|
921
|
+
channelId,
|
|
922
|
+
url,
|
|
923
|
+
fps = 30,
|
|
924
|
+
height,
|
|
925
|
+
width,
|
|
926
|
+
bitrate = 2000,
|
|
927
|
+
audioBitrate,
|
|
928
|
+
videoCodec = 'H264',
|
|
929
|
+
preset,
|
|
930
|
+
audio,
|
|
931
|
+
video = false,
|
|
932
|
+
} = options;
|
|
933
|
+
|
|
934
|
+
if (!url) throw new Error('STREAM_URL_REQUIRED');
|
|
935
|
+
if (!guildId || !channelId) throw new Error('STREAM_CHANNEL_REQUIRED');
|
|
936
|
+
|
|
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);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
await streamConnection.waitForDaveReady();
|
|
954
|
+
streamConnection.sendScreenshareState(false);
|
|
955
|
+
|
|
956
|
+
const session = new StreamSession(this, voiceConnection, streamConnection, {
|
|
957
|
+
url,
|
|
958
|
+
fps,
|
|
959
|
+
height,
|
|
960
|
+
width,
|
|
961
|
+
bitrate,
|
|
962
|
+
audioBitrate,
|
|
963
|
+
preset,
|
|
964
|
+
audio,
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
session._play();
|
|
968
|
+
return session;
|
|
969
|
+
}
|
|
970
|
+
|
|
898
971
|
/**
|
|
899
972
|
* Calls {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
|
|
900
973
|
* with the client as `this`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const VoiceConnection = require('./VoiceConnection');
|
|
4
4
|
const { Error } = require('../../errors');
|
|
5
|
-
const { Events } = require('../../util/Constants');
|
|
5
|
+
const { Events, VoiceStatus, Opcodes } = require('../../util/Constants');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Manages voice connections for the client
|
|
@@ -60,6 +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
64
|
// @discordjs/voice
|
|
64
65
|
if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
|
|
65
66
|
this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
|
|
@@ -71,6 +72,9 @@ class ClientVoiceManager {
|
|
|
71
72
|
this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`);
|
|
72
73
|
if (!connection) return;
|
|
73
74
|
if (!channel_id) {
|
|
75
|
+
if (connection.status === VoiceStatus.AUTHENTICATING || connection.status === VoiceStatus.CONNECTING) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
74
78
|
connection._disconnect();
|
|
75
79
|
this.connection = null;
|
|
76
80
|
return;
|
|
@@ -93,6 +97,62 @@ class ClientVoiceManager {
|
|
|
93
97
|
* @typedef {Object} JoinChannelConfig
|
|
94
98
|
*/
|
|
95
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
|
+
|
|
96
156
|
/**
|
|
97
157
|
* Sets up a request to join a voice channel.
|
|
98
158
|
* @param {VoiceChannel | StageChannel | DMChannel | GroupDMChannel | Snowflake} channel The voice channel to join
|
|
@@ -106,28 +166,32 @@ class ClientVoiceManager {
|
|
|
106
166
|
throw new Error('VOICE_JOIN_CHANNEL', channel.full);
|
|
107
167
|
}
|
|
108
168
|
|
|
169
|
+
const startJoin = () => {
|
|
109
170
|
let connection = this.connection;
|
|
110
171
|
|
|
111
|
-
if (connection) {
|
|
112
|
-
if (connection.channel.id !== channel.id) {
|
|
113
|
-
this.connection.updateChannel(channel);
|
|
114
|
-
}
|
|
172
|
+
if (connection?.status === VoiceStatus.CONNECTED && connection.channel.id === channel.id) {
|
|
115
173
|
resolve(connection);
|
|
116
174
|
return;
|
|
117
|
-
} else {
|
|
118
|
-
connection = new VoiceConnection(this, channel);
|
|
119
|
-
if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
|
|
120
|
-
connection.on('debug', msg =>
|
|
121
|
-
this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`),
|
|
122
|
-
);
|
|
123
|
-
connection.authenticate({
|
|
124
|
-
self_mute: Boolean(config.selfMute),
|
|
125
|
-
self_deaf: Boolean(config.selfDeaf),
|
|
126
|
-
self_video: Boolean(config.selfVideo),
|
|
127
|
-
});
|
|
128
|
-
this.connection = connection;
|
|
129
175
|
}
|
|
130
176
|
|
|
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
|
+
|
|
131
195
|
connection.once('failed', reason => {
|
|
132
196
|
this.connection = null;
|
|
133
197
|
reject(reason);
|
|
@@ -144,6 +208,9 @@ class ClientVoiceManager {
|
|
|
144
208
|
this.connection = null;
|
|
145
209
|
});
|
|
146
210
|
});
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
this.preJoinCleanup(channel).then(startJoin).catch(reject);
|
|
147
214
|
});
|
|
148
215
|
}
|
|
149
216
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EventEmitter } = require('events');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Active screenshare session returned by {@link Client#startStream}.
|
|
7
|
+
* @extends {EventEmitter}
|
|
8
|
+
*/
|
|
9
|
+
class StreamSession extends EventEmitter {
|
|
10
|
+
/**
|
|
11
|
+
* @param {import('../Client')} client Discord client
|
|
12
|
+
* @param {import('./VoiceConnection')} voiceConnection Voice connection
|
|
13
|
+
* @param {import('./VoiceConnection').StreamConnection} streamConnection Stream connection
|
|
14
|
+
* @param {StartStreamOptions} options Stream options
|
|
15
|
+
*/
|
|
16
|
+
constructor(client, voiceConnection, streamConnection, options) {
|
|
17
|
+
super();
|
|
18
|
+
this.client = client;
|
|
19
|
+
this.voiceConnection = voiceConnection;
|
|
20
|
+
this.streamConnection = streamConnection;
|
|
21
|
+
this.options = options;
|
|
22
|
+
this.videoDispatcher = null;
|
|
23
|
+
this.audioDispatcher = null;
|
|
24
|
+
this.voiceAudioDispatcher = null;
|
|
25
|
+
this._positionMs = 0;
|
|
26
|
+
this._stopped = false;
|
|
27
|
+
|
|
28
|
+
const onError = err => this.emit('error', err);
|
|
29
|
+
streamConnection.on('error', onError);
|
|
30
|
+
voiceConnection.on('error', onError);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
_getPositionMs() {
|
|
34
|
+
if (!this.videoDispatcher) return this._positionMs;
|
|
35
|
+
return Math.max(0, this.videoDispatcher.totalStreamTime - this.videoDispatcher.pausedTime);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_play(seek = 0) {
|
|
39
|
+
const { url, fps, height, width, bitrate, audioBitrate } = this.options;
|
|
40
|
+
const streamHeight = height ?? 720;
|
|
41
|
+
this.streamConnection.videoAttributes = {
|
|
42
|
+
width: width ?? Math.round((streamHeight * 16) / 9),
|
|
43
|
+
height: streamHeight,
|
|
44
|
+
fps: fps ?? 30,
|
|
45
|
+
};
|
|
46
|
+
const videoOptions = {
|
|
47
|
+
fps,
|
|
48
|
+
bitrate,
|
|
49
|
+
seek,
|
|
50
|
+
presetH26x: this.options.preset || 'ultrafast',
|
|
51
|
+
outputFFmpegArgs: [
|
|
52
|
+
'-pix_fmt',
|
|
53
|
+
'yuv420p',
|
|
54
|
+
'-g',
|
|
55
|
+
String(fps),
|
|
56
|
+
'-keyint_min',
|
|
57
|
+
String(fps),
|
|
58
|
+
'-sc_threshold',
|
|
59
|
+
'0',
|
|
60
|
+
'-force_key_frames',
|
|
61
|
+
'expr:gte(t,n_forced*1)',
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
if (height) {
|
|
66
|
+
videoOptions.outputFFmpegArgs.unshift('-vf', `scale=-2:${height}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!url.startsWith('http')) {
|
|
70
|
+
videoOptions.inputFFmpegArgs = ['-re'];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.videoDispatcher = this.streamConnection.playVideo(url, videoOptions);
|
|
74
|
+
|
|
75
|
+
if (this.options.audio !== false) {
|
|
76
|
+
const audioOptions = {
|
|
77
|
+
type: 'unknown',
|
|
78
|
+
seek,
|
|
79
|
+
bitrate: audioBitrate ?? 128,
|
|
80
|
+
};
|
|
81
|
+
if (!url.startsWith('http')) {
|
|
82
|
+
audioOptions.inputFFmpegArgs = ['-re'];
|
|
83
|
+
}
|
|
84
|
+
this.voiceAudioDispatcher = this.voiceConnection.playAudio(url, audioOptions);
|
|
85
|
+
this.audioDispatcher = this.streamConnection.playAudio(url, audioOptions);
|
|
86
|
+
this.audioDispatcher.setSyncVideoDispatcher(this.videoDispatcher);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this._stopped = false;
|
|
90
|
+
this.videoDispatcher.once('finish', () => {
|
|
91
|
+
this._positionMs = 0;
|
|
92
|
+
this.emit('finish');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return this.videoDispatcher;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pauses video and audio playback.
|
|
100
|
+
*/
|
|
101
|
+
pause() {
|
|
102
|
+
this.videoDispatcher?.pause();
|
|
103
|
+
this.audioDispatcher?.pause(true);
|
|
104
|
+
this.voiceAudioDispatcher?.pause(true);
|
|
105
|
+
this.streamConnection.sendScreenshareState(true);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resumes video and audio playback.
|
|
110
|
+
*/
|
|
111
|
+
resume() {
|
|
112
|
+
this.streamConnection.sendScreenshareState(false);
|
|
113
|
+
this.videoDispatcher?.resume();
|
|
114
|
+
this.audioDispatcher?.resume();
|
|
115
|
+
this.voiceAudioDispatcher?.resume();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stops playback while keeping voice/stream connections.
|
|
120
|
+
*/
|
|
121
|
+
stop() {
|
|
122
|
+
this._positionMs = this._getPositionMs();
|
|
123
|
+
this._stopped = true;
|
|
124
|
+
this.videoDispatcher?.destroy();
|
|
125
|
+
this.audioDispatcher?.destroy();
|
|
126
|
+
this.voiceAudioDispatcher?.destroy();
|
|
127
|
+
this.videoDispatcher = null;
|
|
128
|
+
this.audioDispatcher = null;
|
|
129
|
+
this.voiceAudioDispatcher = null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resumes playback from the last position.
|
|
134
|
+
* @returns {import('./dispatcher/VideoDispatcher')}
|
|
135
|
+
*/
|
|
136
|
+
replay() {
|
|
137
|
+
const seekSec = (this._stopped ? this._positionMs : this._getPositionMs()) / 1000;
|
|
138
|
+
this.stop();
|
|
139
|
+
return this._play(seekSec);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Stops playback and disconnects from voice and stream.
|
|
144
|
+
*/
|
|
145
|
+
disconnect() {
|
|
146
|
+
this.stop();
|
|
147
|
+
this.streamConnection.disconnect();
|
|
148
|
+
this.voiceConnection.disconnect();
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = StreamSession;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @typedef {Object} StartStreamOptions
|
|
156
|
+
* @property {import('../../util/Snowflake')} guildId Guild id
|
|
157
|
+
* @property {import('../../util/Snowflake')} channelId Voice channel id
|
|
158
|
+
* @property {string} url Video URL or file path
|
|
159
|
+
* @property {number} [fps=30] Video framerate
|
|
160
|
+
* @property {number} [height] Output height (width auto-scaled)
|
|
161
|
+
* @property {number} [width] Video width sent to Discord (default: 16:9 from height)
|
|
162
|
+
* @property {number} [bitrate=2000] Video bitrate in kbps
|
|
163
|
+
* @property {number} [audioBitrate=128] Audio bitrate in kbps
|
|
164
|
+
* @property {'H264' | 'VP8'} [videoCodec='H264'] Video codec
|
|
165
|
+
* @property {string} [preset='ultrafast'] x264 preset
|
|
166
|
+
* @property {boolean} [audio=true] Whether to play audio
|
|
167
|
+
* @property {boolean} [video=false] Enable webcam (false = screenshare only)
|
|
168
|
+
*/
|