5.0 KiB
Spec: Fix Video Capture — Eagerly Establish the Selfbot Voice Connection at Join Time
Status: PLANNED
Date: 2026-08-31
Author: Hermes
Related: docs/specs/2026-08-31_video-receive-phaseC-spec.md (Phase C build, made Option A this fix)
Symptom (from live logs, 2026-08-31 ~12:34)
A user was actively screen-sharing + on camera in the recorded voice channel.
The gateway recorded MANY users' audio (.ogg) fine, but video capture produced
nothing. The only video signal in journalctl -u gmw-discord-gateway was:
[VOICE (guild:2)]: Sending voice state update: {"self_mute":false,...,"flags":2}
[VOICE] received voice state update: {member hunterz ...} # OTHER user, not bot
[VOICE] connection? true, guild session channel
[VOICE (guild:2)]: Setting sessionId <S> (stored as "undefined")
[VOICE (guild:2)]: Authenticated with sessionId <S> # debug print only
[VOICE (guild:2)]: Authenticate failed - VOICE_CONNECTION_TIMEOUT # +15s
video-recorder: userId=..., "Connection not established within 15 seconds."
Root cause (verified against discord.js-selfbot-v13 3.7.1 source)
The gateway records audio via @discordjs/voice (joinVoiceChannel + adapter).
Video receive lives on the SEPARATE selfbot ClientVoiceManager.connection
(a singleton VoiceConnection). videoRecorder.ts currently calls
client.voice.joinChannel(channel) LAZILY — only when a voiceStateUpdate
shows newState.streaming === true.
At that moment the bot is ALREADY connected to the channel via @discordjs/voice.
A selfbot joinChannel then does VoiceConnection.authenticate() →
sendVoiceStateUpdate(), and waits for a fresh VOICE_SERVER_UPDATE
(setTokenAndEndpoint) + VOICE_STATE_UPDATE (setSessionId) to reach
checkAuthenticated() (needs token+endpoint+sessionId). Because the bot is
already in an established voice session, Discord does NOT emit a new
VOICE_SERVER_UPDATE for the lazy selfbot re-join → token/endpoint never set →
15s VOICE_CONNECTION_TIMEOUT.
This is fatal to video: joinStreamConnection(userId) (STREAM_WATCH op 20) and
receiver.createVideoStream(userId, out) (Recorder/ffmpeg) BOTH live on the
parent selfbot VoiceConnection and require it CONNECTED (its own voice
WS+UDP socket feeds PacketHandler.push, authenticated with
authentication.secret_key).
Fix — establish the selfbot connection eagerly, at voice-join time
The selfbot VoiceConnection must exist and be CONNECTED before any streamer
appears. Establish it once, synchronously alongside the @discordjs/voice join in
recorder.startRecording, so it rides the bot's FRESH voice join — when Discord
DOES emit VOICE_SERVER_UPDATE. Then cache it and let videoRecorder reuse it.
Ordering: in startRecording, after the @discordjs/voice joinVoiceChannel
returns (and retries) — fire ensureSelfbotVoice(channel) best-effort:
await client.voice.joinChannel(channel, { selfMute:false, selfDeaf:false, selfVideo:false })(rejects ~VOICE_CONNECTION_TIMEOUT on failure → log + return null; do NOT block audio).- Cache the returned selfbot
VoiceConnectionkeyed by guildId. - Wire teardown: on
recordervoice stop / destroyed →untrackChannel+ destroy the cached selfbot connection (disconnect()).
videoRecorder.startVideoRecording then uses the cached selfbot connection:
- If cached &
status === CONNECTED→ use it. - Else → fall back to a lazy
joinChannel(still best-effort).
The two-connection coexistence risk (must verify live)
@discordjs/voice (audio) and the selfbot VoiceConnection (video) each open
their OWN low-level voice WS+UDP on the same session. The spec's original
open-question flagged this. Mitigations:
- Clear logging:
Selfbot voice connected (guild=...), plus a periodicdjs/voice statuslog so we can confirm audio staysREADYwhile the selfbot connection is up. - If Discord kicks/breaks the audio connection, logs will show
@discordjs/voice
Disconnected/reconnect churn — we detect and pivot.
Files touched
services/discord-gateway/src/modules/voice-recording/videoRecorder.ts: addensureSelfbotVoice(channel)(return cached/connected), use it instartVideoRecording, adddestroyGuildSelfbotVoice(guildId), richer status logging.services/discord-gateway/src/modules/voice-recording/recorder.ts: callensureSelfbotVoice(channel)afterjoinVoiceChannel(best-effort); calldestroyGuildSelfbotVoiceon voice stop/destroy.- Tests:
tests/videoRecorder.test.ts(update to assert eager-connection reuse- status gating).
Verification
pnpm typecheck+pnpm build+ biome clean (discord-gateway).- Tests green.
- Commit + push; CI
Build & Deploy (Nix)green, service restarts. - LIVE (deploy): join a channel with the bot → journal shows
Selfbot voice connected(parent CONNECTED). When a member streams →Sender signal screenshare/Video recorder ready+ a.mkvunder<RECORDINGS_DIR>/<uid>/video-*.mkv; playable via ffmpeg. Confirm audio recording still flows (no djs/voice reconnect churn).