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.
- package/package.json +8 -1
- package/src/client/Client.js +68 -1
- package/src/client/voice/ClientVoiceManager.js +95 -17
- package/src/client/voice/StreamSession.js +193 -0
- package/src/client/voice/VoiceConnection.js +266 -19
- package/src/client/voice/WebRtcStreamSession.js +191 -0
- package/src/client/voice/dispatcher/AnnexBDispatcher.js +64 -11
- package/src/client/voice/dispatcher/BaseDispatcher.js +13 -9
- package/src/client/voice/dispatcher/VideoDispatcher.js +33 -0
- package/src/client/voice/networking/DAVESession.js +234 -0
- package/src/client/voice/networking/VoiceWebSocket.js +240 -24
- package/src/client/voice/player/MediaPlayer.js +3 -1
- package/src/client/voice/player/processing/AnnexBBitstreamReaderWriter.js +137 -0
- package/src/client/voice/player/processing/SPSVUIRewriter.js +203 -0
- package/src/client/voice/receiver/PacketHandler.js +11 -4
- package/src/errors/Messages.js +8 -1
- package/src/util/Constants.js +12 -0
- package/typings/index.d.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "djs-selfbot-v13",
|
|
3
|
-
"version": "3.7.
|
|
3
|
+
"version": "3.7.34",
|
|
4
4
|
"description": "An unofficial discord.js fork for creating selfbots",
|
|
5
5
|
"main": "./src/index.js",
|
|
6
6
|
"types": "./typings/index.d.ts",
|
|
@@ -51,6 +51,8 @@
|
|
|
51
51
|
},
|
|
52
52
|
"homepage": "https://djs-selfbot.vercel.app/",
|
|
53
53
|
"dependencies": {
|
|
54
|
+
"@dank074/discord-video-stream": "^6.0.0",
|
|
55
|
+
"@snazzah/davey": "^0.1.11",
|
|
54
56
|
"@discordjs/builders": "^1.6.3",
|
|
55
57
|
"@discordjs/collection": "^2.1.1",
|
|
56
58
|
"@sapphire/async-queue": "^1.5.5",
|
|
@@ -68,6 +70,11 @@
|
|
|
68
70
|
"werift-rtp": "^0.8.4",
|
|
69
71
|
"ws": "^8.16.0"
|
|
70
72
|
},
|
|
73
|
+
"optionalDependencies": {
|
|
74
|
+
"@snazzah/davey-linux-x64-gnu": "0.1.11",
|
|
75
|
+
"@snazzah/davey-linux-x64-musl": "0.1.11",
|
|
76
|
+
"@snazzah/davey-win32-x64-msvc": "0.1.11"
|
|
77
|
+
},
|
|
71
78
|
"engines": {
|
|
72
79
|
"node": ">=20.18"
|
|
73
80
|
},
|
package/src/client/Client.js
CHANGED
|
@@ -9,6 +9,8 @@ const { authenticator } = require('otplib');
|
|
|
9
9
|
const BaseClient = require('./BaseClient');
|
|
10
10
|
const ActionsManager = require('./actions/ActionsManager');
|
|
11
11
|
const ClientVoiceManager = require('./voice/ClientVoiceManager');
|
|
12
|
+
const StreamSession = require('./voice/StreamSession');
|
|
13
|
+
const WebRtcStreamSession = require('./voice/WebRtcStreamSession');
|
|
12
14
|
const WebSocketManager = require('./websocket/WebSocketManager');
|
|
13
15
|
const { Error, TypeError } = require('../errors');
|
|
14
16
|
const BaseGuildEmojiManager = require('../managers/BaseGuildEmojiManager');
|
|
@@ -36,7 +38,7 @@ const VoiceRegion = require('../structures/VoiceRegion');
|
|
|
36
38
|
const Webhook = require('../structures/Webhook');
|
|
37
39
|
const Widget = require('../structures/Widget');
|
|
38
40
|
const Application = require('../structures/interfaces/Application');
|
|
39
|
-
const { Events, Status } = require('../util/Constants');
|
|
41
|
+
const { Events, Status, VoiceStatus } = require('../util/Constants');
|
|
40
42
|
const DataResolver = require('../util/DataResolver');
|
|
41
43
|
const Intents = require('../util/Intents');
|
|
42
44
|
const DiscordAuthWebsocket = require('../util/RemoteAuth');
|
|
@@ -895,6 +897,71 @@ class Client extends BaseClient {
|
|
|
895
897
|
});
|
|
896
898
|
}
|
|
897
899
|
|
|
900
|
+
/**
|
|
901
|
+
* Joins a voice channel, starts a screenshare stream and plays a video.
|
|
902
|
+
* @param {import('./voice/StreamSession').StartStreamOptions} options Stream options
|
|
903
|
+
* @returns {Promise<import('./voice/StreamSession')>}
|
|
904
|
+
* @example
|
|
905
|
+
* const stream = await client.startStream({
|
|
906
|
+
* guildId: '123',
|
|
907
|
+
* channelId: '456',
|
|
908
|
+
* url: 'video.mp4',
|
|
909
|
+
* fps: 60,
|
|
910
|
+
* height: 720,
|
|
911
|
+
* bitrate: 4500,
|
|
912
|
+
* });
|
|
913
|
+
* stream.pause();
|
|
914
|
+
* stream.resume();
|
|
915
|
+
* stream.stop();
|
|
916
|
+
* stream.replay();
|
|
917
|
+
* stream.disconnect();
|
|
918
|
+
*/
|
|
919
|
+
async startStream(options = {}) {
|
|
920
|
+
const {
|
|
921
|
+
guildId,
|
|
922
|
+
channelId,
|
|
923
|
+
url,
|
|
924
|
+
fps,
|
|
925
|
+
height,
|
|
926
|
+
width,
|
|
927
|
+
bitrate = 5000,
|
|
928
|
+
bitrateMax,
|
|
929
|
+
audioBitrate,
|
|
930
|
+
preset,
|
|
931
|
+
tune,
|
|
932
|
+
audio,
|
|
933
|
+
livestream = false,
|
|
934
|
+
downloadHttp = true,
|
|
935
|
+
} = options;
|
|
936
|
+
|
|
937
|
+
if (!url) throw new Error('STREAM_URL_REQUIRED');
|
|
938
|
+
if (!guildId || !channelId) throw new Error('STREAM_CHANNEL_REQUIRED');
|
|
939
|
+
|
|
940
|
+
let playUrl = url;
|
|
941
|
+
if (typeof url === 'string' && url.startsWith('http') && downloadHttp) {
|
|
942
|
+
playUrl = await WebRtcStreamSession.resolveUrl(url);
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const session = new WebRtcStreamSession(this, {
|
|
946
|
+
guildId,
|
|
947
|
+
channelId,
|
|
948
|
+
url: playUrl,
|
|
949
|
+
fps,
|
|
950
|
+
height,
|
|
951
|
+
width,
|
|
952
|
+
bitrate,
|
|
953
|
+
bitrateMax,
|
|
954
|
+
audioBitrate,
|
|
955
|
+
preset,
|
|
956
|
+
tune,
|
|
957
|
+
audio,
|
|
958
|
+
livestream,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
await session.start();
|
|
962
|
+
return session;
|
|
963
|
+
}
|
|
964
|
+
|
|
898
965
|
/**
|
|
899
966
|
* Calls {@link https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/eval} on a script
|
|
900
967
|
* with the client as `this`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const VoiceConnection = require('./VoiceConnection');
|
|
4
4
|
const { Error } = require('../../errors');
|
|
5
|
-
const { Events } = require('../../util/Constants');
|
|
5
|
+
const { Events, VoiceStatus, Opcodes } = require('../../util/Constants');
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Manages voice connections for the client
|
|
@@ -60,6 +60,7 @@ class ClientVoiceManager {
|
|
|
60
60
|
|
|
61
61
|
onVoiceStateUpdate(payload) {
|
|
62
62
|
const { guild_id, session_id, channel_id } = payload;
|
|
63
|
+
if (payload.user_id !== this.client.user?.id) return;
|
|
63
64
|
// @discordjs/voice
|
|
64
65
|
if (payload.guild_id && payload.session_id && payload.user_id === this.client.user?.id) {
|
|
65
66
|
this.adapters.get(payload.guild_id)?.onVoiceStateUpdate(payload);
|
|
@@ -71,6 +72,9 @@ class ClientVoiceManager {
|
|
|
71
72
|
this.client.emit('debug', `[VOICE] connection? ${!!connection}, ${guild_id} ${session_id} ${channel_id}`);
|
|
72
73
|
if (!connection) return;
|
|
73
74
|
if (!channel_id) {
|
|
75
|
+
if (connection.status === VoiceStatus.AUTHENTICATING || connection.status === VoiceStatus.CONNECTING) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
74
78
|
connection._disconnect();
|
|
75
79
|
this.connection = null;
|
|
76
80
|
return;
|
|
@@ -93,6 +97,73 @@ class ClientVoiceManager {
|
|
|
93
97
|
* @typedef {Object} JoinChannelConfig
|
|
94
98
|
*/
|
|
95
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Clears stale voice/stream state before joining a channel.
|
|
102
|
+
* @param {VoiceChannel} channel The channel to join
|
|
103
|
+
* @returns {Promise<void>}
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
preJoinCleanup(channel) {
|
|
107
|
+
return new Promise(resolve => {
|
|
108
|
+
const guild = channel.guild;
|
|
109
|
+
if (!guild) {
|
|
110
|
+
resolve();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const userId = this.client.user?.id;
|
|
115
|
+
const voiceState = guild.voiceStates.cache.get(userId);
|
|
116
|
+
const inVoice = Boolean(voiceState?.channelId);
|
|
117
|
+
const isStreaming = Boolean(voiceState?.streaming);
|
|
118
|
+
|
|
119
|
+
if (this.connection) {
|
|
120
|
+
this.connection.disconnect();
|
|
121
|
+
this.connection = null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!inVoice && !isStreaming) {
|
|
125
|
+
resolve();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const streamKey = `guild:${guild.id}:${voiceState.channelId}:${userId}`;
|
|
130
|
+
let settled = false;
|
|
131
|
+
const done = () => {
|
|
132
|
+
if (settled) return;
|
|
133
|
+
settled = true;
|
|
134
|
+
this.client.removeListener(Events.VOICE_STATE_UPDATE, onState);
|
|
135
|
+
setTimeout(resolve, 500).unref();
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const onState = (_old, newState) => {
|
|
139
|
+
if (newState.id !== userId || newState.guild?.id !== guild.id) return;
|
|
140
|
+
if (!newState.channelId && !newState.streaming) done();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
this.client.on(Events.VOICE_STATE_UPDATE, onState);
|
|
144
|
+
this.client.emit('debug', `[VOICE] preJoinCleanup: quitte le salon ${voiceState.channelId}`);
|
|
145
|
+
|
|
146
|
+
if (isStreaming) {
|
|
147
|
+
this.client.ws.broadcast({
|
|
148
|
+
op: Opcodes.STREAM_DELETE,
|
|
149
|
+
d: { stream_key: streamKey },
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
this.client.ws.broadcast({
|
|
154
|
+
op: Opcodes.VOICE_STATE_UPDATE,
|
|
155
|
+
d: {
|
|
156
|
+
guild_id: guild.id,
|
|
157
|
+
channel_id: null,
|
|
158
|
+
self_mute: false,
|
|
159
|
+
self_deaf: false,
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
setTimeout(done, 5000).unref();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
96
167
|
/**
|
|
97
168
|
* Sets up a request to join a voice channel.
|
|
98
169
|
* @param {VoiceChannel | StageChannel | DMChannel | GroupDMChannel | Snowflake} channel The voice channel to join
|
|
@@ -106,28 +177,32 @@ class ClientVoiceManager {
|
|
|
106
177
|
throw new Error('VOICE_JOIN_CHANNEL', channel.full);
|
|
107
178
|
}
|
|
108
179
|
|
|
180
|
+
const startJoin = () => {
|
|
109
181
|
let connection = this.connection;
|
|
110
182
|
|
|
111
|
-
if (connection) {
|
|
112
|
-
if (connection.channel.id !== channel.id) {
|
|
113
|
-
this.connection.updateChannel(channel);
|
|
114
|
-
}
|
|
183
|
+
if (connection?.status === VoiceStatus.CONNECTED && connection.channel.id === channel.id) {
|
|
115
184
|
resolve(connection);
|
|
116
185
|
return;
|
|
117
|
-
} else {
|
|
118
|
-
connection = new VoiceConnection(this, channel);
|
|
119
|
-
if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
|
|
120
|
-
connection.on('debug', msg =>
|
|
121
|
-
this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`),
|
|
122
|
-
);
|
|
123
|
-
connection.authenticate({
|
|
124
|
-
self_mute: Boolean(config.selfMute),
|
|
125
|
-
self_deaf: Boolean(config.selfDeaf),
|
|
126
|
-
self_video: Boolean(config.selfVideo),
|
|
127
|
-
});
|
|
128
|
-
this.connection = connection;
|
|
129
186
|
}
|
|
130
187
|
|
|
188
|
+
if (connection) {
|
|
189
|
+
connection.disconnect();
|
|
190
|
+
this.connection = null;
|
|
191
|
+
connection = null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
connection = new VoiceConnection(this, channel);
|
|
195
|
+
if (config?.videoCodec) connection.setVideoCodec(config.videoCodec);
|
|
196
|
+
connection.on('debug', msg =>
|
|
197
|
+
this.client.emit('debug', `[VOICE (${channel.guild?.id || channel.id}:${connection.status})]: ${msg}`),
|
|
198
|
+
);
|
|
199
|
+
connection.authenticate({
|
|
200
|
+
self_mute: Boolean(config.selfMute),
|
|
201
|
+
self_deaf: Boolean(config.selfDeaf),
|
|
202
|
+
self_video: Boolean(config.selfVideo),
|
|
203
|
+
});
|
|
204
|
+
this.connection = connection;
|
|
205
|
+
|
|
131
206
|
connection.once('failed', reason => {
|
|
132
207
|
this.connection = null;
|
|
133
208
|
reject(reason);
|
|
@@ -144,6 +219,9 @@ class ClientVoiceManager {
|
|
|
144
219
|
this.connection = null;
|
|
145
220
|
});
|
|
146
221
|
});
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
this.preJoinCleanup(channel).then(startJoin).catch(reject);
|
|
147
225
|
});
|
|
148
226
|
}
|
|
149
227
|
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const os = require('node:os');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { EventEmitter } = require('events');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Active screenshare session returned by {@link Client#startStream}.
|
|
10
|
+
* @extends {EventEmitter}
|
|
11
|
+
*/
|
|
12
|
+
class StreamSession extends EventEmitter {
|
|
13
|
+
/**
|
|
14
|
+
* Downloads an HTTP(S) video to a temp file (recommended on VPS).
|
|
15
|
+
* @param {string} url Remote video URL
|
|
16
|
+
* @returns {Promise<string>} Local file path
|
|
17
|
+
*/
|
|
18
|
+
static async resolveUrl(url) {
|
|
19
|
+
const response = await fetch(url);
|
|
20
|
+
if (!response.ok) throw new Error(`STREAM_URL_HTTP_${response.status}`);
|
|
21
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
22
|
+
if (buffer.length < 1024) throw new Error('STREAM_URL_INVALID');
|
|
23
|
+
const tmpPath = path.join(os.tmpdir(), `djs-stream-${Date.now()}.mp4`);
|
|
24
|
+
fs.writeFileSync(tmpPath, buffer);
|
|
25
|
+
console.log(`[stream] vidéo téléchargée: ${tmpPath} (${(buffer.length / 1024 / 1024).toFixed(2)} Mo)`);
|
|
26
|
+
return tmpPath;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* @param {import('../Client')} client Discord client
|
|
30
|
+
* @param {import('./VoiceConnection')} voiceConnection Voice connection
|
|
31
|
+
* @param {import('./VoiceConnection').StreamConnection} streamConnection Stream connection
|
|
32
|
+
* @param {StartStreamOptions} options Stream options
|
|
33
|
+
*/
|
|
34
|
+
constructor(client, voiceConnection, streamConnection, options) {
|
|
35
|
+
super();
|
|
36
|
+
this.client = client;
|
|
37
|
+
this.voiceConnection = voiceConnection;
|
|
38
|
+
this.streamConnection = streamConnection;
|
|
39
|
+
this.options = options;
|
|
40
|
+
this.videoDispatcher = null;
|
|
41
|
+
this.audioDispatcher = null;
|
|
42
|
+
this.voiceAudioDispatcher = null;
|
|
43
|
+
this._positionMs = 0;
|
|
44
|
+
this._stopped = false;
|
|
45
|
+
|
|
46
|
+
const onError = err => this.emit('error', err);
|
|
47
|
+
streamConnection.on('error', onError);
|
|
48
|
+
voiceConnection.on('error', onError);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
_getPositionMs() {
|
|
52
|
+
if (!this.videoDispatcher) return this._positionMs;
|
|
53
|
+
return Math.max(0, this.videoDispatcher.totalStreamTime - this.videoDispatcher.pausedTime);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
_play(seek = 0) {
|
|
57
|
+
const { url, fps, height, width, bitrate, audioBitrate } = this.options;
|
|
58
|
+
const streamHeight = height ?? 720;
|
|
59
|
+
this.streamConnection.videoAttributes = {
|
|
60
|
+
width: width ?? Math.round((streamHeight * 16) / 9),
|
|
61
|
+
height: streamHeight,
|
|
62
|
+
fps: fps ?? 30,
|
|
63
|
+
};
|
|
64
|
+
const videoOptions = {
|
|
65
|
+
fps,
|
|
66
|
+
bitrate,
|
|
67
|
+
seek,
|
|
68
|
+
presetH26x: this.options.preset || 'ultrafast',
|
|
69
|
+
outputFFmpegArgs: [
|
|
70
|
+
'-pix_fmt',
|
|
71
|
+
'yuv420p',
|
|
72
|
+
'-g',
|
|
73
|
+
String(fps),
|
|
74
|
+
'-keyint_min',
|
|
75
|
+
String(fps),
|
|
76
|
+
'-sc_threshold',
|
|
77
|
+
'0',
|
|
78
|
+
'-force_key_frames',
|
|
79
|
+
'expr:gte(t,n_forced*1)',
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
if (height) {
|
|
84
|
+
videoOptions.outputFFmpegArgs.unshift('-vf', `scale=-2:${height}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!url.startsWith('http')) {
|
|
88
|
+
videoOptions.inputFFmpegArgs = ['-re'];
|
|
89
|
+
} else {
|
|
90
|
+
videoOptions.inputFFmpegArgs = ['-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1'];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.videoDispatcher = this.streamConnection.playVideo(url, videoOptions);
|
|
94
|
+
|
|
95
|
+
if (this.options.audio !== false) {
|
|
96
|
+
const audioOptions = {
|
|
97
|
+
type: 'unknown',
|
|
98
|
+
seek,
|
|
99
|
+
bitrate: audioBitrate ?? 128,
|
|
100
|
+
};
|
|
101
|
+
if (!url.startsWith('http')) {
|
|
102
|
+
audioOptions.inputFFmpegArgs = ['-re'];
|
|
103
|
+
} else {
|
|
104
|
+
audioOptions.inputFFmpegArgs = ['-reconnect', '1', '-reconnect_at_eof', '1', '-reconnect_streamed', '1'];
|
|
105
|
+
}
|
|
106
|
+
this.voiceAudioDispatcher = this.voiceConnection.playAudio(url, audioOptions);
|
|
107
|
+
this.audioDispatcher = this.streamConnection.playAudio(url, audioOptions);
|
|
108
|
+
this.audioDispatcher.setSyncVideoDispatcher(this.videoDispatcher);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this._stopped = false;
|
|
112
|
+
this.videoDispatcher.once('finish', () => {
|
|
113
|
+
this._positionMs = 0;
|
|
114
|
+
this.emit('finish');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
return this.videoDispatcher;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Pauses video and audio playback.
|
|
122
|
+
*/
|
|
123
|
+
pause() {
|
|
124
|
+
this.videoDispatcher?.pause();
|
|
125
|
+
this.audioDispatcher?.pause(true);
|
|
126
|
+
this.voiceAudioDispatcher?.pause(true);
|
|
127
|
+
this.streamConnection.sendScreenshareState(true);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Resumes video and audio playback.
|
|
132
|
+
*/
|
|
133
|
+
resume() {
|
|
134
|
+
this.streamConnection.sendScreenshareState(false);
|
|
135
|
+
this.videoDispatcher?.resume();
|
|
136
|
+
this.audioDispatcher?.resume();
|
|
137
|
+
this.voiceAudioDispatcher?.resume();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Stops playback while keeping voice/stream connections.
|
|
142
|
+
*/
|
|
143
|
+
stop() {
|
|
144
|
+
this._positionMs = this._getPositionMs();
|
|
145
|
+
this._stopped = true;
|
|
146
|
+
this.videoDispatcher?.destroy();
|
|
147
|
+
this.audioDispatcher?.destroy();
|
|
148
|
+
this.voiceAudioDispatcher?.destroy();
|
|
149
|
+
this.videoDispatcher = null;
|
|
150
|
+
this.audioDispatcher = null;
|
|
151
|
+
this.voiceAudioDispatcher = null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Resumes playback from the last position.
|
|
156
|
+
* @returns {import('./dispatcher/VideoDispatcher')}
|
|
157
|
+
*/
|
|
158
|
+
replay() {
|
|
159
|
+
const seekSec = (this._stopped ? this._positionMs : this._getPositionMs()) / 1000;
|
|
160
|
+
this.stop();
|
|
161
|
+
return this._play(seekSec);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Stops playback and disconnects from voice and stream.
|
|
166
|
+
*/
|
|
167
|
+
disconnect() {
|
|
168
|
+
this.stop();
|
|
169
|
+
this.streamConnection.disconnect();
|
|
170
|
+
this.voiceConnection.disconnect();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = StreamSession;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @typedef {Object} StartStreamOptions
|
|
178
|
+
* @property {import('../../util/Snowflake')} guildId Guild id
|
|
179
|
+
* @property {import('../../util/Snowflake')} channelId Voice channel id
|
|
180
|
+
* @property {string} url Video URL or file path
|
|
181
|
+
* @property {number} [fps=30] Video framerate
|
|
182
|
+
* @property {number} [height] Output height (width auto-scaled)
|
|
183
|
+
* @property {number} [width] Video width sent to Discord (default: 16:9 from height)
|
|
184
|
+
* @property {number} [bitrate=2000] Video bitrate in kbps
|
|
185
|
+
* @property {number} [audioBitrate=128] Audio bitrate in kbps
|
|
186
|
+
* @property {'H264' | 'VP8'} [videoCodec='H264'] Video codec
|
|
187
|
+
* @property {string} [preset='ultrafast'] x264 preset
|
|
188
|
+
* @property {boolean} [audio=true] Whether to play audio
|
|
189
|
+
* @property {boolean} [video=false] Enable webcam (false = screenshare only)
|
|
190
|
+
* @property {boolean} [downloadHttp=true] Download HTTP URLs locally before playback
|
|
191
|
+
* @property {number} [bitrateMax] Max video bitrate in kbps
|
|
192
|
+
* @property {boolean} [livestream=false] Enable readrateInitialBurst (live sources)
|
|
193
|
+
*/
|