djs-selfbot-v13 3.7.36 → 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/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,165 @@ class WebRtcStreamSession extends EventEmitter {
|
|
|
51
83
|
this.options = options;
|
|
52
84
|
this.streamer = new Streamer(client);
|
|
53
85
|
this._abort = new AbortController();
|
|
86
|
+
this._ffmpegCommand = null;
|
|
54
87
|
this._playPromise = null;
|
|
55
|
-
this._command = null;
|
|
56
|
-
this._ffmpegProc = null;
|
|
57
88
|
this._started = false;
|
|
58
89
|
this._running = false;
|
|
59
90
|
this._paused = false;
|
|
60
91
|
this._stopped = false;
|
|
61
92
|
this._positionMs = 0;
|
|
62
93
|
this._runStartWall = 0;
|
|
63
|
-
this._pausedAt = 0;
|
|
64
|
-
this._pausedTotalMs = 0;
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
_getPositionMs() {
|
|
68
97
|
if (!this._running) return this._positionMs;
|
|
69
|
-
|
|
70
|
-
return this._positionMs + (Date.now() - this._runStartWall - this._pausedTotalMs);
|
|
98
|
+
return this._positionMs + (Date.now() - this._runStartWall);
|
|
71
99
|
}
|
|
72
100
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
}
|
|
101
|
+
async _pickEncoder() {
|
|
102
|
+
const { preset = 'ultrafast', encoder = 'auto' } = this.options;
|
|
103
|
+
let mode = encoder;
|
|
104
|
+
if (encoder === 'auto') mode = await detectHwEncoder();
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
106
|
+
if (mode === 'nvenc') {
|
|
107
|
+
console.log('[stream] encodeur: h264_nvenc');
|
|
108
|
+
return Encoders.nvenc({ preset: 'p1' });
|
|
109
|
+
}
|
|
110
|
+
if (mode === 'amf') {
|
|
111
|
+
console.log('[stream] encodeur: h264_amf');
|
|
112
|
+
return () => ({
|
|
113
|
+
H264: {
|
|
114
|
+
name: 'h264_amf',
|
|
115
|
+
options: ['-usage', 'transcoding', '-quality', 'speed'],
|
|
116
|
+
},
|
|
117
|
+
H265: {
|
|
118
|
+
name: 'hevc_amf',
|
|
119
|
+
options: ['-usage', 'transcoding', '-quality', 'speed'],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
86
122
|
}
|
|
123
|
+
console.log('[stream] encodeur: libx264');
|
|
124
|
+
return Encoders.software({
|
|
125
|
+
x264: { preset, tune: 'zerolatency' },
|
|
126
|
+
});
|
|
87
127
|
}
|
|
88
128
|
|
|
89
|
-
|
|
90
|
-
* Construit la commande ffmpeg : transcode en H264 + Opus, sortie nut.
|
|
91
|
-
* Pas de filtre azmq (compatibilité ffmpeg sans libzmq).
|
|
92
|
-
*/
|
|
93
|
-
_buildFfmpeg(url, seekSec = 0) {
|
|
129
|
+
async _buildPrepareOptions(seekSec = 0) {
|
|
94
130
|
const {
|
|
95
|
-
fps,
|
|
96
|
-
height
|
|
97
|
-
width
|
|
98
|
-
bitrate =
|
|
131
|
+
fps = 60,
|
|
132
|
+
height,
|
|
133
|
+
width,
|
|
134
|
+
bitrate = 4500,
|
|
99
135
|
bitrateMax,
|
|
100
136
|
audioBitrate = 128,
|
|
101
|
-
|
|
102
|
-
tune = 'film',
|
|
137
|
+
hardwareAcceleratedDecoding = true,
|
|
103
138
|
audio,
|
|
139
|
+
noTranscoding = false,
|
|
104
140
|
} = this.options;
|
|
105
141
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
'-reconnect_at_eof 1',
|
|
118
|
-
'-reconnect_streamed 1',
|
|
119
|
-
'-reconnect_delay_max 4294',
|
|
120
|
-
]);
|
|
142
|
+
if (noTranscoding) {
|
|
143
|
+
console.log('[stream] mode copie vidéo (pas de ré-encodage)');
|
|
144
|
+
return {
|
|
145
|
+
noTranscoding: true,
|
|
146
|
+
bitrateAudio: audioBitrate,
|
|
147
|
+
includeAudio: audio !== false,
|
|
148
|
+
hardwareAcceleratedDecoding: false,
|
|
149
|
+
minimizeLatency: true,
|
|
150
|
+
customInputOptions: seekSec > 0 ? [`-ss ${String(seekSec)}`] : [],
|
|
151
|
+
customFfmpegFlags: [],
|
|
152
|
+
};
|
|
121
153
|
}
|
|
122
154
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
command.audioFilters('volume=1');
|
|
155
|
+
const gop = String(fps);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
encoder: await this._pickEncoder(),
|
|
159
|
+
height,
|
|
160
|
+
width,
|
|
161
|
+
frameRate: fps,
|
|
162
|
+
bitrateVideo: bitrate,
|
|
163
|
+
bitrateVideoMax: bitrateMax ?? Math.round(bitrate * 1.5),
|
|
164
|
+
bitrateAudio: audioBitrate,
|
|
165
|
+
includeAudio: audio !== false,
|
|
166
|
+
hardwareAcceleratedDecoding,
|
|
167
|
+
minimizeLatency: true,
|
|
168
|
+
customInputOptions: seekSec > 0 ? [`-ss ${String(seekSec)}`] : [],
|
|
169
|
+
customFfmpegFlags: [
|
|
170
|
+
'-g', gop,
|
|
171
|
+
'-keyint_min', gop,
|
|
172
|
+
'-sc_threshold', '0',
|
|
173
|
+
'-threads', '0',
|
|
174
|
+
],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async _disconnectLegacyVoice() {
|
|
179
|
+
const conn = this.client.voice?.connection;
|
|
180
|
+
if (conn) {
|
|
181
|
+
conn.disconnect();
|
|
182
|
+
this.client.voice.connection = null;
|
|
183
|
+
await sleep(800);
|
|
153
184
|
}
|
|
185
|
+
}
|
|
154
186
|
|
|
155
|
-
|
|
187
|
+
async _joinVoice() {
|
|
188
|
+
const { guildId, channelId } = this.options;
|
|
189
|
+
|
|
190
|
+
if (this.streamer.voiceConnection) {
|
|
191
|
+
this.streamer.leaveVoice();
|
|
192
|
+
await sleep(1000);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await this._disconnectLegacyVoice();
|
|
196
|
+
|
|
197
|
+
const vc = this.streamer.voiceConnection;
|
|
198
|
+
if (
|
|
199
|
+
!vc ||
|
|
200
|
+
vc.guildId !== guildId ||
|
|
201
|
+
vc.channelId !== channelId
|
|
202
|
+
) {
|
|
203
|
+
await this.streamer.joinVoice(guildId, channelId);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (this.client.user?.voice?.channel instanceof StageChannel) {
|
|
207
|
+
await this.client.user.voice.setSuppressed(false);
|
|
208
|
+
}
|
|
156
209
|
}
|
|
157
210
|
|
|
158
211
|
async _run(url, signal, seekSec = 0) {
|
|
159
|
-
await
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
this._command = command;
|
|
163
|
-
this._ffmpegProc = null;
|
|
164
|
-
this._running = true;
|
|
165
|
-
this._paused = false;
|
|
166
|
-
this._stopped = false;
|
|
167
|
-
this._resetRunClock();
|
|
212
|
+
const prepareOpts = await this._buildPrepareOptions(seekSec);
|
|
213
|
+
const { command, output } = prepareStream(url, prepareOpts, signal);
|
|
214
|
+
this._ffmpegCommand = command;
|
|
168
215
|
|
|
169
|
-
command.on('start', cmd => {
|
|
170
|
-
this._ffmpegProc = command.ffmpegProc ?? null;
|
|
171
|
-
this.emit('debug', `[cmd] ${cmd}`);
|
|
172
|
-
});
|
|
216
|
+
command.on('start', cmd => this.emit('debug', `[ffmpeg] ${cmd}`));
|
|
173
217
|
command.on('stderr', line => this.emit('debug', String(line)));
|
|
174
|
-
|
|
175
218
|
command.on('error', (err, _stdout, stderr) => {
|
|
176
|
-
if (stderr) this.emit('debug', String(stderr));
|
|
177
219
|
if (signal.aborted) return;
|
|
178
|
-
|
|
179
|
-
this.emit('error', new Error(stderr ? `${msg}\n${stderr}` : msg));
|
|
220
|
+
this.emit('error', new Error(stderr ? `${err.message}\n${stderr}` : err.message));
|
|
180
221
|
});
|
|
181
222
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
};
|
|
187
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
188
|
-
command.run();
|
|
223
|
+
this._running = true;
|
|
224
|
+
this._paused = false;
|
|
225
|
+
this._stopped = false;
|
|
226
|
+
this._runStartWall = Date.now();
|
|
189
227
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
228
|
+
let playOptions;
|
|
229
|
+
if (this.options.noTranscoding) {
|
|
230
|
+
console.log('[stream] envoi à la résolution/fps natifs de la source (WebRTC)');
|
|
231
|
+
playOptions = {};
|
|
232
|
+
} else {
|
|
233
|
+
const { height = 720, width = 1280, fps = 60 } = this.options;
|
|
234
|
+
console.log(`[stream] envoi ${width}x${height} @ ${fps}fps (WebRTC)`);
|
|
235
|
+
playOptions = { width, height, frameRate: fps };
|
|
236
|
+
}
|
|
237
|
+
this.emit('playing');
|
|
193
238
|
|
|
194
239
|
this._playPromise = playStream(output, this.streamer, playOptions, signal);
|
|
195
240
|
|
|
196
241
|
this._playPromise
|
|
197
242
|
.then(() => {
|
|
198
243
|
this._running = false;
|
|
199
|
-
this.
|
|
244
|
+
this._ffmpegCommand = null;
|
|
200
245
|
if (!signal.aborted) {
|
|
201
246
|
this._positionMs = 0;
|
|
202
247
|
this.emit('finish');
|
|
@@ -204,7 +249,7 @@ class WebRtcStreamSession extends EventEmitter {
|
|
|
204
249
|
})
|
|
205
250
|
.catch(err => {
|
|
206
251
|
this._running = false;
|
|
207
|
-
this.
|
|
252
|
+
this._ffmpegCommand = null;
|
|
208
253
|
if (signal.aborted || err?.name === 'AbortError') return;
|
|
209
254
|
this.emit('error', err);
|
|
210
255
|
});
|
|
@@ -213,66 +258,62 @@ class WebRtcStreamSession extends EventEmitter {
|
|
|
213
258
|
}
|
|
214
259
|
|
|
215
260
|
async start() {
|
|
216
|
-
const {
|
|
217
|
-
await
|
|
218
|
-
|
|
261
|
+
const { url } = this.options;
|
|
262
|
+
await this._joinVoice();
|
|
263
|
+
this._started = true;
|
|
219
264
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
265
|
+
const playing = new Promise((resolve, reject) => {
|
|
266
|
+
this.once('playing', resolve);
|
|
267
|
+
this.once('error', reject);
|
|
268
|
+
});
|
|
223
269
|
|
|
224
|
-
this.
|
|
225
|
-
|
|
270
|
+
void this._run(url, this._abort.signal).catch(err => this.emit('error', err));
|
|
271
|
+
await playing;
|
|
226
272
|
}
|
|
227
273
|
|
|
228
|
-
/**
|
|
229
|
-
* Met en pause la lecture (ffmpeg suspendu, connexion vocale conservée).
|
|
230
|
-
*/
|
|
231
274
|
pause() {
|
|
232
275
|
if (!this._running || this._paused || this._stopped) return;
|
|
233
276
|
this._positionMs = this._getPositionMs();
|
|
234
|
-
this._pausedAt = Date.now();
|
|
235
|
-
this._signalFfmpeg('SIGSTOP');
|
|
236
277
|
this._paused = true;
|
|
278
|
+
this._running = false;
|
|
279
|
+
this._ffmpegCommand?.kill?.('SIGTERM');
|
|
280
|
+
this._ffmpegCommand = null;
|
|
281
|
+
this.streamer.stopStream();
|
|
282
|
+
this._abort.abort();
|
|
283
|
+
this._abort = new AbortController();
|
|
284
|
+
console.log(`[stream] pause à ${(this._positionMs / 1000).toFixed(1)}s`);
|
|
237
285
|
}
|
|
238
286
|
|
|
239
|
-
/**
|
|
240
|
-
* Reprend la lecture (après pause ou stop).
|
|
241
|
-
*/
|
|
242
287
|
async resume() {
|
|
243
288
|
if (this._stopped) {
|
|
244
|
-
const seekSec = this._positionMs / 1000;
|
|
245
289
|
this._stopped = false;
|
|
246
|
-
await this._run(this.options.url, this._abort.signal,
|
|
290
|
+
await this._run(this.options.url, this._abort.signal, this._positionMs / 1000);
|
|
247
291
|
return;
|
|
248
292
|
}
|
|
249
|
-
if (!this.
|
|
250
|
-
this._pausedTotalMs += Date.now() - this._pausedAt;
|
|
251
|
-
this._pausedAt = 0;
|
|
252
|
-
this._signalFfmpeg('SIGCONT');
|
|
293
|
+
if (!this._paused) return;
|
|
253
294
|
this._paused = false;
|
|
295
|
+
const seekSec = this._positionMs / 1000;
|
|
296
|
+
console.log(`[stream] reprise à ${seekSec.toFixed(1)}s`);
|
|
297
|
+
await this._run(this.options.url, this._abort.signal, seekSec);
|
|
254
298
|
}
|
|
255
299
|
|
|
256
|
-
/**
|
|
257
|
-
* Arrête la lecture en conservant la position et la connexion vocale.
|
|
258
|
-
*/
|
|
259
300
|
stop() {
|
|
260
|
-
if (!this._running) return;
|
|
301
|
+
if (!this._running && !this._paused) return;
|
|
261
302
|
this._positionMs = this._getPositionMs();
|
|
262
303
|
this._stopped = true;
|
|
263
304
|
this._paused = false;
|
|
264
305
|
this._running = false;
|
|
265
|
-
this.
|
|
306
|
+
this._ffmpegCommand?.kill?.('SIGTERM');
|
|
307
|
+
this._ffmpegCommand = null;
|
|
308
|
+
this.streamer.stopStream();
|
|
266
309
|
this._abort.abort();
|
|
267
310
|
this._abort = new AbortController();
|
|
311
|
+
console.log(`[stream] stop à ${(this._positionMs / 1000).toFixed(1)}s`);
|
|
268
312
|
}
|
|
269
313
|
|
|
270
|
-
/**
|
|
271
|
-
* Relance depuis la dernière position enregistrée.
|
|
272
|
-
*/
|
|
273
314
|
async replay() {
|
|
274
315
|
const seekSec = (this._running ? this._getPositionMs() : this._positionMs) / 1000;
|
|
275
|
-
if (this._running) this.stop();
|
|
316
|
+
if (this._running || this._paused) this.stop();
|
|
276
317
|
this._stopped = false;
|
|
277
318
|
await this._run(this.options.url, this._abort.signal, seekSec);
|
|
278
319
|
}
|
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}. */
|