sipeed/picoclaw · warning
voice connection closed during playback
Error message
voice connection closed during playback
What it means
streamOggOpusToDiscord converts a panic into this error with recover: sending an audio frame on vc.OpusSend panics when discordgo closed the channel because the voice websocket dropped mid-playback (disconnect, kick, move, rehome). The recovered error means TTS/audio playback was aborted because the voice connection went away.
Source
Thrown at pkg/channels/discord/voice.go:122
"channel": m.ChannelID,
"error": sendErr,
})
}
}
return true
}
return false
}
func VoiceReceiveActive(vc *discordgo.VoiceConnection) bool {
return vc != nil && vc.OpusRecv != nil
}
func streamOggOpusToDiscord(ctx context.Context, vc *discordgo.VoiceConnection, r io.Reader) (retErr error) {
// Recover from panic if vc.OpusSend is closed mid-send (e.g. on disconnect)
defer func() {
if rec := recover(); rec != nil {
retErr = fmt.Errorf("voice connection closed during playback")
logger.RecoverPanicNoExit(rec)
}
}()
// Wait for the speaking transition to register
vc.Speaking(true)
defer vc.Speaking(false)
return audio.DecodeOggOpus(r, func(frame []byte) error {
select {
case <-ctx.Done():
return ctx.Err()
case vc.OpusSend <- frame:
return nil
}
})
}
View on GitHub (pinned to 49183d7e8d)
Solutions
- Treat as non-fatal: log it and require a fresh !vc join before the next playback
- Guard playback preconditions: check vc != nil && vc.OpusSend != nil before streaming (mirroring VoiceReceiveActive)
- Cancel the playback ctx from a voice-disconnect handler so DecodeOggOpus exits via ctx.Done() instead of panicking
- Rate-limit TTS length so flaps are less likely mid-playback
Example fix
// before
send := func(frame []byte) error {
vc.OpusSend <- frame // panics 'send on closed channel' on disconnect
return nil
}
// after (as implemented): select on ctx + recover
return audio.DecodeOggOpus(r, func(frame []byte) error {
select {
case <-ctx.Done():
return ctx.Err()
case vc.OpusSend <- frame:
return nil
}
}) Defensive patterns
Strategy: validation
Validate before calling
// Guard before playback (mirror of VoiceReceiveActive for the send side)
func voiceSendReady(vc *discordgo.VoiceConnection) bool {
return vc != nil && vc.OpusSend != nil
}
if !voiceSendReady(vc) {
return fmt.Errorf("voice connection not ready; rejoin with !vc join")
} Try / catch
if err := streamOggOpusToDiscord(ctx, vc, r); err != nil {
if strings.Contains(err.Error(), "voice connection closed during playback") {
// recovered panic: connection dropped mid-TTS; clear state and require rejoin
return nil // or notify: "voice disconnected, use !vc join"
}
return err
} Prevention
- Check vc.OpusSend != nil right before streaming and re-check on disconnect events
- Cancel the playback context from a voice-disconnect handler so frames stop cleanly
- Bound TTS clip length so a mid-clip disconnect wastes less work
- Treat this error as expected churn in voice flows, not a crash
When it happens
Trigger: Playing TTS audio while: a user runs !vc leave, the bot is disconnected/moved from the voice channel, Discord rehomes the voice server (OpusSend closed), or the voice websocket/UDP path drops during a long playback.
Common situations: Users leaving voice while the bot is mid-sentence, voice connection flaps during long TTS output, guild voice region changes.
Understand the failure class
- Connection failures: ECONNREFUSED, ECONNRESET, and friends — why connections get refused, reset, or dropped.
Related errors
- failed to send request: %w
- failed to read response: %w
- API error (status %d): %s
- failed to decode response: %w
- invalid TTS response: missing audio data
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/84bd5680e22c9c5d.
Report an issue: GitHub.