djs-selfbot-v13 3.7.36 → 3.7.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
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,192 @@ 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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
101
|
+
async _pickEncoder() {
|
|
102
|
+
const { preset = 'ultrafast', encoder = 'auto' } = this.options;
|
|
103
|
+
let mode = encoder;
|
|
104
|
+
if (encoder === 'auto') mode = await detectHwEncoder();
|
|
105
|
+
|
|
106
|
+
if (mode === 'nvenc') {
|
|
107
|
+
console.log('[stream] encodeur: h264_nvenc (p1 ll)');
|
|
108
|
+
const llOpts = [
|
|
109
|
+
'-preset', 'p1',
|
|
110
|
+
'-tune', 'll',
|
|
111
|
+
'-delay', '0',
|
|
112
|
+
'-rc-lookahead', '0',
|
|
113
|
+
'-no-scenecut', '1',
|
|
114
|
+
'-zerolatency', '1',
|
|
115
|
+
];
|
|
116
|
+
return () => ({
|
|
117
|
+
H264: { name: 'h264_nvenc', options: llOpts },
|
|
118
|
+
H265: { name: 'hevc_nvenc', options: llOpts },
|
|
119
|
+
AV1: { name: 'av1_nvenc', options: llOpts },
|
|
120
|
+
});
|
|
86
121
|
}
|
|
122
|
+
if (mode === 'amf') {
|
|
123
|
+
console.log('[stream] encodeur: h264_amf (ultra low latency)');
|
|
124
|
+
return () => ({
|
|
125
|
+
H264: {
|
|
126
|
+
name: 'h264_amf',
|
|
127
|
+
options: ['-usage', 'ultralowlatency', '-quality', 'speed', '-preanalysis', 'false'],
|
|
128
|
+
},
|
|
129
|
+
H265: {
|
|
130
|
+
name: 'hevc_amf',
|
|
131
|
+
options: ['-usage', 'ultralowlatency', '-quality', 'speed', '-preanalysis', 'false'],
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
console.log('[stream] encodeur: libx264');
|
|
136
|
+
return Encoders.software({
|
|
137
|
+
x264: { preset, tune: 'zerolatency' },
|
|
138
|
+
});
|
|
87
139
|
}
|
|
88
140
|
|
|
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) {
|
|
141
|
+
async _buildPrepareOptions(seekSec = 0) {
|
|
94
142
|
const {
|
|
95
|
-
fps,
|
|
96
|
-
height
|
|
97
|
-
width
|
|
98
|
-
bitrate =
|
|
143
|
+
fps = 60,
|
|
144
|
+
height,
|
|
145
|
+
width,
|
|
146
|
+
bitrate = 4500,
|
|
99
147
|
bitrateMax,
|
|
100
148
|
audioBitrate = 128,
|
|
101
|
-
|
|
102
|
-
tune = 'film',
|
|
149
|
+
hardwareAcceleratedDecoding = true,
|
|
103
150
|
audio,
|
|
151
|
+
noTranscoding = false,
|
|
104
152
|
} = this.options;
|
|
105
153
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
154
|
+
if (noTranscoding) {
|
|
155
|
+
console.log('[stream] mode copie vidéo (pas de ré-encodage)');
|
|
156
|
+
return {
|
|
157
|
+
noTranscoding: true,
|
|
158
|
+
bitrateAudio: audioBitrate,
|
|
159
|
+
includeAudio: audio !== false,
|
|
160
|
+
hardwareAcceleratedDecoding: false,
|
|
161
|
+
minimizeLatency: true,
|
|
162
|
+
customInputOptions: seekSec > 0 ? [`-ss ${String(seekSec)}`] : [],
|
|
163
|
+
customFfmpegFlags: [],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const gop = String(fps * 2);
|
|
168
|
+
const is720 = Number(height) >= 720;
|
|
169
|
+
const videoBufK = Math.max(Math.round(bitrate * 1.25), is720 ? 3500 : 1200);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
encoder: await this._pickEncoder(),
|
|
173
|
+
height,
|
|
174
|
+
width: width || (is720 ? 1280 : Math.round((height * 16) / 9 / 2) * 2),
|
|
175
|
+
frameRate: fps,
|
|
176
|
+
bitrateVideo: bitrate,
|
|
177
|
+
bitrateVideoMax: bitrateMax ?? Math.round(bitrate * 1.3),
|
|
178
|
+
bitrateAudio: audioBitrate,
|
|
179
|
+
includeAudio: audio !== false,
|
|
180
|
+
hardwareAcceleratedDecoding,
|
|
181
|
+
minimizeLatency: !is720,
|
|
182
|
+
customInputOptions: seekSec > 0 ? [`-ss ${String(seekSec)}`] : [],
|
|
183
|
+
customFfmpegFlags: [
|
|
184
|
+
'-g', gop,
|
|
185
|
+
'-keyint_min', gop,
|
|
186
|
+
'-sc_threshold', '0',
|
|
187
|
+
'-threads', '0',
|
|
188
|
+
'-bufsize:v', `${videoBufK}k`,
|
|
189
|
+
'-maxrate:v', `${bitrateMax ?? Math.round(bitrate * 1.3)}k`,
|
|
190
|
+
'-force_key_frames', 'expr:gte(t,n_forced*2)',
|
|
191
|
+
'-max_muxing_queue_size', '1024',
|
|
192
|
+
'-flush_packets', '1',
|
|
193
|
+
],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async _disconnectLegacyVoice() {
|
|
198
|
+
const conn = this.client.voice?.connection;
|
|
199
|
+
if (conn) {
|
|
200
|
+
conn.disconnect();
|
|
201
|
+
this.client.voice.connection = null;
|
|
202
|
+
await sleep(800);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async _joinVoice() {
|
|
207
|
+
const { guildId, channelId } = this.options;
|
|
208
|
+
|
|
209
|
+
if (this.streamer.voiceConnection) {
|
|
210
|
+
this.streamer.leaveVoice();
|
|
211
|
+
await sleep(1000);
|
|
121
212
|
}
|
|
122
213
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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');
|
|
214
|
+
await this._disconnectLegacyVoice();
|
|
215
|
+
|
|
216
|
+
const vc = this.streamer.voiceConnection;
|
|
217
|
+
if (
|
|
218
|
+
!vc ||
|
|
219
|
+
vc.guildId !== guildId ||
|
|
220
|
+
vc.channelId !== channelId
|
|
221
|
+
) {
|
|
222
|
+
await this.streamer.joinVoice(guildId, channelId);
|
|
153
223
|
}
|
|
154
224
|
|
|
155
|
-
|
|
225
|
+
if (this.client.user?.voice?.channel instanceof StageChannel) {
|
|
226
|
+
await this.client.user.voice.setSuppressed(false);
|
|
227
|
+
}
|
|
156
228
|
}
|
|
157
229
|
|
|
158
230
|
async _run(url, signal, seekSec = 0) {
|
|
159
|
-
await
|
|
231
|
+
const prepareOpts = await this._buildPrepareOptions(seekSec);
|
|
232
|
+
const { command, output } = prepareStream(url, prepareOpts, signal);
|
|
233
|
+
this._ffmpegCommand = command;
|
|
160
234
|
|
|
161
|
-
|
|
162
|
-
this._command = command;
|
|
163
|
-
this._ffmpegProc = null;
|
|
164
|
-
this._running = true;
|
|
165
|
-
this._paused = false;
|
|
166
|
-
this._stopped = false;
|
|
167
|
-
this._resetRunClock();
|
|
168
|
-
|
|
169
|
-
command.on('start', cmd => {
|
|
170
|
-
this._ffmpegProc = command.ffmpegProc ?? null;
|
|
171
|
-
this.emit('debug', `[cmd] ${cmd}`);
|
|
172
|
-
});
|
|
235
|
+
command.on('start', cmd => this.emit('debug', `[ffmpeg] ${cmd}`));
|
|
173
236
|
command.on('stderr', line => this.emit('debug', String(line)));
|
|
174
|
-
|
|
175
237
|
command.on('error', (err, _stdout, stderr) => {
|
|
176
|
-
if (stderr) this.emit('debug', String(stderr));
|
|
177
238
|
if (signal.aborted) return;
|
|
178
|
-
|
|
179
|
-
this.emit('error', new Error(stderr ? `${msg}\n${stderr}` : msg));
|
|
239
|
+
this.emit('error', new Error(stderr ? `${err.message}\n${stderr}` : err.message));
|
|
180
240
|
});
|
|
181
241
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
};
|
|
187
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
188
|
-
command.run();
|
|
242
|
+
this._running = true;
|
|
243
|
+
this._paused = false;
|
|
244
|
+
this._stopped = false;
|
|
245
|
+
this._runStartWall = Date.now();
|
|
189
246
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
247
|
+
let playOptions;
|
|
248
|
+
if (this.options.noTranscoding) {
|
|
249
|
+
console.log('[stream] envoi à la résolution/fps natifs de la source (WebRTC)');
|
|
250
|
+
playOptions = {};
|
|
251
|
+
} else {
|
|
252
|
+
const { height = 720, width: optWidth, fps = 60, bitrate } = this.options;
|
|
253
|
+
const width = optWidth || (height >= 720 ? 1280 : Math.round((height * 16) / 9 / 2) * 2);
|
|
254
|
+
console.log(
|
|
255
|
+
`[stream] envoi ${width}x${height} @ ${fps}fps, ${bitrate ?? '?'}kbps (WebRTC)`,
|
|
256
|
+
);
|
|
257
|
+
playOptions = {
|
|
258
|
+
width,
|
|
259
|
+
height,
|
|
260
|
+
frameRate: fps,
|
|
261
|
+
readrateInitialBurst: 1,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
this.emit('playing');
|
|
193
265
|
|
|
194
266
|
this._playPromise = playStream(output, this.streamer, playOptions, signal);
|
|
195
267
|
|
|
196
268
|
this._playPromise
|
|
197
269
|
.then(() => {
|
|
198
270
|
this._running = false;
|
|
199
|
-
this.
|
|
271
|
+
this._ffmpegCommand = null;
|
|
200
272
|
if (!signal.aborted) {
|
|
201
273
|
this._positionMs = 0;
|
|
202
274
|
this.emit('finish');
|
|
@@ -204,7 +276,7 @@ class WebRtcStreamSession extends EventEmitter {
|
|
|
204
276
|
})
|
|
205
277
|
.catch(err => {
|
|
206
278
|
this._running = false;
|
|
207
|
-
this.
|
|
279
|
+
this._ffmpegCommand = null;
|
|
208
280
|
if (signal.aborted || err?.name === 'AbortError') return;
|
|
209
281
|
this.emit('error', err);
|
|
210
282
|
});
|
|
@@ -213,66 +285,62 @@ class WebRtcStreamSession extends EventEmitter {
|
|
|
213
285
|
}
|
|
214
286
|
|
|
215
287
|
async start() {
|
|
216
|
-
const {
|
|
217
|
-
await
|
|
218
|
-
|
|
288
|
+
const { url } = this.options;
|
|
289
|
+
await this._joinVoice();
|
|
290
|
+
this._started = true;
|
|
219
291
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
292
|
+
const playing = new Promise((resolve, reject) => {
|
|
293
|
+
this.once('playing', resolve);
|
|
294
|
+
this.once('error', reject);
|
|
295
|
+
});
|
|
223
296
|
|
|
224
|
-
this.
|
|
225
|
-
|
|
297
|
+
void this._run(url, this._abort.signal).catch(err => this.emit('error', err));
|
|
298
|
+
await playing;
|
|
226
299
|
}
|
|
227
300
|
|
|
228
|
-
/**
|
|
229
|
-
* Met en pause la lecture (ffmpeg suspendu, connexion vocale conservée).
|
|
230
|
-
*/
|
|
231
301
|
pause() {
|
|
232
302
|
if (!this._running || this._paused || this._stopped) return;
|
|
233
303
|
this._positionMs = this._getPositionMs();
|
|
234
|
-
this._pausedAt = Date.now();
|
|
235
|
-
this._signalFfmpeg('SIGSTOP');
|
|
236
304
|
this._paused = true;
|
|
305
|
+
this._running = false;
|
|
306
|
+
this._ffmpegCommand?.kill?.('SIGTERM');
|
|
307
|
+
this._ffmpegCommand = null;
|
|
308
|
+
this.streamer.stopStream();
|
|
309
|
+
this._abort.abort();
|
|
310
|
+
this._abort = new AbortController();
|
|
311
|
+
console.log(`[stream] pause à ${(this._positionMs / 1000).toFixed(1)}s`);
|
|
237
312
|
}
|
|
238
313
|
|
|
239
|
-
/**
|
|
240
|
-
* Reprend la lecture (après pause ou stop).
|
|
241
|
-
*/
|
|
242
314
|
async resume() {
|
|
243
315
|
if (this._stopped) {
|
|
244
|
-
const seekSec = this._positionMs / 1000;
|
|
245
316
|
this._stopped = false;
|
|
246
|
-
await this._run(this.options.url, this._abort.signal,
|
|
317
|
+
await this._run(this.options.url, this._abort.signal, this._positionMs / 1000);
|
|
247
318
|
return;
|
|
248
319
|
}
|
|
249
|
-
if (!this.
|
|
250
|
-
this._pausedTotalMs += Date.now() - this._pausedAt;
|
|
251
|
-
this._pausedAt = 0;
|
|
252
|
-
this._signalFfmpeg('SIGCONT');
|
|
320
|
+
if (!this._paused) return;
|
|
253
321
|
this._paused = false;
|
|
322
|
+
const seekSec = this._positionMs / 1000;
|
|
323
|
+
console.log(`[stream] reprise à ${seekSec.toFixed(1)}s`);
|
|
324
|
+
await this._run(this.options.url, this._abort.signal, seekSec);
|
|
254
325
|
}
|
|
255
326
|
|
|
256
|
-
/**
|
|
257
|
-
* Arrête la lecture en conservant la position et la connexion vocale.
|
|
258
|
-
*/
|
|
259
327
|
stop() {
|
|
260
|
-
if (!this._running) return;
|
|
328
|
+
if (!this._running && !this._paused) return;
|
|
261
329
|
this._positionMs = this._getPositionMs();
|
|
262
330
|
this._stopped = true;
|
|
263
331
|
this._paused = false;
|
|
264
332
|
this._running = false;
|
|
265
|
-
this.
|
|
333
|
+
this._ffmpegCommand?.kill?.('SIGTERM');
|
|
334
|
+
this._ffmpegCommand = null;
|
|
335
|
+
this.streamer.stopStream();
|
|
266
336
|
this._abort.abort();
|
|
267
337
|
this._abort = new AbortController();
|
|
338
|
+
console.log(`[stream] stop à ${(this._positionMs / 1000).toFixed(1)}s`);
|
|
268
339
|
}
|
|
269
340
|
|
|
270
|
-
/**
|
|
271
|
-
* Relance depuis la dernière position enregistrée.
|
|
272
|
-
*/
|
|
273
341
|
async replay() {
|
|
274
342
|
const seekSec = (this._running ? this._getPositionMs() : this._positionMs) / 1000;
|
|
275
|
-
if (this._running) this.stop();
|
|
343
|
+
if (this._running || this._paused) this.stop();
|
|
276
344
|
this._stopped = false;
|
|
277
345
|
await this._run(this.options.url, this._abort.signal, seekSec);
|
|
278
346
|
}
|
package/typings/index.d.ts
CHANGED
|
@@ -1215,6 +1215,16 @@ export interface StartStreamOptions {
|
|
|
1215
1215
|
livestream?: boolean;
|
|
1216
1216
|
/** Low-latency encoding and playback. Default true. */
|
|
1217
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;
|
|
1218
1228
|
}
|
|
1219
1229
|
|
|
1220
1230
|
/** Session Go Live WebRTC retournée par {@link Client#startStream}. */
|