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 +8 -1
- package/src/client/Client.js +18 -24
- package/src/client/voice/ClientVoiceManager.js +95 -84
- package/src/client/voice/StreamSession.js +25 -0
- package/src/client/voice/VoiceConnection.js +1 -0
- package/src/client/voice/WebRtcStreamSession.js +191 -0
- package/src/client/voice/networking/VoiceWebSocket.js +11 -55
- package/src/errors/Messages.js +4 -2
- package/typings/index.d.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "djs-selfbot-v13",
|
|
3
|
-
"version": "3.7.
|
|
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
|
},
|
package/src/client/Client.js
CHANGED
|
@@ -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
|
|
924
|
+
fps,
|
|
924
925
|
height,
|
|
925
926
|
width,
|
|
926
|
-
bitrate =
|
|
927
|
+
bitrate = 5000,
|
|
928
|
+
bitrateMax,
|
|
927
929
|
audioBitrate,
|
|
928
|
-
videoCodec = 'H264',
|
|
929
930
|
preset,
|
|
931
|
+
tune,
|
|
930
932
|
audio,
|
|
931
|
-
|
|
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
|
-
|
|
938
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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.
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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:
|
|
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
|
}
|
package/src/errors/Messages.js
CHANGED
|
@@ -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:
|
|
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:
|
|
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.',
|
package/typings/index.d.ts
CHANGED
|
@@ -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 {
|