djs-selfbot-v13 3.7.35 → 3.7.37
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/LICENSE +4 -7
- package/README.md +0 -5
- package/package.json +3 -3
- package/src/client/Client.js +11 -1
- package/src/client/voice/ClientVoiceManager.js +74 -0
- package/src/client/voice/StreamSession.js +23 -4
- package/src/client/voice/WebRtcStreamSession.js +197 -156
- package/src/structures/BaseGuildVoiceChannel.js +25 -0
- package/src/structures/VoiceChannel.js +7 -0
- package/typings/index.d.ts +12 -1
package/LICENSE
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
GNU GENERAL PUBLIC LICENSE
|
|
2
2
|
Version 3, 29 June 2007
|
|
3
3
|
|
|
4
|
-
Copyright (C)
|
|
4
|
+
Copyright (C) 2025 002-sans aiko-chan-ai and discordjs
|
|
5
5
|
Everyone is permitted to copy and distribute verbatim copies
|
|
6
6
|
of this license document, but changing it is not allowed.
|
|
7
7
|
|
|
@@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least
|
|
|
632
632
|
the "copyright" line and a pointer to where the full notice is found.
|
|
633
633
|
|
|
634
634
|
<one line to give the program's name and a brief idea of what it does.>
|
|
635
|
-
Copyright (C)
|
|
635
|
+
Copyright (C) 2025 002-sans aiko-chan-ai and discordjs
|
|
636
636
|
|
|
637
637
|
This program is free software: you can redistribute it and/or modify
|
|
638
638
|
it under the terms of the GNU General Public License as published by
|
|
@@ -647,12 +647,9 @@ the "copyright" line and a pointer to where the full notice is found.
|
|
|
647
647
|
You should have received a copy of the GNU General Public License
|
|
648
648
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
649
649
|
|
|
650
|
-
|
|
650
|
+
notice like this when it starts in an interactive mode:
|
|
651
651
|
|
|
652
|
-
|
|
653
|
-
notice like this when it starts in an interactive mode:
|
|
654
|
-
|
|
655
|
-
<program> Copyright (C) <year> <name of author>
|
|
652
|
+
djs-selfbot-v13 Copyright (C) 2025 002-sans aiko-chan-ai and discordjs
|
|
656
653
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
|
657
654
|
This is free software, and you are welcome to redistribute it
|
|
658
655
|
under certain conditions; type `show c' for details.
|
package/README.md
CHANGED
|
@@ -108,11 +108,6 @@ Github Discussion: [Here](https://github.com/002-sans/djs-selfbot-v13/discussion
|
|
|
108
108
|
## Credits
|
|
109
109
|
- [Discord.js](https://github.com/discordjs/discord.js)
|
|
110
110
|
|
|
111
|
-
## <strong>Other project(s)
|
|
112
|
-
|
|
113
|
-
- 📘 [***aiko-chan-ai/DiscordBotClient***](https://github.com/aiko-chan-ai/DiscordBotClient) <br/>
|
|
114
|
-
A patched version of discord, with bot login support
|
|
115
|
-
|
|
116
111
|
## Star History
|
|
117
112
|
|
|
118
113
|
[](https://star-history.com/#002-sans/djs-selfbot-v13&Date)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "djs-selfbot-v13",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.37",
|
|
4
4
|
"description": "An unofficial discord.js fork for creating selfbots",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"types": "./typings/index.d.ts",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
"lint:typings:fix": "tslint typings/index.d.ts --fix",
|
|
16
16
|
"format": "prettier --write src/**/*.js typings/**/*.ts",
|
|
17
17
|
"lint:all": "npm run lint && npm run lint:typings",
|
|
18
|
-
"docs": "
|
|
19
|
-
"docs:test": "
|
|
18
|
+
"docs": "node scripts/generate-docs.js --source src --custom docs/index.yml --output docs/main.json",
|
|
19
|
+
"docs:test": "node scripts/generate-docs.js --source src --custom docs/index.yml",
|
|
20
20
|
"build": "npm run lint:fix && npm run lint:typings:fix && npm run format && npm run docs"
|
|
21
21
|
},
|
|
22
22
|
"files": [
|
package/src/client/Client.js
CHANGED
|
@@ -932,6 +932,11 @@ class Client extends BaseClient {
|
|
|
932
932
|
audio,
|
|
933
933
|
livestream = false,
|
|
934
934
|
downloadHttp = true,
|
|
935
|
+
encoder,
|
|
936
|
+
hardwareAcceleratedDecoding,
|
|
937
|
+
nvencPreset,
|
|
938
|
+
preEncode,
|
|
939
|
+
goLive,
|
|
935
940
|
} = options;
|
|
936
941
|
|
|
937
942
|
if (!url) throw new Error('STREAM_URL_REQUIRED');
|
|
@@ -939,7 +944,7 @@ class Client extends BaseClient {
|
|
|
939
944
|
|
|
940
945
|
let playUrl = url;
|
|
941
946
|
if (typeof url === 'string' && url.startsWith('http') && downloadHttp) {
|
|
942
|
-
playUrl = await
|
|
947
|
+
playUrl = await StreamSession.resolveUrl(url);
|
|
943
948
|
}
|
|
944
949
|
|
|
945
950
|
const session = new WebRtcStreamSession(this, {
|
|
@@ -956,6 +961,11 @@ class Client extends BaseClient {
|
|
|
956
961
|
tune,
|
|
957
962
|
audio,
|
|
958
963
|
livestream,
|
|
964
|
+
encoder,
|
|
965
|
+
hardwareAcceleratedDecoding,
|
|
966
|
+
nvencPreset,
|
|
967
|
+
preEncode,
|
|
968
|
+
goLive,
|
|
959
969
|
});
|
|
960
970
|
|
|
961
971
|
await session.start();
|
|
@@ -164,6 +164,80 @@ class ClientVoiceManager {
|
|
|
164
164
|
});
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
+
/**
|
|
168
|
+
* Force quitter vocal + stream (session stale / erreur 4006).
|
|
169
|
+
* @param {import('../../structures/VoiceChannel') | import('../../structures/StageChannel')} channel
|
|
170
|
+
* @returns {Promise<void>}
|
|
171
|
+
*/
|
|
172
|
+
resetVoiceSession(channel) {
|
|
173
|
+
return new Promise(resolve => {
|
|
174
|
+
if (this.connection) {
|
|
175
|
+
if (this.connection.streamConnection) {
|
|
176
|
+
this.connection.streamConnection.disconnect();
|
|
177
|
+
this.connection.streamConnection = null;
|
|
178
|
+
}
|
|
179
|
+
this.connection.disconnect();
|
|
180
|
+
this.connection = null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const guild = channel.guild;
|
|
184
|
+
if (!guild) {
|
|
185
|
+
setTimeout(resolve, 1000).unref();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const userId = this.client.user?.id;
|
|
190
|
+
const voiceState = guild.voiceStates.cache.get(userId);
|
|
191
|
+
const channelId = voiceState?.channelId ?? channel.id;
|
|
192
|
+
const streamKey = `guild:${guild.id}:${channelId}:${userId}`;
|
|
193
|
+
|
|
194
|
+
this.client.ws.broadcast({
|
|
195
|
+
op: Opcodes.STREAM_DELETE,
|
|
196
|
+
d: { stream_key: streamKey },
|
|
197
|
+
});
|
|
198
|
+
this.client.ws.broadcast({
|
|
199
|
+
op: Opcodes.VOICE_STATE_UPDATE,
|
|
200
|
+
d: {
|
|
201
|
+
guild_id: guild.id,
|
|
202
|
+
channel_id: null,
|
|
203
|
+
self_mute: false,
|
|
204
|
+
self_deaf: false,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this.client.emit('debug', '[VOICE] resetVoiceSession: vocal et stream réinitialisés');
|
|
209
|
+
setTimeout(resolve, 2500).unref();
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @param {import('../../structures/VoiceChannel') | import('../../structures/StageChannel')} channel
|
|
215
|
+
* @param {object} [config]
|
|
216
|
+
* @param {number} [maxAttempts=3]
|
|
217
|
+
* @returns {Promise<VoiceConnection>}
|
|
218
|
+
*/
|
|
219
|
+
async joinChannelWithRetry(channel, config = {}, maxAttempts = 3) {
|
|
220
|
+
let lastError;
|
|
221
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
222
|
+
try {
|
|
223
|
+
if (attempt > 1) {
|
|
224
|
+
console.log(`[stream] reconnexion vocale (${attempt}/${maxAttempts})…`);
|
|
225
|
+
await this.resetVoiceSession(channel);
|
|
226
|
+
}
|
|
227
|
+
return await this.joinChannel(channel, config);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
lastError = err;
|
|
230
|
+
const expired =
|
|
231
|
+
err?.code === 'VOICE_SESSION_EXPIRED' ||
|
|
232
|
+
String(err?.message ?? '').includes('4006') ||
|
|
233
|
+
String(err?.message ?? '').includes('VOICE_SESSION_EXPIRED');
|
|
234
|
+
if (!expired || attempt === maxAttempts) throw err;
|
|
235
|
+
await this.resetVoiceSession(channel);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
throw lastError;
|
|
239
|
+
}
|
|
240
|
+
|
|
167
241
|
/**
|
|
168
242
|
* Sets up a request to join a voice channel.
|
|
169
243
|
* @param {VoiceChannel | StageChannel | DMChannel | GroupDMChannel | Snowflake} channel The voice channel to join
|
|
@@ -4,6 +4,7 @@ const fs = require('node:fs');
|
|
|
4
4
|
const os = require('node:os');
|
|
5
5
|
const path = require('node:path');
|
|
6
6
|
const { EventEmitter } = require('events');
|
|
7
|
+
const StageChannel = require('../../structures/StageChannel');
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Active screenshare session returned by {@link Client#startStream}.
|
|
@@ -53,16 +54,27 @@ class StreamSession extends EventEmitter {
|
|
|
53
54
|
return Math.max(0, this.videoDispatcher.totalStreamTime - this.videoDispatcher.pausedTime);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Starts playback on the stream connection.
|
|
59
|
+
*/
|
|
60
|
+
start() {
|
|
61
|
+
if (this.client.user?.voice?.channel instanceof StageChannel) {
|
|
62
|
+
void this.client.user.voice.setSuppressed(false);
|
|
63
|
+
}
|
|
64
|
+
return this._play();
|
|
65
|
+
}
|
|
66
|
+
|
|
56
67
|
_play(seek = 0) {
|
|
57
68
|
const { url, fps, height, width, bitrate, audioBitrate } = this.options;
|
|
69
|
+
const streamFps = fps ?? 30;
|
|
58
70
|
const streamHeight = height ?? 720;
|
|
59
71
|
this.streamConnection.videoAttributes = {
|
|
60
72
|
width: width ?? Math.round((streamHeight * 16) / 9),
|
|
61
73
|
height: streamHeight,
|
|
62
|
-
fps:
|
|
74
|
+
fps: streamFps,
|
|
63
75
|
};
|
|
64
76
|
const videoOptions = {
|
|
65
|
-
fps,
|
|
77
|
+
fps: streamFps,
|
|
66
78
|
bitrate,
|
|
67
79
|
seek,
|
|
68
80
|
presetH26x: this.options.preset || 'ultrafast',
|
|
@@ -70,9 +82,9 @@ class StreamSession extends EventEmitter {
|
|
|
70
82
|
'-pix_fmt',
|
|
71
83
|
'yuv420p',
|
|
72
84
|
'-g',
|
|
73
|
-
String(
|
|
85
|
+
String(streamFps),
|
|
74
86
|
'-keyint_min',
|
|
75
|
-
String(
|
|
87
|
+
String(streamFps),
|
|
76
88
|
'-sc_threshold',
|
|
77
89
|
'0',
|
|
78
90
|
'-force_key_frames',
|
|
@@ -90,6 +102,8 @@ class StreamSession extends EventEmitter {
|
|
|
90
102
|
videoOptions.inputFFmpegArgs = ['-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1'];
|
|
91
103
|
}
|
|
92
104
|
|
|
105
|
+
console.log(`[stream] lecture ${width ?? '?'}x${streamHeight} @ ${streamFps}fps (UDP)`);
|
|
106
|
+
|
|
93
107
|
this.videoDispatcher = this.streamConnection.playVideo(url, videoOptions);
|
|
94
108
|
|
|
95
109
|
if (this.options.audio !== false) {
|
|
@@ -190,4 +204,9 @@ module.exports = StreamSession;
|
|
|
190
204
|
* @property {boolean} [downloadHttp=true] Download HTTP URLs locally before playback
|
|
191
205
|
* @property {number} [bitrateMax] Max video bitrate in kbps
|
|
192
206
|
* @property {boolean} [livestream=false] Enable readrateInitialBurst (live sources)
|
|
207
|
+
* @property {'auto' | 'amf' | 'nvenc' | 'qsv' | 'software'} [encoder='auto'] Video encoder
|
|
208
|
+
* @property {boolean} [hardwareAcceleratedDecoding=true] Use GPU decoding (-hwaccel auto)
|
|
209
|
+
* @property {string} [nvencPreset='p1'] NVENC preset (p1 = lowest latency)
|
|
210
|
+
* @property {boolean} [preEncode=true] Pré-encode en local avant lecture (fluidité)
|
|
211
|
+
* @property {boolean} [goLive=false] WebRTC Go Live (sinon UDP classique, plus stable)
|
|
193
212
|
*/
|
|
@@ -1,9 +1,17 @@
|
|
|
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
|
-
const {
|
|
7
|
+
const { spawn } = require('child_process');
|
|
5
8
|
const ffmpeg = require('fluent-ffmpeg');
|
|
6
|
-
const {
|
|
9
|
+
const {
|
|
10
|
+
Streamer,
|
|
11
|
+
prepareStream,
|
|
12
|
+
playStream,
|
|
13
|
+
Encoders,
|
|
14
|
+
} = require('@dank074/discord-video-stream');
|
|
7
15
|
const StreamSession = require('./StreamSession');
|
|
8
16
|
const StageChannel = require('../../structures/StageChannel');
|
|
9
17
|
|
|
@@ -11,31 +19,55 @@ if (typeof globalThis.WebSocket === 'undefined') {
|
|
|
11
19
|
globalThis.WebSocket = require('ws');
|
|
12
20
|
}
|
|
13
21
|
|
|
14
|
-
|
|
22
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
15
23
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
+
/** @type {'amf' | 'nvenc' | 'software' | null} */
|
|
25
|
+
let _cachedEncoder = null;
|
|
26
|
+
|
|
27
|
+
function detectHwEncoder() {
|
|
28
|
+
if (_cachedEncoder) return Promise.resolve(_cachedEncoder);
|
|
29
|
+
return new Promise(resolve => {
|
|
30
|
+
ffmpeg.getAvailableEncoders((err, encoders) => {
|
|
31
|
+
if (err || !encoders) {
|
|
32
|
+
_cachedEncoder = 'software';
|
|
33
|
+
resolve('software');
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (encoders.h264_amf) {
|
|
37
|
+
_cachedEncoder = 'amf';
|
|
38
|
+
resolve('amf');
|
|
39
|
+
return;
|
|
24
40
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
41
|
+
if (encoders.h264_nvenc) {
|
|
42
|
+
const proc = spawn('ffmpeg', [
|
|
43
|
+
'-hide_banner', '-loglevel', 'error',
|
|
44
|
+
'-f', 'lavfi', '-i', 'color=c=black:s=64x64:d=0.04',
|
|
45
|
+
'-c:v', 'h264_nvenc', '-f', 'null', '-',
|
|
46
|
+
]);
|
|
47
|
+
proc.on('close', code => {
|
|
48
|
+
if (code === 0) {
|
|
49
|
+
_cachedEncoder = 'nvenc';
|
|
50
|
+
resolve('nvenc');
|
|
51
|
+
} else {
|
|
52
|
+
_cachedEncoder = 'software';
|
|
53
|
+
resolve('software');
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
proc.on('error', () => {
|
|
57
|
+
_cachedEncoder = 'software';
|
|
58
|
+
resolve('software');
|
|
59
|
+
});
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
_cachedEncoder = 'software';
|
|
63
|
+
resolve('software');
|
|
33
64
|
});
|
|
34
|
-
|
|
65
|
+
});
|
|
35
66
|
}
|
|
36
67
|
|
|
37
68
|
/**
|
|
38
|
-
* Go Live
|
|
69
|
+
* Go Live WebRTC — même approche que discord-livestream-selfbot :
|
|
70
|
+
* Streamer.joinVoice + prepareStream + playStream
|
|
39
71
|
* @extends {EventEmitter}
|
|
40
72
|
*/
|
|
41
73
|
class WebRtcStreamSession extends EventEmitter {
|
|
@@ -51,152 +83,165 @@ class WebRtcStreamSession extends EventEmitter {
|
|
|
51
83
|
this.options = options;
|
|
52
84
|
this.streamer = new Streamer(client);
|
|
53
85
|
this._abort = new AbortController();
|
|
86
|
+
this._ffmpegCommand = null;
|
|
54
87
|
this._playPromise = null;
|
|
55
|
-
this._command = null;
|
|
56
|
-
this._ffmpegProc = null;
|
|
57
88
|
this._started = false;
|
|
58
89
|
this._running = false;
|
|
59
90
|
this._paused = false;
|
|
60
91
|
this._stopped = false;
|
|
61
92
|
this._positionMs = 0;
|
|
62
93
|
this._runStartWall = 0;
|
|
63
|
-
this._pausedAt = 0;
|
|
64
|
-
this._pausedTotalMs = 0;
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
_getPositionMs() {
|
|
68
97
|
if (!this._running) return this._positionMs;
|
|
69
|
-
|
|
70
|
-
return this._positionMs + (Date.now() - this._runStartWall - this._pausedTotalMs);
|
|
98
|
+
return this._positionMs + (Date.now() - this._runStartWall);
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
101
|
+
async _pickEncoder() {
|
|
102
|
+
const { preset = 'ultrafast', encoder = 'auto' } = this.options;
|
|
103
|
+
let mode = encoder;
|
|
104
|
+
if (encoder === 'auto') mode = await detectHwEncoder();
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
if (mode === 'nvenc') {
|
|
107
|
+
console.log('[stream] encodeur: h264_nvenc');
|
|
108
|
+
return Encoders.nvenc({ preset: 'p1' });
|
|
109
|
+
}
|
|
110
|
+
if (mode === 'amf') {
|
|
111
|
+
console.log('[stream] encodeur: h264_amf');
|
|
112
|
+
return () => ({
|
|
113
|
+
H264: {
|
|
114
|
+
name: 'h264_amf',
|
|
115
|
+
options: ['-usage', 'transcoding', '-quality', 'speed'],
|
|
116
|
+
},
|
|
117
|
+
H265: {
|
|
118
|
+
name: 'hevc_amf',
|
|
119
|
+
options: ['-usage', 'transcoding', '-quality', 'speed'],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
86
122
|
}
|
|
123
|
+
console.log('[stream] encodeur: libx264');
|
|
124
|
+
return Encoders.software({
|
|
125
|
+
x264: { preset, tune: 'zerolatency' },
|
|
126
|
+
});
|
|
87
127
|
}
|
|
88
128
|
|
|
89
|
-
|
|
90
|
-
* Construit la commande ffmpeg : transcode en H264 + Opus, sortie nut.
|
|
91
|
-
* Pas de filtre azmq (compatibilité ffmpeg sans libzmq).
|
|
92
|
-
*/
|
|
93
|
-
_buildFfmpeg(url, seekSec = 0) {
|
|
129
|
+
async _buildPrepareOptions(seekSec = 0) {
|
|
94
130
|
const {
|
|
95
|
-
fps,
|
|
96
|
-
height
|
|
97
|
-
width
|
|
98
|
-
bitrate =
|
|
131
|
+
fps = 60,
|
|
132
|
+
height,
|
|
133
|
+
width,
|
|
134
|
+
bitrate = 4500,
|
|
99
135
|
bitrateMax,
|
|
100
136
|
audioBitrate = 128,
|
|
101
|
-
|
|
102
|
-
tune = 'film',
|
|
137
|
+
hardwareAcceleratedDecoding = true,
|
|
103
138
|
audio,
|
|
139
|
+
noTranscoding = false,
|
|
104
140
|
} = this.options;
|
|
105
141
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
'-reconnect_at_eof 1',
|
|
118
|
-
'-reconnect_streamed 1',
|
|
119
|
-
'-reconnect_delay_max 4294',
|
|
120
|
-
]);
|
|
142
|
+
if (noTranscoding) {
|
|
143
|
+
console.log('[stream] mode copie vidéo (pas de ré-encodage)');
|
|
144
|
+
return {
|
|
145
|
+
noTranscoding: true,
|
|
146
|
+
bitrateAudio: audioBitrate,
|
|
147
|
+
includeAudio: audio !== false,
|
|
148
|
+
hardwareAcceleratedDecoding: false,
|
|
149
|
+
minimizeLatency: true,
|
|
150
|
+
customInputOptions: seekSec > 0 ? [`-ss ${String(seekSec)}`] : [],
|
|
151
|
+
customFfmpegFlags: [],
|
|
152
|
+
};
|
|
121
153
|
}
|
|
122
154
|
|
|
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
|
-
command.audioFilters('volume=1');
|
|
155
|
+
const gop = String(fps);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
encoder: await this._pickEncoder(),
|
|
159
|
+
height,
|
|
160
|
+
width,
|
|
161
|
+
frameRate: fps,
|
|
162
|
+
bitrateVideo: bitrate,
|
|
163
|
+
bitrateVideoMax: bitrateMax ?? Math.round(bitrate * 1.5),
|
|
164
|
+
bitrateAudio: audioBitrate,
|
|
165
|
+
includeAudio: audio !== false,
|
|
166
|
+
hardwareAcceleratedDecoding,
|
|
167
|
+
minimizeLatency: true,
|
|
168
|
+
customInputOptions: seekSec > 0 ? [`-ss ${String(seekSec)}`] : [],
|
|
169
|
+
customFfmpegFlags: [
|
|
170
|
+
'-g', gop,
|
|
171
|
+
'-keyint_min', gop,
|
|
172
|
+
'-sc_threshold', '0',
|
|
173
|
+
'-threads', '0',
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async _disconnectLegacyVoice() {
|
|
179
|
+
const conn = this.client.voice?.connection;
|
|
180
|
+
if (conn) {
|
|
181
|
+
conn.disconnect();
|
|
182
|
+
this.client.voice.connection = null;
|
|
183
|
+
await sleep(800);
|
|
153
184
|
}
|
|
185
|
+
}
|
|
154
186
|
|
|
155
|
-
|
|
187
|
+
async _joinVoice() {
|
|
188
|
+
const { guildId, channelId } = this.options;
|
|
189
|
+
|
|
190
|
+
if (this.streamer.voiceConnection) {
|
|
191
|
+
this.streamer.leaveVoice();
|
|
192
|
+
await sleep(1000);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await this._disconnectLegacyVoice();
|
|
196
|
+
|
|
197
|
+
const vc = this.streamer.voiceConnection;
|
|
198
|
+
if (
|
|
199
|
+
!vc ||
|
|
200
|
+
vc.guildId !== guildId ||
|
|
201
|
+
vc.channelId !== channelId
|
|
202
|
+
) {
|
|
203
|
+
await this.streamer.joinVoice(guildId, channelId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.client.user?.voice?.channel instanceof StageChannel) {
|
|
207
|
+
await this.client.user.voice.setSuppressed(false);
|
|
208
|
+
}
|
|
156
209
|
}
|
|
157
210
|
|
|
158
211
|
async _run(url, signal, seekSec = 0) {
|
|
159
|
-
await
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this._command = command;
|
|
163
|
-
this._ffmpegProc = null;
|
|
164
|
-
this._running = true;
|
|
165
|
-
this._paused = false;
|
|
166
|
-
this._stopped = false;
|
|
167
|
-
this._resetRunClock();
|
|
212
|
+
const prepareOpts = await this._buildPrepareOptions(seekSec);
|
|
213
|
+
const { command, output } = prepareStream(url, prepareOpts, signal);
|
|
214
|
+
this._ffmpegCommand = command;
|
|
168
215
|
|
|
169
|
-
command.on('start', cmd => {
|
|
170
|
-
this._ffmpegProc = command.ffmpegProc ?? null;
|
|
171
|
-
this.emit('debug', `[cmd] ${cmd}`);
|
|
172
|
-
});
|
|
216
|
+
command.on('start', cmd => this.emit('debug', `[ffmpeg] ${cmd}`));
|
|
173
217
|
command.on('stderr', line => this.emit('debug', String(line)));
|
|
174
|
-
|
|
175
218
|
command.on('error', (err, _stdout, stderr) => {
|
|
176
|
-
if (stderr) this.emit('debug', String(stderr));
|
|
177
219
|
if (signal.aborted) return;
|
|
178
|
-
|
|
179
|
-
this.emit('error', new Error(stderr ? `${msg}\n${stderr}` : msg));
|
|
220
|
+
this.emit('error', new Error(stderr ? `${err.message}\n${stderr}` : err.message));
|
|
180
221
|
});
|
|
181
222
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
};
|
|
187
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
188
|
-
command.run();
|
|
223
|
+
this._running = true;
|
|
224
|
+
this._paused = false;
|
|
225
|
+
this._stopped = false;
|
|
226
|
+
this._runStartWall = Date.now();
|
|
189
227
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
228
|
+
let playOptions;
|
|
229
|
+
if (this.options.noTranscoding) {
|
|
230
|
+
console.log('[stream] envoi à la résolution/fps natifs de la source (WebRTC)');
|
|
231
|
+
playOptions = {};
|
|
232
|
+
} else {
|
|
233
|
+
const { height = 720, width = 1280, fps = 60 } = this.options;
|
|
234
|
+
console.log(`[stream] envoi ${width}x${height} @ ${fps}fps (WebRTC)`);
|
|
235
|
+
playOptions = { width, height, frameRate: fps };
|
|
236
|
+
}
|
|
237
|
+
this.emit('playing');
|
|
193
238
|
|
|
194
239
|
this._playPromise = playStream(output, this.streamer, playOptions, signal);
|
|
195
240
|
|
|
196
241
|
this._playPromise
|
|
197
242
|
.then(() => {
|
|
198
243
|
this._running = false;
|
|
199
|
-
this.
|
|
244
|
+
this._ffmpegCommand = null;
|
|
200
245
|
if (!signal.aborted) {
|
|
201
246
|
this._positionMs = 0;
|
|
202
247
|
this.emit('finish');
|
|
@@ -204,7 +249,7 @@ class WebRtcStreamSession extends EventEmitter {
|
|
|
204
249
|
})
|
|
205
250
|
.catch(err => {
|
|
206
251
|
this._running = false;
|
|
207
|
-
this.
|
|
252
|
+
this._ffmpegCommand = null;
|
|
208
253
|
if (signal.aborted || err?.name === 'AbortError') return;
|
|
209
254
|
this.emit('error', err);
|
|
210
255
|
});
|
|
@@ -213,66 +258,62 @@ class WebRtcStreamSession extends EventEmitter {
|
|
|
213
258
|
}
|
|
214
259
|
|
|
215
260
|
async start() {
|
|
216
|
-
const {
|
|
217
|
-
await
|
|
218
|
-
|
|
261
|
+
const { url } = this.options;
|
|
262
|
+
await this._joinVoice();
|
|
263
|
+
this._started = true;
|
|
219
264
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
265
|
+
const playing = new Promise((resolve, reject) => {
|
|
266
|
+
this.once('playing', resolve);
|
|
267
|
+
this.once('error', reject);
|
|
268
|
+
});
|
|
223
269
|
|
|
224
|
-
this.
|
|
225
|
-
|
|
270
|
+
void this._run(url, this._abort.signal).catch(err => this.emit('error', err));
|
|
271
|
+
await playing;
|
|
226
272
|
}
|
|
227
273
|
|
|
228
|
-
/**
|
|
229
|
-
* Met en pause la lecture (ffmpeg suspendu, connexion vocale conservée).
|
|
230
|
-
*/
|
|
231
274
|
pause() {
|
|
232
275
|
if (!this._running || this._paused || this._stopped) return;
|
|
233
276
|
this._positionMs = this._getPositionMs();
|
|
234
|
-
this._pausedAt = Date.now();
|
|
235
|
-
this._signalFfmpeg('SIGSTOP');
|
|
236
277
|
this._paused = true;
|
|
278
|
+
this._running = false;
|
|
279
|
+
this._ffmpegCommand?.kill?.('SIGTERM');
|
|
280
|
+
this._ffmpegCommand = null;
|
|
281
|
+
this.streamer.stopStream();
|
|
282
|
+
this._abort.abort();
|
|
283
|
+
this._abort = new AbortController();
|
|
284
|
+
console.log(`[stream] pause à ${(this._positionMs / 1000).toFixed(1)}s`);
|
|
237
285
|
}
|
|
238
286
|
|
|
239
|
-
/**
|
|
240
|
-
* Reprend la lecture (après pause ou stop).
|
|
241
|
-
*/
|
|
242
287
|
async resume() {
|
|
243
288
|
if (this._stopped) {
|
|
244
|
-
const seekSec = this._positionMs / 1000;
|
|
245
289
|
this._stopped = false;
|
|
246
|
-
await this._run(this.options.url, this._abort.signal,
|
|
290
|
+
await this._run(this.options.url, this._abort.signal, this._positionMs / 1000);
|
|
247
291
|
return;
|
|
248
292
|
}
|
|
249
|
-
if (!this.
|
|
250
|
-
this._pausedTotalMs += Date.now() - this._pausedAt;
|
|
251
|
-
this._pausedAt = 0;
|
|
252
|
-
this._signalFfmpeg('SIGCONT');
|
|
293
|
+
if (!this._paused) return;
|
|
253
294
|
this._paused = false;
|
|
295
|
+
const seekSec = this._positionMs / 1000;
|
|
296
|
+
console.log(`[stream] reprise à ${seekSec.toFixed(1)}s`);
|
|
297
|
+
await this._run(this.options.url, this._abort.signal, seekSec);
|
|
254
298
|
}
|
|
255
299
|
|
|
256
|
-
/**
|
|
257
|
-
* Arrête la lecture en conservant la position et la connexion vocale.
|
|
258
|
-
*/
|
|
259
300
|
stop() {
|
|
260
|
-
if (!this._running) return;
|
|
301
|
+
if (!this._running && !this._paused) return;
|
|
261
302
|
this._positionMs = this._getPositionMs();
|
|
262
303
|
this._stopped = true;
|
|
263
304
|
this._paused = false;
|
|
264
305
|
this._running = false;
|
|
265
|
-
this.
|
|
306
|
+
this._ffmpegCommand?.kill?.('SIGTERM');
|
|
307
|
+
this._ffmpegCommand = null;
|
|
308
|
+
this.streamer.stopStream();
|
|
266
309
|
this._abort.abort();
|
|
267
310
|
this._abort = new AbortController();
|
|
311
|
+
console.log(`[stream] stop à ${(this._positionMs / 1000).toFixed(1)}s`);
|
|
268
312
|
}
|
|
269
313
|
|
|
270
|
-
/**
|
|
271
|
-
* Relance depuis la dernière position enregistrée.
|
|
272
|
-
*/
|
|
273
314
|
async replay() {
|
|
274
315
|
const seekSec = (this._running ? this._getPositionMs() : this._positionMs) / 1000;
|
|
275
|
-
if (this._running) this.stop();
|
|
316
|
+
if (this._running || this._paused) this.stop();
|
|
276
317
|
this._stopped = false;
|
|
277
318
|
await this._run(this.options.url, this._abort.signal, seekSec);
|
|
278
319
|
}
|
|
@@ -198,6 +198,7 @@ class BaseGuildVoiceChannel extends GuildChannel {
|
|
|
198
198
|
return this.edit({ rtcRegion }, reason);
|
|
199
199
|
}
|
|
200
200
|
|
|
201
|
+
|
|
201
202
|
/**
|
|
202
203
|
* Sets the user limit of the channel.
|
|
203
204
|
* @param {number} userLimit The new user limit
|
|
@@ -223,6 +224,30 @@ class BaseGuildVoiceChannel extends GuildChannel {
|
|
|
223
224
|
return this.edit({ videoQualityMode }, reason);
|
|
224
225
|
}
|
|
225
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Sets the status of the voice channel.
|
|
229
|
+
* @param {?string} status The new status (max 500 characters). Set to `null` to remove the status
|
|
230
|
+
* @returns {Promise<BaseGuildVoiceChannel>}
|
|
231
|
+
* @example
|
|
232
|
+
* // Set the status of a voice channel
|
|
233
|
+
* voiceChannel.setStatus('Hello!')
|
|
234
|
+
* .then(channel => console.log(`Set status to ${channel.status} for ${channel.name}`))
|
|
235
|
+
* .catch(console.error);
|
|
236
|
+
* @example
|
|
237
|
+
* // Remove the status of a voice channel
|
|
238
|
+
* voiceChannel.setStatus(null);
|
|
239
|
+
*/
|
|
240
|
+
setStatus(status) {
|
|
241
|
+
return this.client.api.channels(this.id, 'voice-status').put({
|
|
242
|
+
data: {
|
|
243
|
+
status,
|
|
244
|
+
},
|
|
245
|
+
}).then(() => {
|
|
246
|
+
this.status = status;
|
|
247
|
+
return this;
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
226
251
|
}
|
|
227
252
|
|
|
228
253
|
TextBasedChannel.applyToClass(BaseGuildVoiceChannel, true, ['lastPinAt']);
|
|
@@ -105,6 +105,13 @@ class VoiceChannel extends BaseGuildVoiceChannel {
|
|
|
105
105
|
* @param {string} [reason] Reason for changing the camera video quality mode.
|
|
106
106
|
* @returns {Promise<VoiceChannel>}
|
|
107
107
|
*/
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sets the status of the voice channel.
|
|
111
|
+
* @name VoiceChannel#setStatus
|
|
112
|
+
* @param {?string} status The new status (max 500 characters). Set to `null` to remove the status
|
|
113
|
+
* @returns {Promise<VoiceChannel>}
|
|
114
|
+
*/
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
module.exports = VoiceChannel;
|
package/typings/index.d.ts
CHANGED
|
@@ -719,13 +719,14 @@ export class BaseGuildVoiceChannel extends TextBasedChannelMixin(GuildChannel, [
|
|
|
719
719
|
public rateLimitPerUser: number | null;
|
|
720
720
|
public userLimit: number;
|
|
721
721
|
public videoQualityMode: VideoQualityMode | null;
|
|
722
|
-
public status
|
|
722
|
+
public status: string | null;
|
|
723
723
|
public createInvite(options?: CreateInviteOptions): Promise<Invite>;
|
|
724
724
|
public setRTCRegion(rtcRegion: string | null, reason?: string): Promise<this>;
|
|
725
725
|
public fetchInvites(cache?: boolean): Promise<Collection<string, Invite>>;
|
|
726
726
|
public setBitrate(bitrate: number, reason?: string): Promise<VoiceChannel>;
|
|
727
727
|
public setUserLimit(userLimit: number, reason?: string): Promise<VoiceChannel>;
|
|
728
728
|
public setVideoQualityMode(videoQualityMode: VideoQualityMode | number, reason?: string): Promise<VoiceChannel>;
|
|
729
|
+
public setStatus(status: string | null): Promise<this>;
|
|
729
730
|
}
|
|
730
731
|
|
|
731
732
|
export class BaseMessageComponent {
|
|
@@ -1214,6 +1215,16 @@ export interface StartStreamOptions {
|
|
|
1214
1215
|
livestream?: boolean;
|
|
1215
1216
|
/** Low-latency encoding and playback. Default true. */
|
|
1216
1217
|
lowLatency?: boolean;
|
|
1218
|
+
/** Video encoder. Default 'auto' (NVENC if available). */
|
|
1219
|
+
encoder?: 'auto' | 'amf' | 'nvenc' | 'qsv' | 'software';
|
|
1220
|
+
/** GPU decoding via -hwaccel auto. Default true. */
|
|
1221
|
+
hardwareAcceleratedDecoding?: boolean;
|
|
1222
|
+
/** NVENC preset (p1 = lowest latency). Default 'p1'. */
|
|
1223
|
+
nvencPreset?: string;
|
|
1224
|
+
/** Pre-encode locally before playback for smooth speed. Default true. */
|
|
1225
|
+
preEncode?: boolean;
|
|
1226
|
+
/** WebRTC Go Live. Default false (UDP classic, more stable). */
|
|
1227
|
+
goLive?: boolean;
|
|
1217
1228
|
}
|
|
1218
1229
|
|
|
1219
1230
|
/** Session Go Live WebRTC retournée par {@link Client#startStream}. */
|