Description
Hey all! Same person as disgoorg/disgo#595 and disgoorg/disgo#604, back with another voice one. This time I think the cause is in golibdave, but I'd really value your read on whether I've understood the intent.
Full disclosure: like disgoorg/disgo#595, I investigated this with the help of an AI agent. I've checked what follows myself, live and in isolation.
What I was doing
I've been adding Stage channel support to a bot built on disgo and golibdave. Joining a stage works fine: the bot lands in the audience as you'd expect. But it never receives a single frame. The same build, in an ordinary voice channel a few minutes later, hears everything.
What I found
On a stage, the voice gateway's select-protocol ack comes back with dave_protocol_version: 0, and none of the DAVE opcodes follow. I'm guessing that's because stages aren't end-to-end encrypted: Discord's changelog lists E2EE for DMs, group DMs, voice channels and Go Live, but not stages.
With version 0, golibdave puts the encryptor into passthrough without a key ratchet, which looks right to me. But Ready() is
return !s.encryptor.IsPassthroughMode() && s.encryptor.HasKeyRatchet()so it stays false for the whole connection. Since disgoorg/disgo#561, disgo's AudioSender and AudioReceiver both wait for Ready(), so nothing gets sent or read.
The part that made me think this isn't intended: Encrypt on that same session happily passes frames straight through. So the session already behaves as "no encryption here" and just doesn't say it's ready. The Session.Ready doc also says sessions that never establish E2EE "always return true". And on #8 you made the point that the stream shouldn't stop advancing when there's no E2EE group, which feels like exactly this situation.
Am I reading it right?
If there's a reason a disabled protocol should keep Ready() false, I'd love to understand it. If not, I have a small fix with tests ready on a fork. I'm happy to open it as a PR, or to rework it if you'd rather this was handled somewhere else, say in disgo or in how protocolInit treats version 0.
For what it's worth, I've worked around it on my side by reporting a disabled protocol as ready in a wrapper, and with that the stage delivers audio normally (around 625 frames in 20s from one speaker, where before it was 0).
Reproduction without Discord (golibdave v0.3.0)
s := golibdave.NewSession(logger, godave.UserID("1"), callbacks)
s.OnSelectProtocolAck(0)
frame := []byte{0xF8, 0xFF, 0xFE}
out := make([]byte, s.MaxEncryptedFrameSize(len(frame)))
n, err := s.Encrypt(1234, frame, out)
fmt.Println(s.Ready()) // false
fmt.Println(err == nil && bytes.Equal(out[:n], frame)) // true, passthrough worksWith version 1 and no epoch yet, Ready() is false too, which is correct, so the difference is specifically the disabled case.
What the voice gateway sends on a stage
From disgo's debug log, decoded:
op 11 clients connect: [two users]
op 4 select protocol ack, dave_protocol_version = 0
op 5 speaking: <user>, ssrc ... (someone is talking)
no DAVE ops (21-31) follow, and AudioReceiver delivers nothing
The path through the code
OnSelectProtocolAck(0) → protocolInit(0) → the init transition runs with version 0 → setupKeyRatchetForUser(self, 0) sets passthrough and no key ratchet → Ready() is false for ever.
A downgrade to 0 by a later transition ends up in the same state once it executes.
Versions: godave and golibdave v0.3.0 (also checked on master at f34f982), libdave v0.3.0, disgo master at 68b7dbe6.
Activity