{"record":{"id":"84bd5680e22c9c5d","repo":"sipeed/picoclaw","slug":"voice-connection-closed-during-playback","errorCode":null,"errorMessage":"voice connection closed during playback","messagePattern":"voice connection closed during playback","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"pkg/channels/discord/voice.go","lineNumber":122,"sourceCode":"\t\t\t\t\t\"channel\": m.ChannelID,\n\t\t\t\t\t\"error\":   sendErr,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc VoiceReceiveActive(vc *discordgo.VoiceConnection) bool {\n\treturn vc != nil && vc.OpusRecv != nil\n}\n\nfunc streamOggOpusToDiscord(ctx context.Context, vc *discordgo.VoiceConnection, r io.Reader) (retErr error) {\n\t// Recover from panic if vc.OpusSend is closed mid-send (e.g. on disconnect)\n\tdefer func() {\n\t\tif rec := recover(); rec != nil {\n\t\t\tretErr = fmt.Errorf(\"voice connection closed during playback\")\n\t\t\tlogger.RecoverPanicNoExit(rec)\n\t\t}\n\t}()\n\n\t// Wait for the speaking transition to register\n\tvc.Speaking(true)\n\tdefer vc.Speaking(false)\n\n\treturn audio.DecodeOggOpus(r, func(frame []byte) error {\n\t\tselect {\n\t\tcase <-ctx.Done():\n\t\t\treturn ctx.Err()\n\t\tcase vc.OpusSend <- frame:\n\t\t\treturn nil\n\t\t}\n\t})\n}\n","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/discord/voice.go#L104-L140","documentation":"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.","triggerScenarios":"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.","commonSituations":"Users leaving voice while the bot is mid-sentence, voice connection flaps during long TTS output, guild voice region changes.","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"],"exampleFix":"// before\nsend := func(frame []byte) error {\n    vc.OpusSend <- frame // panics 'send on closed channel' on disconnect\n    return nil\n}\n\n// after (as implemented): select on ctx + recover\nreturn audio.DecodeOggOpus(r, func(frame []byte) error {\n    select {\n    case <-ctx.Done():\n        return ctx.Err()\n    case vc.OpusSend <- frame:\n        return nil\n    }\n})","handlingStrategy":"validation","validationCode":"// Guard before playback (mirror of VoiceReceiveActive for the send side)\nfunc voiceSendReady(vc *discordgo.VoiceConnection) bool {\n    return vc != nil && vc.OpusSend != nil\n}\n\nif !voiceSendReady(vc) {\n    return fmt.Errorf(\"voice connection not ready; rejoin with !vc join\")\n}","typeGuard":null,"tryCatchPattern":"if err := streamOggOpusToDiscord(ctx, vc, r); err != nil {\n    if strings.Contains(err.Error(), \"voice connection closed during playback\") {\n        // recovered panic: connection dropped mid-TTS; clear state and require rejoin\n        return nil // or notify: \"voice disconnected, use !vc join\"\n    }\n    return err\n}","preventionTips":["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"],"tags":["discord","voice","tts","panic-recovery","opus"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}