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 CHANGED
@@ -1,7 +1,7 @@
1
1
  GNU GENERAL PUBLIC LICENSE
2
2
  Version 3, 29 June 2007
3
3
 
4
- Copyright (C) 2022 aiko-chan-ai and discordjs
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) <year> <name of author>
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
- Also add information on how to contact you by electronic and paper mail.
650
+ notice like this when it starts in an interactive mode:
651
651
 
652
- If the program does terminal interaction, make it output a short
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
  [![Star History Chart](https://api.star-history.com/svg?repos=002-sans/djs-selfbot-v13&type=Date)](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.35",
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": "docgen --source src --custom docs/index.yml --output docs/main.json",
19
- "docs:test": "docgen --source src --custom docs/index.yml",
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": [
@@ -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 WebRtcStreamSession.resolveUrl(url);
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: fps ?? 30,
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(fps),
85
+ String(streamFps),
74
86
  '-keyint_min',
75
- String(fps),
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 { PassThrough } = require('stream');
7
+ const { spawn } = require('child_process');
5
8
  const ffmpeg = require('fluent-ffmpeg');
6
- const { Streamer, playStream } = require('@dank074/discord-video-stream');
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
- let _ffmpegPathReady = null;
22
+ const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
15
23
 
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
+ /** @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
- 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;
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
- return _ffmpegPathReady;
65
+ });
35
66
  }
36
67
 
37
68
  /**
38
- * Go Live via WebRTC — ffmpeg custom (sans azmq) + playStream de la librairie.
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
- if (this._paused) return this._positionMs;
70
- return this._positionMs + (Date.now() - this._runStartWall - this._pausedTotalMs);
98
+ return this._positionMs + (Date.now() - this._runStartWall);
71
99
  }
72
100
 
73
- _resetRunClock() {
74
- this._runStartWall = Date.now();
75
- this._pausedAt = 0;
76
- this._pausedTotalMs = 0;
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
- _signalFfmpeg(signal) {
80
- const pid = this._ffmpegProc?.pid;
81
- if (!pid) return;
82
- try {
83
- process.kill(pid, signal);
84
- } catch {
85
- // process already exited
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 = 720,
97
- width = -2,
98
- bitrate = 5000,
131
+ fps = 60,
132
+ height,
133
+ width,
134
+ bitrate = 4500,
99
135
  bitrateMax,
100
136
  audioBitrate = 128,
101
- preset = 'superfast',
102
- tune = 'film',
137
+ hardwareAcceleratedDecoding = true,
103
138
  audio,
139
+ noTranscoding = false,
104
140
  } = this.options;
105
141
 
106
- const maxRate = bitrateMax ?? Math.round(bitrate * 1.5);
107
- const includeAudio = audio !== false;
108
- const output = new PassThrough();
109
- const command = ffmpeg(url);
110
-
111
- if (seekSec > 0) command.seekInput(seekSec);
112
-
113
- const isHttp = typeof url === 'string' && /^https?:\/\//i.test(url);
114
- if (isHttp) {
115
- command.inputOptions([
116
- '-reconnect 1',
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
- command.output(output).outputFormat('nut');
124
-
125
- command.addOutputOption('-map 0:v');
126
- command.videoFilter(`scale=${width}:${height}`);
127
- // fps forcé seulement si demandé explicitement (sinon on garde celui de la source = vitesse 1x)
128
- if (fps && fps > 0) command.fpsOutput(fps);
129
- command.addOutputOption([
130
- '-b:v',
131
- `${bitrate}k`,
132
- '-maxrate:v',
133
- `${maxRate}k`,
134
- '-bufsize:v',
135
- `${Math.round(bitrate / 2)}k`,
136
- '-bf',
137
- '0',
138
- '-pix_fmt',
139
- 'yuv420p',
140
- '-force_key_frames',
141
- 'expr:gte(t,n_forced*1)',
142
- ]);
143
- command.videoCodec('libx264');
144
- command.outputOptions(['-forced-idr 1', `-tune ${tune}`, `-preset ${preset}`]);
145
-
146
- if (includeAudio) {
147
- command.addOutputOption('-map 0:a:0?');
148
- command.audioChannels(2);
149
- command.audioFrequency(48000);
150
- command.audioCodec('libopus');
151
- command.audioBitrate(`${audioBitrate}k`);
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
- return { command, output };
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 ensureFfmpegPath();
160
-
161
- const { command, output } = this._buildFfmpeg(url, seekSec);
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
- const msg = err?.message || String(err);
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
- const onAbort = () => {
183
- this._running = false;
184
- this._ffmpegProc = null;
185
- command.kill('SIGTERM');
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
- const { livestream } = this.options;
191
- const playOptions = { type: 'go-live' };
192
- if (livestream) playOptions.readrateInitialBurst = 10;
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._ffmpegProc = null;
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._ffmpegProc = null;
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 { guildId, channelId, url } = this.options;
217
- await ensureFfmpegPath();
218
- await this.streamer.joinVoice(guildId, channelId);
261
+ const { url } = this.options;
262
+ await this._joinVoice();
263
+ this._started = true;
219
264
 
220
- if (this.client.user?.voice?.channel instanceof StageChannel) {
221
- await this.client.user.voice.setSuppressed(false);
222
- }
265
+ const playing = new Promise((resolve, reject) => {
266
+ this.once('playing', resolve);
267
+ this.once('error', reject);
268
+ });
223
269
 
224
- this._started = true;
225
- void this._run(url, this._abort.signal);
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, seekSec);
290
+ await this._run(this.options.url, this._abort.signal, this._positionMs / 1000);
247
291
  return;
248
292
  }
249
- if (!this._running || !this._paused) return;
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._ffmpegProc = null;
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;
@@ -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?: string;
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}. */