feat(selfbot): broadcast desktop audio + smart subtitles in the browse scenario

Two broadcast-experience improvements:

- Audio: the Go-Live stream was video-only. Capture the desktop sound (the
  default PipeWire/Pulse sink monitor, @DEFAULT_MONITOR@) as a second ffmpeg
  input and mux AAC into the mpegts; the library re-encodes it to Opus for
  Discord. Controlled by STREAM_AUDIO / STREAM_AUDIO_SOURCE (default on). ffmpeg
  inherits XDG_RUNTIME_DIR to reach the pulse socket. Verified: the streamer now
  reports "Found audio stream" and the monitor carries Chrome audio (~-11 dB).
- Subtitles: in the browse scenario, default captions OFF, but auto-enable a
  Korean track when the video offers one (getOption captions tracklist ->
  setOption / unloadModule).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
javis-bot
2026-06-10 15:50:32 +09:00
parent c6a0ca4572
commit 208fbbc851
5 changed files with 44 additions and 0 deletions

View File

@@ -42,6 +42,11 @@ export const config = {
selfbotToken: opt("DISCORD_SELFBOT_TOKEN"),
// Use NVENC hardware encode + hw-accelerated decode for the stream (RTX 5050).
streamHw: opt("STREAM_HW", "1") !== "0",
// Capture desktop audio into the broadcast so the stream has sound. Pulls the
// PipeWire/Pulse monitor of the default sink (what the desktop plays). Set
// STREAM_AUDIO=0 to mute; STREAM_AUDIO_SOURCE overrides the capture source.
streamAudio: opt("STREAM_AUDIO", "1") !== "0",
streamAudioSource: opt("STREAM_AUDIO_SOURCE", "@DEFAULT_MONITOR@"),
// novnc backend
novncUrl: opt("NOVNC_URL", ""),

View File

@@ -143,17 +143,31 @@ export class SelfbotStreamer implements ScreenStreamer {
const captureCodecArgs = hw
? ["-c:v", "h264_nvenc", "-preset", "p4", "-tune", "ll", "-forced-idr", "1"]
: ["-c:v", "libx264", "-preset", "ultrafast", "-tune", "zerolatency"];
// Optionally pull desktop audio (the default sink's PipeWire/Pulse monitor)
// so the broadcast has sound. We add it as a second input and mux AAC into
// the mpegts; the library re-encodes it to Opus for Discord. ffmpeg needs
// XDG_RUNTIME_DIR (inherited) to reach the pulse socket. -map is required
// once there are two inputs.
const audioOn = this.config.streamAudio;
const audioInput = audioOn ? ["-f", "pulse", "-i", this.config.streamAudioSource] : [];
const audioMap = audioOn ? ["-map", "0:v:0", "-map", "1:a:0"] : [];
const audioCodec = audioOn ? ["-c:a", "aac", "-b:a", "160k", "-ar", "48000", "-ac", "2"] : [];
capture = this.capture = spawn("ffmpeg", [
"-loglevel", "error",
"-thread_queue_size", "1024",
"-f", "x11grab",
"-framerate", String(this.config.vncFramerate),
"-video_size", this.config.vncResolution,
"-i", this.config.vncDisplay,
...(audioOn ? ["-thread_queue_size", "1024"] : []),
...audioInput,
...audioMap,
...captureCodecArgs,
"-b:v", `${kbps}k`, "-maxrate", `${maxKbps}k`, "-bufsize", `${kbps}k`,
"-bf", "0",
"-pix_fmt", "yuv420p",
"-g", String(this.config.vncFramerate),
...audioCodec,
"-f", "mpegts", "pipe:1",
]);
capture.stderr?.on("data", (d) => {