djs-selfbot-v13 3.7.32 → 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.
@@ -16,6 +16,14 @@ const { Opcodes, VoiceOpcodes, VoiceStatus, Events } = require('../../util/Const
16
16
  const Speaking = require('../../util/Speaking');
17
17
  const Util = require('../../util/Util');
18
18
 
19
+ function normalizeVoiceEndpoint(endpoint, { preservePort = false } = {}) {
20
+ const match = endpoint?.match(/^([^:]+)(?::(\d+))?/);
21
+ if (!match) return endpoint;
22
+ const [, host, port] = match;
23
+ if (!preservePort || !port || port === '443' || port === '80') return host;
24
+ return `${host}:${port}`;
25
+ }
26
+
19
27
  // Workaround for Discord now requiring silence to be sent before being able to receive audio
20
28
  class SingleSilence extends Silence {
21
29
  _read() {
@@ -166,6 +174,18 @@ class VoiceConnection extends EventEmitter {
166
174
  * @type {Collection<Snowflake, StreamConnectionReadonly>}
167
175
  */
168
176
  this.streamWatchConnection = new Collection();
177
+
178
+ /**
179
+ * DAVE end-to-end encryption session
180
+ * @type {?DAVESession}
181
+ */
182
+ this.dave = null;
183
+
184
+ /**
185
+ * Connected client user IDs for DAVE MLS
186
+ * @type {Set<Snowflake>}
187
+ */
188
+ this.connectedClients = new Set();
169
189
  }
170
190
 
171
191
  /**
@@ -228,6 +248,20 @@ class VoiceConnection extends EventEmitter {
228
248
  return this;
229
249
  }
230
250
 
251
+ /**
252
+ * SSRC values assigned by the voice server.
253
+ * @returns {{ audioSsrc: number, videoSsrc: number, rtxSsrc: number }}
254
+ */
255
+ getStreamSsrcs() {
256
+ const stream = this.authentication.streams?.[0];
257
+ const audioSsrc = this.authentication.ssrc;
258
+ return {
259
+ audioSsrc,
260
+ videoSsrc: stream?.ssrc ?? audioSsrc + 1,
261
+ rtxSsrc: stream?.rtx_ssrc ?? audioSsrc + 2,
262
+ };
263
+ }
264
+
231
265
  /**
232
266
  * Sets video status
233
267
  * @param {boolean} value Video on or off
@@ -236,6 +270,10 @@ class VoiceConnection extends EventEmitter {
236
270
  if (value === this.videoStatus) return;
237
271
  if (this.status !== VoiceStatus.CONNECTED) return;
238
272
  this.videoStatus = value;
273
+ const attrs = this.videoAttributes ?? {};
274
+ const fps = attrs.fps ?? 30;
275
+ const height = attrs.height ?? 720;
276
+ const width = attrs.width ?? Math.round((height * 16) / 9);
239
277
  if (!value) {
240
278
  this.sockets.ws
241
279
  .sendPacket({
@@ -251,27 +289,28 @@ class VoiceConnection extends EventEmitter {
251
289
  this.emit('debug', e);
252
290
  });
253
291
  } else {
292
+ const { audioSsrc, videoSsrc, rtxSsrc } = this.getStreamSsrcs();
254
293
  this.sockets.ws
255
294
  .sendPacket({
256
295
  op: VoiceOpcodes.SOURCES,
257
296
  d: {
258
- audio_ssrc: this.authentication.ssrc,
259
- video_ssrc: this.authentication.ssrc + 1,
260
- rtx_ssrc: this.authentication.ssrc + 2,
297
+ audio_ssrc: audioSsrc,
298
+ video_ssrc: videoSsrc,
299
+ rtx_ssrc: rtxSsrc,
261
300
  streams: [
262
301
  {
263
302
  type: 'video',
264
303
  rid: '100',
265
- ssrc: this.authentication.ssrc + 1,
304
+ ssrc: videoSsrc,
266
305
  active: true,
267
306
  quality: 100,
268
- rtx_ssrc: this.authentication.ssrc + 2,
269
- max_bitrate: 8000000,
270
- max_framerate: 60,
307
+ rtx_ssrc: rtxSsrc,
308
+ max_bitrate: 10_000_000,
309
+ max_framerate: fps,
271
310
  max_resolution: {
272
- type: 'source',
273
- width: 0,
274
- height: 0,
311
+ type: 'fixed',
312
+ width,
313
+ height,
275
314
  },
276
315
  },
277
316
  ],
@@ -305,11 +344,14 @@ class VoiceConnection extends EventEmitter {
305
344
  self_mute: this.voice ? this.voice.selfMute : false,
306
345
  self_deaf: this.voice ? this.voice.selfDeaf : false,
307
346
  self_video: this.voice ? this.voice.selfVideo : false,
308
- flags: 2,
309
347
  },
310
348
  options,
311
349
  );
312
350
 
351
+ if (options.self_video) {
352
+ options.flags = 2;
353
+ }
354
+
313
355
  this.emit('debug', `Sending voice state update: ${JSON.stringify(options)}`);
314
356
 
315
357
  return this.channel.client.ws.broadcast({
@@ -371,6 +413,21 @@ class VoiceConnection extends EventEmitter {
371
413
  this.checkAuthenticated();
372
414
  } else if (sessionId !== this.authentication.sessionId) {
373
415
  this.authentication.sessionId = sessionId;
416
+ if (
417
+ this.constructor.name !== 'StreamConnection' &&
418
+ [VoiceStatus.CONNECTED, VoiceStatus.CONNECTING].includes(this.status)
419
+ ) {
420
+ this.status = VoiceStatus.RECONNECTING;
421
+ if (this.sockets.ws) {
422
+ this.sockets.ws.shutdown();
423
+ this.sockets.ws = null;
424
+ }
425
+ if (this.sockets.udp) {
426
+ this.sockets.udp.shutdown();
427
+ this.sockets.udp = null;
428
+ }
429
+ this.checkAuthenticated();
430
+ }
374
431
  /**
375
432
  * Emitted when a new session ID is received.
376
433
  * @event VoiceConnection#newSession
@@ -386,8 +443,9 @@ class VoiceConnection extends EventEmitter {
386
443
  */
387
444
  checkAuthenticated() {
388
445
  const { token, endpoint, sessionId } = this.authentication;
446
+ const needsServerId = this.constructor.name === 'StreamConnection';
389
447
  this.emit('debug', `Authenticated with sessionId ${sessionId}`);
390
- if (token && endpoint && sessionId) {
448
+ if (token && endpoint && sessionId && (!needsServerId || this.serverId)) {
391
449
  this.status = VoiceStatus.CONNECTING;
392
450
  /**
393
451
  * Emitted when we successfully initiate a voice connection.
@@ -440,8 +498,9 @@ class VoiceConnection extends EventEmitter {
440
498
  * @private
441
499
  */
442
500
  authenticate(options = {}) {
501
+ this._joinOptions = options;
443
502
  this.sendVoiceStateUpdate(options);
444
- this.connectTimeout = setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 15_000).unref();
503
+ this.connectTimeout = setTimeout(() => this.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 30_000).unref();
445
504
  }
446
505
 
447
506
  /**
@@ -500,6 +559,16 @@ class VoiceConnection extends EventEmitter {
500
559
  cleanup() {
501
560
  this.player.destroy();
502
561
  this.speaking = new Speaking().freeze();
562
+ if (this.dave) {
563
+ const isSharedDave =
564
+ this.constructor.name === 'StreamConnection' && this.voiceConnection?.dave === this.dave;
565
+ if (!isSharedDave) {
566
+ this.dave.destroy();
567
+ this.dave.removeAllListeners();
568
+ }
569
+ this.dave = null;
570
+ }
571
+ this.connectedClients.clear();
503
572
  const { ws, udp } = this.sockets;
504
573
 
505
574
  this.emit('debug', 'Connection clean up');
@@ -571,11 +640,21 @@ class VoiceConnection extends EventEmitter {
571
640
  * @param {Object} data The received data
572
641
  * @private
573
642
  */
643
+ isDaveReady() {
644
+ return (
645
+ !this.authentication.dave_protocol_version ||
646
+ (this.dave?.lastTransitionId === 0 && Boolean(this.dave?.session?.ready))
647
+ );
648
+ }
649
+
574
650
  onSessionDescription(data) {
575
651
  Object.assign(this.authentication, data);
576
652
  this.status = VoiceStatus.CONNECTED;
653
+ clearTimeout(this.connectTimeout);
654
+ let daveFallback;
577
655
  const ready = () => {
578
656
  clearTimeout(this.connectTimeout);
657
+ clearTimeout(daveFallback);
579
658
  this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`);
580
659
  /**
581
660
  * Emitted once the connection is ready, when a promise to join a voice channel resolves,
@@ -584,10 +663,32 @@ class VoiceConnection extends EventEmitter {
584
663
  */
585
664
  this.emit('ready');
586
665
  };
587
- if (this.dispatcher || this.videoDispatcher) {
666
+ const waitForDave = () => {
667
+ if (this.isDaveReady()) {
668
+ ready();
669
+ return;
670
+ }
671
+ const onTransitioned = () => {
672
+ if (this.isDaveReady()) {
673
+ this.sockets.ws?.removeListener('transitioned', onTransitioned);
674
+ ready();
675
+ }
676
+ };
677
+ this.sockets.ws?.on('transitioned', onTransitioned);
678
+ daveFallback = setTimeout(() => {
679
+ this.sockets.ws?.removeListener('transitioned', onTransitioned);
680
+ this.emit('debug', 'DAVE handshake incomplete, continuing with transport encryption only');
681
+ ready();
682
+ }, 15_000).unref();
683
+ };
684
+ if (data.dave_protocol_version) {
685
+ if (!this.dispatcher && !this.videoDispatcher) {
686
+ this.playAudio(new SingleSilence(), { type: 'opus', volume: false });
687
+ }
688
+ waitForDave();
689
+ } else if (this.dispatcher || this.videoDispatcher) {
588
690
  ready();
589
691
  } else {
590
- // This serves to provide support for voice receive, sending audio is required to receive it.
591
692
  const dispatcher = this.playAudio(new SingleSilence(), { type: 'opus', volume: false });
592
693
  dispatcher.once('finish', ready);
593
694
  }
@@ -672,12 +773,97 @@ class VoiceConnection extends EventEmitter {
672
773
  * Create new connection to screenshare stream
673
774
  * @returns {Promise<StreamConnection>}
674
775
  */
776
+ waitForDaveReady() {
777
+ if (this.isDaveReady() || this.status === VoiceStatus.CONNECTED) return Promise.resolve();
778
+ return new Promise((resolve, reject) => {
779
+ const timeout = setTimeout(() => reject(new Error('VOICE_CONNECTION_TIMEOUT')), 30_000).unref();
780
+ const onReady = () => {
781
+ if (!this.isDaveReady()) return;
782
+ clearTimeout(timeout);
783
+ this.sockets.ws?.removeListener('transitioned', onReady);
784
+ resolve();
785
+ };
786
+ this.sockets.ws?.on('transitioned', onReady);
787
+ onReady();
788
+ });
789
+ }
790
+
791
+ stopExistingStream() {
792
+ return new Promise(resolve => {
793
+ const hadStreamConnection = Boolean(this.streamConnection);
794
+ if (this.streamConnection) {
795
+ this.streamConnection.disconnect();
796
+ this.streamConnection = null;
797
+ }
798
+
799
+ const streamKey = `${['DM', 'GROUP_DM'].includes(this.channel.type) ? 'call' : `guild:${this.channel.guild.id}`}:${
800
+ this.channel.id
801
+ }:${this.client.user.id}`;
802
+
803
+ const voiceState = this.channel.guild?.voiceStates.cache.get(this.client.user.id);
804
+ const wasStreaming = this.client.user?.voice?.streaming || voiceState?.streaming;
805
+
806
+ if (!wasStreaming && !hadStreamConnection) {
807
+ setTimeout(resolve, 200).unref();
808
+ return;
809
+ }
810
+
811
+ const sendDelete = () => {
812
+ this.channel.client.ws.broadcast({
813
+ op: Opcodes.STREAM_DELETE,
814
+ d: { stream_key: streamKey },
815
+ });
816
+ };
817
+
818
+ sendDelete();
819
+ if (wasStreaming) setTimeout(sendDelete, 400).unref();
820
+
821
+ let settled = false;
822
+ const cleanup = () => {
823
+ this.client.off(Events.VOICE_STATE_UPDATE, onState);
824
+ this.off('ready', onVoiceReady);
825
+ };
826
+ const finish = () => {
827
+ if (settled) return;
828
+ if (this.status !== VoiceStatus.CONNECTED) return;
829
+ settled = true;
830
+ cleanup();
831
+ setTimeout(resolve, 500).unref();
832
+ };
833
+
834
+ const onState = (_oldState, newState) => {
835
+ if (newState.id !== this.client.user.id || newState.streaming) return;
836
+ finish();
837
+ };
838
+ const onVoiceReady = () => finish();
839
+
840
+ this.client.on(Events.VOICE_STATE_UPDATE, onState);
841
+ this.on('ready', onVoiceReady);
842
+
843
+ if (!wasStreaming) {
844
+ setTimeout(resolve, 1500).unref();
845
+ return;
846
+ }
847
+
848
+ setTimeout(() => finish(), 2000).unref();
849
+ setTimeout(() => {
850
+ if (settled) return;
851
+ settled = true;
852
+ cleanup();
853
+ resolve();
854
+ }, 12_000).unref();
855
+ });
856
+ }
857
+
675
858
  createStreamConnection() {
676
859
  // eslint-disable-next-line consistent-return
677
860
  return new Promise((resolve, reject) => {
678
861
  if (this.streamConnection) {
679
862
  return resolve(this.streamConnection);
680
863
  } else {
864
+ this.waitForDaveReady()
865
+ .then(() => this.stopExistingStream())
866
+ .then(() => {
681
867
  const connection = (this.streamConnection = new StreamConnection(this.voiceManager, this.channel, this));
682
868
  connection.setVideoCodec(this.videoCodec); // Sync :?
683
869
  // Setup event...
@@ -697,7 +883,7 @@ class VoiceConnection extends EventEmitter {
697
883
  // Current user stream
698
884
  switch (event) {
699
885
  case 'STREAM_CREATE': {
700
- this.streamConnection.setSessionId(this.authentication.sessionId);
886
+ this.streamConnection.setSessionId(data.session_id || this.authentication.sessionId);
701
887
  this.streamConnection.serverId = data.rtc_server_id;
702
888
  break;
703
889
  }
@@ -706,6 +892,13 @@ class VoiceConnection extends EventEmitter {
706
892
  break;
707
893
  }
708
894
  case 'STREAM_DELETE': {
895
+ if (
896
+ [VoiceStatus.CONNECTING, VoiceStatus.AUTHENTICATING, VoiceStatus.RECONNECTING].includes(
897
+ this.streamConnection.status,
898
+ )
899
+ ) {
900
+ break;
901
+ }
709
902
  this.streamConnection.disconnect();
710
903
  break;
711
904
  }
@@ -743,7 +936,7 @@ class VoiceConnection extends EventEmitter {
743
936
  }
744
937
 
745
938
  connection.sendSignalScreenshare();
746
- connection.sendScreenshareState(true);
939
+ connection.connectTimeout = setTimeout(() => connection.authenticateFailed('VOICE_CONNECTION_TIMEOUT'), 30_000).unref();
747
940
 
748
941
  connection.on('debug', msg =>
749
942
  this.channel.client.emit(
@@ -760,6 +953,7 @@ class VoiceConnection extends EventEmitter {
760
953
 
761
954
  connection.once('authenticated', () => {
762
955
  connection.once('ready', () => {
956
+ connection.sendScreenshareState(false);
763
957
  resolve(connection);
764
958
  connection.removeListener('error', reject);
765
959
  });
@@ -767,6 +961,8 @@ class VoiceConnection extends EventEmitter {
767
961
  this.streamConnection = null;
768
962
  });
769
963
  });
964
+ })
965
+ .catch(reject);
770
966
  }
771
967
  });
772
968
  }
@@ -811,7 +1007,7 @@ class VoiceConnection extends EventEmitter {
811
1007
  // Current user stream
812
1008
  switch (event) {
813
1009
  case 'STREAM_CREATE': {
814
- this.streamConnection.setSessionId(this.authentication.sessionId);
1010
+ this.streamConnection.setSessionId(data.session_id || this.authentication.sessionId);
815
1011
  this.streamConnection.serverId = data.rtc_server_id;
816
1012
  break;
817
1013
  }
@@ -820,6 +1016,13 @@ class VoiceConnection extends EventEmitter {
820
1016
  break;
821
1017
  }
822
1018
  case 'STREAM_DELETE': {
1019
+ if (
1020
+ [VoiceStatus.CONNECTING, VoiceStatus.AUTHENTICATING, VoiceStatus.RECONNECTING].includes(
1021
+ this.streamConnection.status,
1022
+ )
1023
+ ) {
1024
+ break;
1025
+ }
823
1026
  this.streamConnection.disconnect();
824
1027
  break;
825
1028
  }
@@ -963,6 +1166,49 @@ class StreamConnection extends VoiceConnection {
963
1166
  return Promise.resolve(this);
964
1167
  }
965
1168
 
1169
+ setTokenAndEndpoint(token, endpoint) {
1170
+ this.emit('debug', `Token "${token}" and endpoint "${endpoint}"`);
1171
+ if (!endpoint) return;
1172
+
1173
+ if (!token) {
1174
+ this.authenticateFailed('VOICE_TOKEN_ABSENT');
1175
+ return;
1176
+ }
1177
+
1178
+ endpoint = normalizeVoiceEndpoint(endpoint, { preservePort: true });
1179
+ this.emit('debug', `Endpoint resolved as ${endpoint}`);
1180
+
1181
+ if (!endpoint) {
1182
+ this.authenticateFailed('VOICE_INVALID_ENDPOINT');
1183
+ return;
1184
+ }
1185
+
1186
+ const connect = () => {
1187
+ if (this.status === VoiceStatus.AUTHENTICATING) {
1188
+ this.authentication.token = token;
1189
+ this.authentication.endpoint = endpoint;
1190
+ this.checkAuthenticated();
1191
+ } else if (token !== this.authentication.token || endpoint !== this.authentication.endpoint) {
1192
+ this.reconnect(token, endpoint);
1193
+ }
1194
+ };
1195
+
1196
+ if (this.status === VoiceStatus.AUTHENTICATING) {
1197
+ setTimeout(connect, 750).unref();
1198
+ } else {
1199
+ connect();
1200
+ }
1201
+ }
1202
+
1203
+ onSessionDescription(data) {
1204
+ Object.assign(this.authentication, data);
1205
+ this.status = VoiceStatus.CONNECTED;
1206
+ this._sessionRetries = 0;
1207
+ clearTimeout(this.connectTimeout);
1208
+ this.emit('debug', `Ready with authentication details: ${JSON.stringify(this.authentication)}`);
1209
+ this.emit('ready');
1210
+ }
1211
+
966
1212
  joinStreamConnection() {
967
1213
  throw new Error('STREAM_CANNOT_JOIN');
968
1214
  }
@@ -1005,10 +1251,11 @@ class StreamConnection extends VoiceConnection {
1005
1251
  preferred_region: null,
1006
1252
  };
1007
1253
  this.emit('debug', `Signal Stream: ${JSON.stringify(data)}`);
1008
- return this.channel.client.ws.broadcast({
1254
+ this.channel.client.ws.broadcast({
1009
1255
  op: Opcodes.STREAM_CREATE,
1010
1256
  d: data,
1011
1257
  });
1258
+ this.sendScreenshareState(false);
1012
1259
  }
1013
1260
 
1014
1261
  /**
@@ -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;