Merge pull request #141 from b1naryth1ef/feature/multiple-voice-connection

Add the ability to have multiple cross-guild voice connections open.
This commit is contained in:
Bruce 2016-03-09 21:14:43 -06:00
commit 0dac7777c1
4 changed files with 177 additions and 78 deletions

View file

@ -460,6 +460,45 @@ func (s *State) MessageRemove(message *Message) error {
return errors.New("Message not found.") return errors.New("Message not found.")
} }
func (s *State) VoiceStateUpdate(update *VoiceStateUpdate) error {
var exists bool
var guild *Guild
for _, guild = range s.Guilds {
if guild.ID == update.GuildID {
exists = true
break
}
}
if !exists {
return nil
}
// Handle Leaving Channel
if update.ChannelID == "" {
for i, state := range guild.VoiceStates {
if state.UserID == update.UserID {
guild.VoiceStates = append(guild.VoiceStates[:i], guild.VoiceStates[i+1:]...)
}
}
} else {
exists := false
for _, state := range guild.VoiceStates {
if state.UserID == update.UserID {
state.ChannelID = update.ChannelID
exists = true
}
}
if !exists {
guild.VoiceStates = append(guild.VoiceStates, update.VoiceState)
}
}
return nil
}
// Message gets a message by channel and message ID. // Message gets a message by channel and message ID.
func (s *State) Message(channelID, messageID string) (*Message, error) { func (s *State) Message(channelID, messageID string) (*Message, error) {
if s == nil { if s == nil {
@ -521,6 +560,8 @@ func (s *State) onInterface(se *Session, i interface{}) (err error) {
err = s.MessageAdd(t.Message) err = s.MessageAdd(t.Message)
case *MessageDelete: case *MessageDelete:
err = s.MessageRemove(t.Message) err = s.MessageRemove(t.Message)
case *VoiceStateUpdate:
err = s.VoiceStateUpdate(t)
} }
return return

View file

@ -54,8 +54,8 @@ type Session struct {
// Whether the UDP Connection is ready // Whether the UDP Connection is ready
UDPReady bool UDPReady bool
// Stores all details related to voice connections // Stores a mapping of guild id's to VoiceConnections
Voice *Voice VoiceConnections map[string]*VoiceConnection
// Managed state object, updated internally with events when // Managed state object, updated internally with events when
// StateEnabled is true. // StateEnabled is true.
@ -203,6 +203,7 @@ type VoiceState struct {
UserID string `json:"user_id"` UserID string `json:"user_id"`
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
GuildID string `json:"guild_id"`
Suppress bool `json:"suppress"` Suppress bool `json:"suppress"`
SelfMute bool `json:"self_mute"` SelfMute bool `json:"self_mute"`
SelfDeaf bool `json:"self_deaf"` SelfDeaf bool `json:"self_deaf"`

101
voice.go
View file

@ -12,6 +12,7 @@ package discordgo
import ( import (
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net" "net"
"runtime" "runtime"
@ -24,37 +25,45 @@ 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. // A VoiceConnectionConnection struct holds all the data and functions related to a Discord Voice Connection.
type Voice struct { type VoiceConnection struct {
sync.Mutex // future use sync.Mutex // future use
Ready bool // If true, voice is ready to send/receive audio Ready bool // If true, voice is ready to send/receive audio
Debug bool // If true, print extra logging Debug bool // If true, print extra logging
Receive bool // If false, don't try to receive packets
OP2 *voiceOP2 // exported for dgvoice, may change. OP2 *voiceOP2 // exported for dgvoice, may change.
OpusSend chan []byte // Chan for sending opus audio OpusSend chan []byte // Chan for sending opus audio
OpusRecv chan *Packet // Chan for receiving opus audio OpusRecv chan *Packet // Chan for receiving opus audio
GuildID string
ChannelID string
UserID string
// FrameRate int // This can be used to set the FrameRate of Opus data // FrameRate int // This can be used to set the FrameRate of Opus data
// FrameSize int // This can be used to set the FrameSize of Opus data // FrameSize int // This can be used to set the FrameSize of Opus data
wsConn *websocket.Conn wsConn *websocket.Conn
UDPConn *net.UDPConn // this will become unexported soon. UDPConn *net.UDPConn // this will become unexported soon.
session *Session
sessionID string sessionID string
token string token string
endpoint string endpoint string
guildID string
channelID string
userID string
op4 voiceOP4 op4 voiceOP4
// Used to send a close signal to goroutines // Used to send a close signal to goroutines
close chan struct{} 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 // A voiceOP4 stores the data for the voice operation 4 websocket event
@ -86,10 +95,9 @@ type voiceHandshakeOp struct {
} }
// Open opens a voice connection. This should be called // 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. // are captured.
func (v *Voice) Open() (err error) { func (v *VoiceConnection) Open() (err error) {
v.Lock() v.Lock()
defer v.Unlock() defer v.Unlock()
@ -98,7 +106,7 @@ func (v *Voice) Open() (err error) {
return return
} }
// Connect to Voice Websocket // Connect to VoiceConnection Websocket
vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80")) vg := fmt.Sprintf("wss://%s", strings.TrimSuffix(v.endpoint, ":80"))
v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil) v.wsConn, _, err = websocket.DefaultDialer.Dial(vg, nil)
if err != nil { if err != nil {
@ -106,7 +114,7 @@ func (v *Voice) Open() (err error) {
return return
} }
data := voiceHandshakeOp{0, voiceHandshakeData{v.guildID, v.userID, v.sessionID, v.token}} data := voiceHandshakeOp{0, voiceHandshakeData{v.GuildID, v.UserID, v.sessionID, v.token}}
err = v.wsConn.WriteJSON(data) err = v.wsConn.WriteJSON(data)
if err != nil { if err != nil {
@ -123,9 +131,24 @@ func (v *Voice) Open() (err error) {
return return
} }
func (v *VoiceConnection) WaitUntilConnected() error {
if v.Ready {
return nil
}
value, ok := <-v.connected
if (!value && !v.Ready) || !ok {
delete(v.session.VoiceConnections, v.GuildID)
return errors.New("Timed out connecting to voice")
}
return nil
}
// wsListen listens on the voice websocket for messages and passes them // wsListen listens on the voice websocket for messages and passes them
// to the voice event handler. This is automatically called by the Open func // 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 { for {
messageType, message, err := v.wsConn.ReadMessage() messageType, message, err := v.wsConn.ReadMessage()
@ -133,7 +156,7 @@ func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) {
// TODO: add reconnect, matching wsapi.go:listen() // TODO: add reconnect, matching wsapi.go:listen()
// TODO: Handle this problem better. // TODO: Handle this problem better.
// TODO: needs proper logging // TODO: needs proper logging
fmt.Println("Voice Listen Error:", err) fmt.Println("VoiceConnection Listen Error:", err)
return return
} }
@ -149,7 +172,7 @@ func (v *Voice) wsListen(wsConn *websocket.Conn, close <-chan struct{}) {
// wsEvent handles any voice websocket events. This is only called by the // wsEvent handles any voice websocket events. This is only called by the
// wsListen() function. // wsListen() function.
func (v *Voice) wsEvent(messageType int, message []byte) { func (v *VoiceConnection) wsEvent(messageType int, message []byte) {
if v.Debug { if v.Debug {
fmt.Println("wsEvent received: ", messageType) fmt.Println("wsEvent received: ", messageType)
@ -195,7 +218,13 @@ func (v *Voice) wsEvent(messageType int, message []byte) {
if v.OpusRecv == nil { if v.OpusRecv == nil {
v.OpusRecv = make(chan *Packet, 2) v.OpusRecv = make(chan *Packet, 2)
} }
if v.Receive {
go v.opusReceiver(v.UDPConn, v.close, v.OpusRecv) go v.opusReceiver(v.UDPConn, v.close, v.OpusRecv)
}
// Send the ready event
v.connected <- true
return return
case 3: // HEARTBEAT response case 3: // HEARTBEAT response
@ -240,7 +269,7 @@ type voiceHeartbeatOp struct {
// wsHeartbeat sends regular heartbeats to voice Discord so it knows the client // wsHeartbeat sends regular heartbeats to voice Discord so it knows the client
// is still connected. If you do not send these heartbeats Discord will // is still connected. If you do not send these heartbeats Discord will
// disconnect the websocket connection after a few seconds. // 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 { if close == nil || wsConn == nil {
return return
@ -278,10 +307,10 @@ type voiceSpeakingOp struct {
// This must be sent as true prior to sending audio and should be set to false // This must be sent as true prior to sending audio and should be set to false
// once finished sending audio. // once finished sending audio.
// b : Send true if speaking, false if not. // 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 { if v.wsConn == nil {
return fmt.Errorf("No Voice websocket.") return fmt.Errorf("No VoiceConnection websocket.")
} }
data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}} data := voiceSpeakingOp{5, voiceSpeakingData{b, 0}}
@ -295,7 +324,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 { type voiceUDPData struct {
@ -318,7 +347,7 @@ type voiceUDPOp struct {
// initial required handshake. This connection is left open in the session // 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 // and can be used to send or receive audio. This should only be called
// from voice.wsEvent OP2 // from voice.wsEvent OP2
func (v *Voice) udpOpen() (err error) { func (v *VoiceConnection) udpOpen() (err error) {
v.Lock() v.Lock()
defer v.Unlock() defer v.Unlock()
@ -354,7 +383,7 @@ func (v *Voice) udpOpen() (err error) {
return 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 // into it. Then send that over the UDP connection to Discord
sb := make([]byte, 70) sb := make([]byte, 70)
binary.BigEndian.PutUint32(sb, v.OP2.SSRC) binary.BigEndian.PutUint32(sb, v.OP2.SSRC)
@ -377,7 +406,7 @@ func (v *Voice) udpOpen() (err error) {
return return
} }
if rlen < 70 { 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 // Loop over position 4 though 20 to grab the IP address
@ -412,7 +441,7 @@ func (v *Voice) udpOpen() (err error) {
// udpKeepAlive sends a udp packet to keep the udp connection open // udpKeepAlive sends a udp packet to keep the udp connection open
// This is still a bit of a "proof of concept" // 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 { if UDPConn == nil || close == nil {
return return
@ -446,7 +475,7 @@ func (v *Voice) udpKeepAlive(UDPConn *net.UDPConn, close <-chan struct{}, i time
// opusSender will listen on the given channel and send any // opusSender will listen on the given channel and send any
// pre-encoded opus audio to Discord. Supposedly. // 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 { if UDPConn == nil || close == nil {
return return
@ -454,7 +483,7 @@ func (v *Voice) opusSender(UDPConn *net.UDPConn, close <-chan struct{}, opus <-c
runtime.LockOSThread() 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. // TODO: this needs reviewed as I think there must be a better way.
v.Ready = true v.Ready = true
defer func() { v.Ready = false }() defer func() { v.Ready = false }()
@ -536,7 +565,7 @@ type Packet struct {
// opusReceiver listens on the UDP socket for incoming packets // opusReceiver listens on the UDP socket for incoming packets
// and sends them across the given channel // and sends them across the given channel
// NOTE :: This function may change names later. // 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 { if UDPConn == nil || close == nil {
return return
@ -581,11 +610,17 @@ func (v *Voice) opusReceiver(UDPConn *net.UDPConn, close <-chan struct{}, c chan
} }
// Close closes the voice ws and udp connections // Close closes the voice ws and udp connections
func (v *Voice) Close() { func (v *VoiceConnection) Close() {
v.Lock() v.Lock()
defer v.Unlock() defer v.Unlock()
// Send a OP4 with a nil channel to disconnect
if v.sessionID != "" {
data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}}
v.session.wsConn.WriteJSON(data)
v.sessionID = ""
}
v.Ready = false v.Ready = false
if v.close != nil { if v.close != nil {
@ -608,4 +643,14 @@ func (v *Voice) Close() {
} }
v.wsConn = nil v.wsConn = nil
} }
delete(v.session.VoiceConnections, v.GuildID)
}
// Request to change channels
func (v *VoiceConnection) ChangeChannel(channelID string) (err error) {
data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, &channelID, true, true}}
err = v.session.wsConn.WriteJSON(data)
return err
} }

View file

@ -55,6 +55,8 @@ func (s *Session) Open() (err error) {
} }
}() }()
s.VoiceConnections = make(map[string]*VoiceConnection)
if s.wsConn != nil { if s.wsConn != nil {
err = errors.New("Web socket already opened.") err = errors.New("Web socket already opened.")
return return
@ -248,6 +250,7 @@ func (s *Session) UpdateStatus(idle int, game string) (err error) {
func (s *Session) event(messageType int, message []byte) { func (s *Session) event(messageType int, message []byte) {
var err error var err error
var reader io.Reader var reader io.Reader
reader = bytes.NewBuffer(message) reader = bytes.NewBuffer(message)
if messageType == 2 { if messageType == 2 {
@ -333,58 +336,60 @@ type voiceChannelJoinOp struct {
// cID : Channel ID of the channel to join. // cID : Channel ID of the channel to join.
// mute : If true, you will be set to muted upon joining. // mute : If true, you will be set to muted upon joining.
// deaf : If true, you will be set to deafened 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) { // timeout : If greater than zero, the timeout in milliseconds after which connecting will fail
func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool, timeout int) (voice *VoiceConnection, err error) {
// Create new voice{} struct if one does not exist. // If a voice connection for the guild exists, return that
// If you create this prior to calling this func then you can manually if _, exists := s.VoiceConnections[gID]; exists {
// set some variables if needed, such as to enable debugging. return s.VoiceConnections[gID], err
if s.Voice == nil {
s.Voice = &Voice{}
} }
// Send the request to Discord that we want to join the voice channel // Send the request to Discord that we want to join the voice channel
data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}}
err = s.wsConn.WriteJSON(data) err = s.wsConn.WriteJSON(data)
if err != nil { 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 // Store gID and cID for later use
s.Voice.guildID = gID voice.GuildID = gID
s.Voice.channelID = cID voice.ChannelID = cID
return // Queue the timeout in case we fail to connect
if timeout > 0 {
go func() {
time.Sleep(time.Millisecond * time.Duration(timeout))
if !voice.Ready {
voice.connected <- false
}
}()
} }
// ChannelVoiceLeave disconnects from the currently connected return voice, err
// 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
} }
// onVoiceStateUpdate handles Voice State Update events on the data // onVoiceStateUpdate handles Voice State Update events on the data
// websocket. This comes immediately after the call to VoiceChannelJoin // websocket. This comes immediately after the call to VoiceChannelJoin
// for the session user. // for the session user.
func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) { 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 // Check if we have a voice connection to update
if s.Voice == nil { voice, exists := s.VoiceConnections[st.GuildID]
if !exists {
return return
} }
@ -397,16 +402,14 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) {
return return
} }
// This event comes for all users, if it's not for the session // We only care about events that are about us
// user just ignore it.
// TODO Move this IF to the event() func
if st.UserID != self.ID { if st.UserID != self.ID {
return return
} }
// Store the SessionID for later use. // Store the SessionID for later use.
s.Voice.userID = self.ID // TODO: Review voice.UserID = self.ID // TODO: Review
s.Voice.sessionID = st.SessionID voice.sessionRecv <- st.SessionID
} }
// onVoiceServerUpdate handles the Voice Server Update data websocket event. // onVoiceServerUpdate handles the Voice Server Update data websocket event.
@ -417,19 +420,28 @@ func (s *Session) onVoiceStateUpdate(se *Session, st *VoiceStateUpdate) {
// to a voice channel. In that case, need to re-establish connection to // to a voice channel. In that case, need to re-establish connection to
// the new region endpoint. // the new region endpoint.
func (s *Session) onVoiceServerUpdate(se *Session, st *VoiceServerUpdate) { 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 // Store values for later use
s.Voice.token = st.Token voice.token = st.Token
s.Voice.endpoint = st.Endpoint voice.endpoint = st.Endpoint
s.Voice.guildID = st.GuildID voice.GuildID = st.GuildID
// If currently connected to voice ws/udp, then disconnect. // If currently connected to voice ws/udp, then disconnect.
// Has no effect if not connected. // 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 // We now have enough information to open a voice websocket conenction
// so, that's what the next call does. // so, that's what the next call does.
err := s.Voice.Open() err := voice.Open()
if err != nil { if err != nil {
fmt.Println("onVoiceServerUpdate Voice.Open error: ", err) fmt.Println("onVoiceServerUpdate Voice.Open error: ", err)
// TODO better logging // TODO better logging