discord.js: VoiceConnection stuck in signalling state about a minute after creating it

Which package is this bug report for?

voice

Issue description

I’ve noticed a bug that seems to affect also some other users (see #8482). All the VoiceConnection works as intended the first minute, and then stop playing anything. The player subscribed switch in AutoPaused and the connection stays signalling state indefinitely.

After looking at the code (https://github.com/discordjs/discord.js/blob/main/packages/voice/src/VoiceConnection.ts), it seems that the only way for it to go back to ready state is to receive a message from discord api, that then create events that can put status back in ready state. But, if I understand correctly, this discord message never comes(line 335), so an event is not created through the function configureNetworking(line 338, 420), and the state cannot change to ready(line 484). That’s a messy explanation, and maybe false btw, but I’ve tried my best to understand.

Code sample

let voice_connection = Voice.joinVoiceChannel({
	adapterCreator: guild.voiceAdapterCreator,
	guildId: guild_id,
	channelId: channel_id,
	selfDeaf: true,
	selfMute: false
});

let player = new Voice.AudioPlayer({noSubscriber: Voice.NoSubscriberBehavior.Pause});
voice_connection.subscribe(player);

let resource = Voice.createAudioResource(local_file_or_stream);
player.play(resource);

Package version

14.7.0

Node.js version

18.7.0; 18.14.2; 19.7.0 and maybe others

Operating system

Windows, WSL, Ubuntu, Debian…

Priority this issue should have

Medium (should be fixed soon)

Which partials do you have configured?

Not applicable (subpackage bug)

Which gateway intents are you subscribing to?

Guilds, GuildVoiceStates

I have tested this issue on a development release

No response

About this issue

  • Original URL
  • State: closed
  • Created a year ago
  • Reactions: 25
  • Comments: 63 (5 by maintainers)

Commits related to this issue

Most upvoted comments

A workaround is to remove keepAlive timer/interval.

You can do this without patching @discordjs/voice in node_modules.

Simply put the following code for a voiceConnection object.

const networkStateChangeHandler = (oldNetworkState: any, newNetworkState: any) => {
  const newUdp = Reflect.get(newNetworkState, 'udp');
  clearInterval(newUdp?.keepAliveInterval);
}

voiceConnection.on('stateChange', (oldState, newState) => {
  const oldNetworking = Reflect.get(oldState, 'networking');
  const newNetworking = Reflect.get(newState, 'networking');

  oldNetworking?.off('stateChange', networkStateChangeHandler);
  newNetworking?.on('stateChange', networkStateChangeHandler);
});

This is TypeScript, for JavaScript simply remove all type annotations.

Thanks to @ramsydx for the updated version.

I wasn’t able to switch the region on the server so I made this as a temporary workaround:

this.connection.on('stateChange', (old_state, new_state) => {
            this.logger.logMessage('join', 'Connection state change from', old_state.status, 'to', new_state.status)
            if (old_state.status === VoiceConnectionStatus.Ready && new_state.status === VoiceConnectionStatus.Connecting) {
                connection.configureNetworking();
            }
        })

The little pause is shorter then on this solution. Maybe this helps anyopne looking for a temporary solution.

Indeed it has been fixed! Slap that npm i @discordjs/voice@latest in your terminal and (assuming npm cache doesn’t troll you and installs 0.14.0 still) it’ll be all gud!

voiceConnection.on('stateChange', (oldState, newState) => {
  const oldNetworking = Reflect.get(oldState, 'networking');
  const newNetworking = Reflect.get(newState, 'networking');

  const networkStateChangeHandler = (oldNetworkState: any, newNetworkState: any) => {
    const newUdp = Reflect.get(newNetworkState, 'udp');
    clearInterval(newUdp?.keepAliveInterval);
  }

  oldNetworking?.off('stateChange', networkStateChangeHandler);
  newNetworking?.on('stateChange', networkStateChangeHandler);
});

This is TypeScript, for JavaScript simply remove all type annotations.

This way of writing would result in an infinite number of listeners being added. oldNetworking?.off() was not working. This is because it is not the function registered in the previous ‘on’.

Here is my proposal.

const networkStateChangeHandler = (oldNetworkState: any, newNetworkState: any) => {
  const newUdp = Reflect.get(newNetworkState, 'udp');
  clearInterval(newUdp?.keepAliveInterval);
}
voiceConnection.on('stateChange', (oldState, newState) => {
  Reflect.get(oldState, 'networking')?.off('stateChange', networkStateChangeHandler);
  Reflect.get(newState, 'networking')?.on('stateChange', networkStateChangeHandler);
});

After investigation I am sure that this is a bug in @discordjs/voice that affecting some voice servers.

The VoiceUDPSocket object implemented some undocumented keepAlive functionality which sends a keepAlive counter packed in a 8-bytes data every 5 seconds interval, and it expects a reply from voice server.

But somehow some of the voice servers out there did not response back with the keepAlive counter, resulting in the keepAlives array to grow and eventually reach the 12 length limit, which took exactly 60 seconds.

https://github.com/discordjs/discord.js/blob/dc142c47e4021d6d37712453cc2fad6e4fe4e9f7/packages/voice/src/networking/VoiceUDPSocket.ts#L39-L47

This code sets up the keepAlive functionality. https://github.com/discordjs/discord.js/blob/dc142c47e4021d6d37712453cc2fad6e4fe4e9f7/packages/voice/src/networking/VoiceUDPSocket.ts#L117-L120

This method expects a keepAlive counter data response from the voice server. https://github.com/discordjs/discord.js/blob/dc142c47e4021d6d37712453cc2fad6e4fe4e9f7/packages/voice/src/networking/VoiceUDPSocket.ts#L130-L143

Here are some debugging logs from VoiceUDPSocket connected to singapore63 voice server which affected by this issue.

1677785882271 [VoiceUDPSocket onMessage] 52
1677785883272 [VoiceUDPSocket onMessage] 52
1677785884271 [VoiceUDPSocket onMessage] 52
1677785885271 [VoiceUDPSocket onMessage] 52
1677785886271 [VoiceUDPSocket onMessage] 52
1677785886600 [VoiceUDPSocket keepAlive] this.keepAlives.length >= KEEP_ALIVE_LIMIT 11 12
1677785887271 [VoiceUDPSocket onMessage] 52
1677785888271 [VoiceUDPSocket onMessage] 52
1677785889271 [VoiceUDPSocket onMessage] 52
1677785890271 [VoiceUDPSocket onMessage] 52
1677785891271 [VoiceUDPSocket onMessage] 52
1677785891601 [VoiceUDPSocket keepAlive] this.keepAlives.length >= KEEP_ALIVE_LIMIT 12 12
--------------- VoiceUDPSocket destroy called Error
    at EventEmitter.destroy
    at EventEmitter.keepAlive
    at Timeout._onTimeout
    at listOnTimeout (node:internal/timers:564:17)
    at process.processTimers (node:internal/timers:507:7)
UDP SOCKET CLOSED

As you can see, there is no 8-bytes response and the VoiceUDPSocket.destroy() method was called by the keepAlive method.

I managed to find an unaffected voice server which is singapore11050, and here are the logs

1677786081734 [VoiceUDPSocket keepAlive] this.keepAlives.length >= KEEP_ALIVE_LIMIT 1 12
1677786081766 [VoiceUDPSocket onMessage] 8
1677786082613 [VoiceUDPSocket onMessage] 52
1677786083613 [VoiceUDPSocket onMessage] 52
1677786084612 [VoiceUDPSocket onMessage] 52
1677786085613 [VoiceUDPSocket onMessage] 52
1677786086612 [VoiceUDPSocket onMessage] 52
1677786086736 [VoiceUDPSocket keepAlive] this.keepAlives.length >= KEEP_ALIVE_LIMIT 1 12
1677786086767 [VoiceUDPSocket onMessage] 8
1677786087612 [VoiceUDPSocket onMessage] 52
1677786088612 [VoiceUDPSocket onMessage] 52
1677786089613 [VoiceUDPSocket onMessage] 52
1677786090633 [VoiceUDPSocket onMessage] 52
1677786091633 [VoiceUDPSocket onMessage] 52
1677786091735 [VoiceUDPSocket keepAlive] this.keepAlives.length >= KEEP_ALIVE_LIMIT 1 12

The voice server did response back with expected data so keepAlives array will never grow and I didn’t experience a disconnection from this server.

I’ve also found a workaround to this problem :

Add this.configureNetworking(); on the line 355 of this file : https://github.com/discordjs/discord.js/blob/main/packages/voice/src/VoiceConnection.ts Or Add this.configureNetworking(); on the line 1361 of the file located at node_modules/@discordjs/voice/dist/index.js

A workaround is to remove keepAlive timer/interval. You can do this without patching @discordjs/voice in node_modules. Simply put the following code for a voiceConnection object

voiceConnection.on('stateChange', (oldState, newState) => {
  const oldNetworking = Reflect.get(oldState, 'networking');
  const newNetworking = Reflect.get(newState, 'networking');

  const networkStateChangeHandler = (oldNetworkState: any, newNetworkState: any) => {
    const newUdp = Reflect.get(newNetworkState, 'udp');
    clearInterval(newUdp?.keepAliveInterval);
  }

  oldNetworking?.off('stateChange', networkStateChangeHandler);
  newNetworking?.on('stateChange', networkStateChangeHandler);
});

This is TypeScript, for JavaScript simply remove all type annotations.

I’m a bit confused as to where to put this? Tried following this and a few threads about this same issue and got a bit lost.

Add it after you create or get the voiceConnection. Ex:

const connection = joinVoiceChannel({ channelId: channel.id, guildId: guild.id, adapterCreator: guild.voiceAdapterCreator });
connection.on('stateChange', async (oldState, newState) => {
   ...
});

The discord voice documentation is a solid 3 versions behind. This is a non-exhaustive list of missing features:

I believe discord may be lazy in documenting their voice API as they do not see the need to.

Oof. Could you open an issue on their documentation repo and ask for inputs on what we should be doing? It’s really not ideal to have to just reverse engineer their clients just so bots can provide voice support ☹️

The discord voice documentation is a solid 3 versions behind. This is a non-exhaustive list of missing features:

I believe discord may be lazy in documenting their voice API as they do not see the need to.

I believe this was a Discord issue that should be resolved now. Can you guys check (on a new connection, if possible (restart your bot)) if you can still reproduce this?

Restarted my bot several times. Still same issue.

I have the same problem. It started to happen today without me changing anything in the code. So probably discord changing something on their side of the connection 🤔. @Theiremi your solution works for me but now i have a short pause every minute while playling.

@davidzeng0 I’m not quite sure if this is a good idea, Discord uses that magic number for some reasons.

I think it’s okay since it translates to “leet cafe” which makes me inclined to think Discord just wanted a magic number to differentiate heartbeats. If they changed it, it would brick all of their clients as well. I have checked a few months ago to find the same magic constant in their older module before they updated their voice servers. And either way, I believe it’s still best to patch the issue for now and work on a more “permanent” fix later.

Discord recently did a new update that is causing this issue Message Link: https://discord.com/channels/613425648685547541/697138785317814292/1080623873629884486 We recently pushed out a change related to Voice Connections that broke apps sending 70-byte UDP packets to the voice server when using IP Discovery. While the documentation and deprecation were updated in December 2019, there wasn’t communication around the the change starting to roll out so we’ve temporarily reverted the change to give developers more time to handle the breaking change.

I am not sure how to folow that link. For me it says “No text channels” is there a discord server I have to join?

This is a message from Discord Developers Server. .gg/discord-developers

Imagine you have no idea about how to write code? Where would you put this so that things work again in Discord?

Well if you don’t know how to write code, you propably don’t use this discord.js. But as there is at least one person that doesn’t know where to put this, you are probably right.

not sure where to put that fix but the other one referenced was easy to put in and sorta fixes it

Put this after you created your connection. For me it looks something like this:

const connection = joinVoiceChannel({
            channelId: channel_id,
            guildId: guild_id,
            adapterCreator: this.voice_channel.guild.voiceAdapterCreator,
            selfDeaf: true,
            selfMute: false
        });
connection.on('stateChange', (old_state, new_state) => {
            if (old_state.status === VoiceConnectionStatus.Ready && new_state.status === VoiceConnectionStatus.Connecting) {
                connection.configureNetworking();
            }
        })