fix(gateway): re-assert server-undeafen+unmute before every video watch
The bot's own VOICE_STATE_UPDATE showed server-level deaf:true — a server-deafened member is NOT sent the streamer's audiovisual RTP by Discord, which is the likely reason no H264 arrives despite the DAVE watch reaching Ready. The previous fire-and-forget forceSelfServerUnmuteUndeafen() ran once after the first Ready join and silently reverted on reconnect/restart. - Export forceSelfServerUnmuteUndeafen from recorder.ts; re-assert it (with read-back verification logging stillDeaf) at the START of every startStreamWatch() before STREAM_WATCH is sent (dynamic import avoids the recorder <-> videoRecorder <-> streamWatchReceiver module cycle). - Re-assert it again after a successful voice reconnect. - startStreamWatch() is now async; callers use void.
This commit is contained in:
@@ -45,8 +45,14 @@ export function setEventBroadcaster(broadcaster: EventBroadcaster | undefined) {
|
||||
* Requires the `MUTE_MEMBERS` + `DEAFEN_MEMBERS` permissions on the bot
|
||||
* (user has granted them). Failures are logged, never fatal — an unmute
|
||||
* failure must not break the voice join/recording.
|
||||
*
|
||||
* Exported so the video-receive path (videoRecorder.ts → streamWatch) can
|
||||
* re-assert it right before requesting STREAM_WATCH — a server-deafened bot
|
||||
* is NOT sent the streamer's audiovisual RTP, which is why no video arrives
|
||||
* even when the DAVE watch reaches Ready. Server-deaf can silently revert on
|
||||
* reconnect/restart, so this is re-invoked on every video watch attempt.
|
||||
*/
|
||||
async function forceSelfServerUnmuteUndeafen(
|
||||
export async function forceSelfServerUnmuteUndeafen(
|
||||
client: Client,
|
||||
guild: Guild,
|
||||
): Promise<void> {
|
||||
@@ -54,7 +60,15 @@ async function forceSelfServerUnmuteUndeafen(
|
||||
const selfId = client.user?.id;
|
||||
if (!selfId) return;
|
||||
await guild.members.edit(selfId, { mute: false, deaf: false });
|
||||
logger.info({ guildId: guild.id }, "Forced bot server unmute + undeafen");
|
||||
// Verify the PATCH actually landed — read back the live member voice state.
|
||||
const fresh = guild.members.cache.get(selfId);
|
||||
const stillDeaf = fresh?.voice?.deaf ?? false;
|
||||
logger.info(
|
||||
{ guildId: guild.id, stillDeaf },
|
||||
stillDeaf
|
||||
? "Forced bot server unmute + undeafen (but guild reports still deaf — will retry on next video)"
|
||||
: "Forced bot server unmute + undeafen",
|
||||
);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{
|
||||
@@ -225,6 +239,9 @@ export async function startRecording(
|
||||
),
|
||||
]);
|
||||
// Reconnected successfully
|
||||
// Server-deaf can silently revert across a reconnect — re-assert the bot
|
||||
// is undeafened + unmuted so video receive keeps working.
|
||||
void forceSelfServerUnmuteUndeafen(client, channel.guild);
|
||||
} catch {
|
||||
logger.error("Could not reconnect. Destroying connection");
|
||||
connection.destroy();
|
||||
|
||||
@@ -159,13 +159,24 @@ function handleRaw(packet: unknown): void {
|
||||
* waits for STREAM_CREATE / STREAM_SERVER_UPDATE (handled by handleRaw) to
|
||||
* actually open the DAVE connection. Best-effort.
|
||||
*/
|
||||
export function startStreamWatch(channel: VoiceChannel, userId: string): void {
|
||||
export async function startStreamWatch(
|
||||
channel: VoiceChannel,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
if (!_client) {
|
||||
logger.warn("startStreamWatch: no client set");
|
||||
return;
|
||||
}
|
||||
const watchKey = `${channel.guild.id}:${userId}`;
|
||||
if (watches.has(watchKey)) return; // already watching
|
||||
// Re-assert the bot is server-undeafened + unmuted BEFORE requesting video.
|
||||
// A server-deafened bot is NOT sent the streamer's audiovisual RTP by
|
||||
// Discord, so no video would arrive even though the DAVE watch reaches
|
||||
// Ready. Server-deaf can silently revert on reconnect/restart, so this is
|
||||
// re-invoked on every watch attempt. Dynamic import avoids a static module
|
||||
// cycle (recorder ⇄ videoRecorder ⇄ streamWatchReceiver).
|
||||
const { forceSelfServerUnmuteUndeafen } = await import("./recorder.js");
|
||||
void forceSelfServerUnmuteUndeafen(_client, channel.guild);
|
||||
const sk = streamKeyFor(channel.id, channel.guild.id, userId);
|
||||
logger.info(
|
||||
{ userId, guildId: channel.guild.id, channelId: channel.id, streamKey: sk },
|
||||
|
||||
@@ -155,7 +155,7 @@ export function startVideoRecording(
|
||||
try {
|
||||
const selfId = _client?.user?.id;
|
||||
if (selfId && userId === selfId) return;
|
||||
startStreamWatch(channel, userId);
|
||||
void startStreamWatch(channel, userId);
|
||||
} catch (err) {
|
||||
logger.warn(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user