From 1fc0e2053b3dbb81f042a0e99296480d584d33e2 Mon Sep 17 00:00:00 2001 From: andrei Date: Sat, 5 Mar 2016 01:02:21 -0800 Subject: [PATCH] Add support for multiple voice connections With the upcoming API changes, Discord will be allowing bot users to be onnected to more than one voice channel at a time. This commit is a first stab at implementing that functionality in discordgo. Voice works pretty good right now, ideally the next step is to cleanup some of the channel-spam and weird blocking-spots. --- structs.go | 4 +-- voice.go | 70 +++++++++++++++++++++++++++-------------- wsapi.go | 91 ++++++++++++++++++++++++++++-------------------------- 3 files changed, 95 insertions(+), 70 deletions(-) diff --git a/structs.go b/structs.go index 2646541..54c8eab 100644 --- a/structs.go +++ b/structs.go @@ -54,8 +54,8 @@ type Session struct { // Whether the UDP Connection is ready UDPReady bool - // Stores all details related to voice connections - Voice *Voice + // Stores a mapping of channel id's to VoiceConnections + VoiceConnections map[string]*VoiceConnection // Managed state object, updated internally with events when // StateEnabled is true. diff --git a/voice.go b/voice.go index fb1fa3a..fa32456 100644 --- a/voice.go +++ b/voice.go @@ -24,14 +24,15 @@ import ( ) // ------------------------------------------------------------------------------------------------ -// Code related to both Voice Websocket and UDP connections. +// Code related to both VoiceConnection Websocket and UDP connections. // ------------------------------------------------------------------------------------------------ -// A Voice struct holds all data and functions related to Discord Voice support. -type Voice struct { +// A VoiceConnectionConnection struct holds all the data and functions related to a Discord Voice Connection. +type VoiceConnection struct { sync.Mutex // future use Ready bool // If true, voice is ready to send/receive audio Debug bool // If true, print extra logging + Receive bool // If false, don't try to receive packets OP2 *voiceOP2 // exported for dgvoice, may change. OpusSend chan []byte // Chan for sending opus audio OpusRecv chan *Packet // Chan for receiving opus audio @@ -40,6 +41,7 @@ type Voice struct { wsConn *websocket.Conn UDPConn *net.UDPConn // this will become unexported soon. + session *Session sessionID string token string @@ -51,10 +53,16 @@ type Voice struct { // Used to send a close signal to goroutines close chan struct{} + + // Used to allow blocking until connected + connected chan bool + + // Used to pass the sessionid from onVoiceStateUpdate + sessionRecv chan string } // ------------------------------------------------------------------------------------------------ -// Code related to the Voice websocket connection +// Code related to the VoiceConnection websocket connection // ------------------------------------------------------------------------------------------------ // A voiceOP4 stores the data for the voice operation 4 websocket event @@ -86,9 +94,9 @@ type voiceHandshakeOp struct { } // Open opens a voice connection. This should be called -// after VoiceChannelJoin is used and the data VOICE websocket events +// after VoiceConnectionChannelJoin is used and the data VOICE websocket events // are captured. -func (v *Voice) Open() (err error) { +func (v *VoiceConnection) Open() (err error) { v.Lock() defer v.Unlock() @@ -98,7 +106,7 @@ func (v *Voice) Open() (err error) { return } - // Connect to Voice Websocket + // Connect to VoiceConnection Websocket vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80")) v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) if err != nil { @@ -123,9 +131,13 @@ func (v *Voice) Open() (err error) { return } +func (v *VoiceConnection) WaitUntilConnected() { + <-v.connected +} + // wsListen listens on the voice websocket for messages and passes them // to the voice event handler. This is automatically called by the Open func -func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { +func (v *VoiceConnection) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { for { messageType, message, err := v.wsConn.ReadMessage() @@ -133,7 +145,7 @@ func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { // TODO: add reconnect, matching wsapi.go:listen() // TODO: Handle this problem better. // TODO: needs proper logging - fmt.Println("Voice Listen Error:", err) + fmt.Println("VoiceConnection Listen Error:", err) return } @@ -149,7 +161,7 @@ func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) { // wsEvent handles any voice websocket events. This is only called by the // wsListen() function. -func (v *Voice) wsEvent(messageType int, message []byte) { +func (v *VoiceConnection) wsEvent(messageType int, message []byte) { if v.Debug { fmt.Println("wsEvent received: ", messageType) @@ -195,7 +207,13 @@ func (v *Voice) wsEvent(messageType int, message []byte) { if v.OpusRecv == nil { v.OpusRecv = make(chan *Packet, 2) } - go v.opusReceiver(v.UDPConn, v.close, v.OpusRecv) + + if v.Receive { + go v.opusReceiver(v.UDPConn, v.close, v.OpusRecv) + } + + // Send the ready event + v.connected <- true return case 3: // HEARTBEAT response @@ -240,7 +258,7 @@ type voiceHeartbeatOp struct { // wsHeartbeat sends regular heartbeats to voice Discord so it knows the client // is still connected. If you do not send these heartbeats Discord will // disconnect the websocket connection after a few seconds. -func (v *Voice) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) { +func (v *VoiceConnection) wsHeartbeat(wsConn *websocket.Conn, close <-chan struct{}, i time.Duration) { if close == nil || wsConn == nil { return @@ -278,10 +296,10 @@ type voiceSpeakingOp struct { // This must be sent as true prior to sending audio and should be set to false // once finished sending audio. // b : Send true if speaking, false if not. -func (v *Voice) Speaking(b bool) (err error) { +func (v *VoiceConnection) Speaking(b bool) (err error) { if v.wsConn == nil { - return fmt.Errorf("No Voice websocket.") + return fmt.Errorf("No VoiceConnection websocket.") } data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}} @@ -295,7 +313,7 @@ func (v *Voice) Speaking(b bool) (err error) { } // ------------------------------------------------------------------------------------------------ -// Code related to the Voice UDP connection +// Code related to the VoiceConnection UDP connection // ------------------------------------------------------------------------------------------------ type voiceUDPData struct { @@ -318,7 +336,7 @@ type voiceUDPOp struct { // initial required handshake. This connection is left open in the session // and can be used to send or receive audio. This should only be called // from voice.wsEvent OP2 -func (v *Voice) udpOpen() (err error) { +func (v *VoiceConnection) udpOpen() (err error) { v.Lock() defer v.Unlock() @@ -354,7 +372,7 @@ func (v *Voice) udpOpen() (err error) { return } - // Create a 70 byte array and put the SSRC code from the Op 2 Voice event + // Create a 70 byte array and put the SSRC code from the Op 2 VoiceConnection event // into it. Then send that over the UDP connection to Discord sb := make([]byte, 70) binary.BigEndian.PutUint32(sb, v.OP2.SSRC) @@ -377,7 +395,7 @@ func (v *Voice) udpOpen() (err error) { return } if rlen < 70 { - fmt.Println("Voice RLEN should be 70 but isn't") + fmt.Println("VoiceConnection RLEN should be 70 but isn't") } // Loop over position 4 though 20 to grab the IP address @@ -412,7 +430,7 @@ func (v *Voice) udpOpen() (err error) { // udpKeepAlive sends a udp packet to keep the udp connection open // This is still a bit of a "proof of concept" -func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time.Duration) { +func (v *VoiceConnection) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time.Duration) { if UDPConn == nil || close == nil { return @@ -446,7 +464,7 @@ func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time // opusSender will listen on the given channel and send any // pre-encoded opus audio to Discord. Supposedly. -func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) { +func (v *VoiceConnection) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-chan []byte, rate, size int) { if UDPConn == nil || close == nil { return @@ -454,7 +472,7 @@ func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-c runtime.LockOSThread() - // Voice is now ready to receive audio packets + // VoiceConnection is now ready to receive audio packets // TODO: this needs reviewed as I think there must be a better way. v.Ready = true defer func() { v.Ready = false }() @@ -536,7 +554,7 @@ type Packet struct { // opusReceiver listens on the UDP socket for incoming packets // and sends them across the given channel // NOTE :: This function may change names later. -func (v *Voice) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan *Packet) { +func (v *VoiceConnection) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan *Packet) { if UDPConn == nil || close == nil { return @@ -581,11 +599,15 @@ func (v *Voice) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan } // Close closes the voice ws and udp connections -func (v *Voice) Close() { - +func (v *VoiceConnection) Close() { v.Lock() defer v.Unlock() + if v.Ready { + data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.guildID, nil, true, true}} + v.session.wsConn.WriteJSON(data) + } + v.Ready = false if v.close != nil { diff --git a/wsapi.go b/wsapi.go index 7e4ad19..9e93b20 100644 --- a/wsapi.go +++ b/wsapi.go @@ -55,6 +55,8 @@ func (s *Session) Open() (err error) { } }() + s.VoiceConnections = make(map[string]*VoiceConnection) + if s.wsConn != nil { err = errors.New("Web socket already opened.") return @@ -248,6 +250,7 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) { func (s *Session) event(messageType int, message []byte) { var err error var reader io.Reader + reader = bytes.NewBuffer(message) if messageType == 2 { @@ -333,58 +336,49 @@ type voiceChannelJoinOp struct { // cID : Channel ID of the channel to join. // mute : If true, you will be set to muted upon joining. // deaf : If true, you will be set to deafened upon joining. -func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (err error) { - - // Create new voice{} struct if one does not exist. - // If you create this prior to calling this func then you can manually - // set some variables if needed, such as to enable debugging. - if s.Voice == nil { - s.Voice = &Voice{} - } - +func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *VoiceConnection, err error) { // Send the request to Discord that we want to join the voice channel data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} err = s.wsConn.WriteJSON(data) if err != nil { - return + return nil, err } + // Create a new voice session + voice = &VoiceConnection{ + Receive: true, + session: s, + connected: make(chan bool), + sessionRecv: make(chan string), + } + + // Store this in the waiting map so it can get a session/token + s.VoiceConnections[gID] = voice + // Store gID and cID for later use - s.Voice.guildID = gID - s.Voice.channelID = cID + voice.guildID = gID + voice.channelID = cID - return -} - -// ChannelVoiceLeave disconnects from the currently connected -// voice channel. -func (s *Session) ChannelVoiceLeave() (err error) { - - if s.Voice == nil { - return - } - - // Send the request to Discord that we want to leave voice - data := voiceChannelJoinOp{4, voiceChannelJoinData{nil, nil, true, true}} - err = s.wsConn.WriteJSON(data) - if err != nil { - return - } - - // Close voice and nil data struct - s.Voice.Close() - s.Voice = nil - - return + return voice, err } // onVoiceStateUpdate handles Voice State Update events on the data // websocket. This comes immediately after the call to VoiceChannelJoin // for the session user. func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { + // If we don't have a connection for the channel, don't bother + if st.ChannelID == "" { + return + } - // Ignore if Voice is nil - if s.Voice == nil { + channel, err := s.Channel(st.ChannelID) + if err != nil { + fmt.Println(err) + return + } + + voice, exists := s.VoiceConnections[channel.GuildID] + if !exists { return } @@ -405,8 +399,8 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { } // Store the SessionID for later use. - s.Voice.userID = self.ID // TODO: Review - s.Voice.sessionID = st.SessionID + voice.userID = self.ID // TODO: Review + voice.sessionRecv <- st.SessionID } // onVoiceServerUpdate handles the Voice Server Update data websocket event. @@ -417,19 +411,28 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { // to a voice channel. In that case, need to re-establish connection to // the new region endpoint. func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) { + voice, exists := s.VoiceConnections[st.GuildID] + + // If no VoiceConnection exists, just skip this + if !exists { + return + } // Store values for later use - s.Voice.token = st.Token - s.Voice.endpoint = st.Endpoint - s.Voice.guildID = st.GuildID + voice.token = st.Token + voice.endpoint = st.Endpoint + voice.guildID = st.GuildID // If currently connected to voice ws/udp, then disconnect. // Has no effect if not connected. - s.Voice.Close() + voice.Close() + + // Wait for the sessionID from onVoiceStateUpdate + voice.sessionID = <-voice.sessionRecv // We now have enough information to open a voice websocket conenction // so, that's what the next call does. - err := s.Voice.Open() + err := voice.Open() if err != nil { fmt.Println("onVoiceServerUpdate Voice.Open error: ", err) // TODO better logging