소스 검색

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.
andrei 8 년 전
부모
커밋
1fc0e2053b
3개의 변경된 파일93개의 추가작업 그리고 68개의 파일을 삭제
  1. 2 2
      structs.go
  2. 46 24
      voice.go
  3. 45 42
      wsapi.go

+ 2 - 2
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.

+ 46 - 24
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 {

+ 45 - 42
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
 	}
 
-	// Store gID and cID for later use
-	s.Voice.guildID = gID
-	s.Voice.channelID = cID
-
-	return
-}
-
-// ChannelVoiceLeave disconnects from the currently connected
-// voice channel.
-func (s *Session) ChannelVoiceLeave() (err error) {
-
-	if s.Voice == nil {
-		return
+	// Create a new voice session
+	voice = &VoiceConnection{
+		Receive:     true,
+		session:     s,
+		connected:   make(chan bool),
+		sessionRecv: make(chan string),
 	}
 
-	// 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
-	}
+	// Store this in the waiting map so it can get a session/token
+	s.VoiceConnections[gID] = voice
 
-	// Close voice and nil data struct
-	s.Voice.Close()
-	s.Voice = nil
+	// Store gID and cID for later use
+	voice.guildID = gID
+	voice.channelID = cID
 
-	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