Initial code to support voice reconnecting

Also includes some logging improvements and a small fix to track
speaking state and to send speaking packet if needed before sending
opus packets.
This commit is contained in:
Bruce Marriner 2016-05-26 19:35:17 -05:00
parent 45a6c0d70d
commit 754d64d339
2 changed files with 91 additions and 15 deletions

View file

@ -32,14 +32,16 @@ import (
type VoiceConnection struct {
sync.RWMutex
Debug bool // If true, print extra logging -- DEPRECATED
LogLevel int
Ready bool // If true, voice is ready to send/receive audio
UserID string
GuildID string
ChannelID string
deaf bool
mute bool
Debug bool // If true, print extra logging -- DEPRECATED
LogLevel int
Ready bool // If true, voice is ready to send/receive audio
UserID string
GuildID string
ChannelID string
deaf bool
mute bool
speaking bool
reconnecting bool // If true, voice connection is trying to reconnect
OpusSend chan []byte // Chan for sending opus audio
OpusRecv chan *Packet // Chan for receiving opus audio
@ -78,6 +80,8 @@ type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdat
// b : Send true if speaking, false if not.
func (v *VoiceConnection) Speaking(b bool) (err error) {
v.log(LogDebug, "called (%t)", b)
type voiceSpeakingData struct {
Speaking bool `json:"speaking"`
Delay int `json:"delay"`
@ -97,9 +101,11 @@ func (v *VoiceConnection) Speaking(b bool) (err error) {
err = v.wsConn.WriteJSON(data)
v.wsMutex.Unlock()
if err != nil {
v.speaking = false
log.Println("Speaking() write json error:", err)
return
}
v.speaking = b
return
}
@ -118,6 +124,7 @@ func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err
v.ChannelID = channelID
v.deaf = deaf
v.mute = mute
v.speaking = false
return
}
@ -148,10 +155,13 @@ func (v *VoiceConnection) Disconnect() (err error) {
// Close closes the voice ws and udp connections
func (v *VoiceConnection) Close() {
v.log(LogInformational, "called")
v.Lock()
defer v.Unlock()
v.Ready = false
v.speaking = false
if v.close != nil {
close(v.close)
@ -234,11 +244,14 @@ func (v *VoiceConnection) waitUntilConnected() error {
// are captured.
func (v *VoiceConnection) open() (err error) {
v.log(LogInformational, "called")
v.Lock()
defer v.Unlock()
// Don't open a websocket if one is already open
if v.wsConn != nil {
v.log(LogWarning, "refusing to overwrite non-nil websocket")
return
}
@ -321,14 +334,10 @@ func (v *VoiceConnection) wsListen(wsConn *websocket.Conn, close <-chan struct{}
}
v.log(LogDebug, "neterr udp error %s", neterr.Error())
}
// There has been an error reading, Close() the websocket so that
// OnDisconnect is fired.
// TODO add Voice OnDisconnect event :)
v.Close()
// TODO: close should return errs like data websocket Close
// Attempt to reconnect, with expenonential backoff up to 10 minutes.
// TODO add reconnect code
// Start reconnect goroutine then exit.
go v.reconnect()
}
return
}
@ -660,6 +669,13 @@ func (v *VoiceConnection) opusSender(udpConn *net.UDPConn, close <-chan struct{}
// else, continue loop
}
if !v.speaking {
err := v.Speaking(true)
if err != nil {
v.log(LogError, "error sending speaking packet, %s", err)
}
}
// Add sequence and timestamp to udpPacket
binary.BigEndian.PutUint16(udpHeader[2:], sequence)
binary.BigEndian.PutUint32(udpHeader[4:], timestamp)
@ -797,3 +813,56 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct
}
}
}
// Reconnect will close down a voice connection then immediately try to
// reconnect to that session.
func (v *VoiceConnection) reconnect() {
v.Lock()
if v.reconnecting {
return
}
v.reconnecting = true
v.Unlock()
defer func() { v.reconnecting = false }()
v.log(LogInformational, "called")
v.Close()
// Take a short nap to allow everything to close.
time.Sleep(1 * time.Second)
wait := time.Duration(1)
// TODO After X attempts abort.
// Right now this code has the potential to create abandoned goroutines
for {
if v.session == nil {
v.log(LogInformational, "cannot reconnect with nil session")
return
}
if v.session.DataReady == false {
v.log(LogInformational, "cannot reconenct with unready session")
continue
}
v.log(LogInformational, "trying to reconnect to voice")
_, err := v.session.ChannelVoiceJoin(v.GuildID, v.ChannelID, v.mute, v.deaf)
if err == nil {
v.log(LogInformational, "successfully reconnected to voice")
return
}
<-time.After(wait * time.Second)
wait *= 2
if wait > 600 {
wait = 600
}
}
}

View file

@ -236,6 +236,11 @@ func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) {
if s.Open() == nil {
s.log(LogInformational, "successfully reconnected to gateway")
// Now, if we have any VoiceConnections, reconnect all of them.
for _, v := range s.VoiceConnections {
go v.reconnect()
}
return
}
@ -607,6 +612,8 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) {
// the new region endpoint.
func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) {
s.log(LogInformational, "called")
voice, exists := s.VoiceConnections[st.GuildID]
// If no VoiceConnection exists, just skip this