Harden UDP audio transport and logging
This commit is contained in:
committed by
Brandon McGinty
parent
64d0ef6f0f
commit
7d7d59649b
+29
-10
@@ -1,6 +1,7 @@
|
|||||||
package fileplayback
|
package fileplayback
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
@@ -18,6 +19,8 @@ type Player struct {
|
|||||||
filename string
|
filename string
|
||||||
audioChan chan gumble.AudioBuffer
|
audioChan chan gumble.AudioBuffer
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
playing bool
|
playing bool
|
||||||
errorFunc func(error)
|
errorFunc func(error)
|
||||||
@@ -73,6 +76,7 @@ func (p *Player) PlayFile(filename string) error {
|
|||||||
// Start the file reading goroutine
|
// Start the file reading goroutine
|
||||||
p.playing = true
|
p.playing = true
|
||||||
p.stopChan = make(chan struct{})
|
p.stopChan = make(chan struct{})
|
||||||
|
p.ctx, p.cancel = context.WithCancel(context.Background())
|
||||||
go p.readFileAudio()
|
go p.readFileAudio()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -88,17 +92,23 @@ func (p *Player) Stop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
close(p.stopChan)
|
close(p.stopChan)
|
||||||
|
if p.cancel != nil {
|
||||||
|
p.cancel()
|
||||||
|
p.cancel = nil
|
||||||
|
}
|
||||||
p.playing = false
|
p.playing = false
|
||||||
|
localPlayback := p.localPlayback
|
||||||
|
p.mutex.Unlock()
|
||||||
|
|
||||||
if p.localPlayback != nil {
|
if localPlayback != nil {
|
||||||
p.localPlayback(nil)
|
localPlayback(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain the audio channel
|
// Drain the audio channel.
|
||||||
for len(p.audioChan) > 0 {
|
for len(p.audioChan) > 0 {
|
||||||
<-p.audioChan
|
<-p.audioChan
|
||||||
}
|
}
|
||||||
|
p.mutex.Lock()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +148,10 @@ func (p *Player) readFileAudio() {
|
|||||||
args := []string{"-loglevel", "error", "-i", p.filename}
|
args := []string{"-loglevel", "error", "-i", p.filename}
|
||||||
args = append(args, "-ac", "2", "-ar", strconv.Itoa(gumble.AudioSampleRate), "-f", "s16le", "-")
|
args = append(args, "-ac", "2", "-ar", strconv.Itoa(gumble.AudioSampleRate), "-f", "s16le", "-")
|
||||||
|
|
||||||
cmd := exec.Command("ffmpeg", args...)
|
p.mutex.Lock()
|
||||||
|
ctx := p.ctx
|
||||||
|
p.mutex.Unlock()
|
||||||
|
cmd := exec.CommandContext(ctx, "ffmpeg", args...)
|
||||||
pipe, err := cmd.StdoutPipe()
|
pipe, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
p.mutex.Lock()
|
p.mutex.Lock()
|
||||||
@@ -171,15 +184,21 @@ func (p *Player) readFileAudio() {
|
|||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
n, err := io.ReadFull(pipe, byteBuffer)
|
n, err := io.ReadFull(pipe, byteBuffer)
|
||||||
if err != nil || n != len(byteBuffer) {
|
if err != nil || n != len(byteBuffer) {
|
||||||
// File finished playing
|
select {
|
||||||
|
case <-p.stopChan:
|
||||||
|
cmd.Wait()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
// File finished playing.
|
||||||
p.mutex.Lock()
|
p.mutex.Lock()
|
||||||
p.playing = false
|
p.playing = false
|
||||||
if p.localPlayback != nil {
|
localPlayback := p.localPlayback
|
||||||
p.localPlayback(nil)
|
|
||||||
}
|
|
||||||
p.mutex.Unlock()
|
p.mutex.Unlock()
|
||||||
|
if localPlayback != nil {
|
||||||
|
localPlayback(nil)
|
||||||
|
}
|
||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
// Notify that file finished
|
|
||||||
p.reportError(errors.New("file playback finished"))
|
p.reportError(errors.New("file playback finished"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,4 +92,5 @@ type AudioPacket struct {
|
|||||||
|
|
||||||
HasPosition bool
|
HasPosition bool
|
||||||
X, Y, Z float32
|
X, Y, Z float32
|
||||||
|
VolumeAdjustment float32
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package gumble
|
package gumble
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
type audioEventItem struct {
|
type audioEventItem struct {
|
||||||
parent *AudioListeners
|
parent *AudioListeners
|
||||||
prev, next *audioEventItem
|
prev, next *audioEventItem
|
||||||
@@ -8,6 +10,8 @@ type audioEventItem struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *audioEventItem) Detach() {
|
func (e *audioEventItem) Detach() {
|
||||||
|
e.parent.mu.Lock()
|
||||||
|
defer e.parent.mu.Unlock()
|
||||||
if e.prev == nil {
|
if e.prev == nil {
|
||||||
e.parent.head = e.next
|
e.parent.head = e.next
|
||||||
} else {
|
} else {
|
||||||
@@ -23,11 +27,14 @@ func (e *audioEventItem) Detach() {
|
|||||||
// AudioListeners is a list of audio listeners. Each attached listener is
|
// AudioListeners is a list of audio listeners. Each attached listener is
|
||||||
// called in sequence when a new user audio stream begins.
|
// called in sequence when a new user audio stream begins.
|
||||||
type AudioListeners struct {
|
type AudioListeners struct {
|
||||||
|
mu sync.Mutex
|
||||||
head, tail *audioEventItem
|
head, tail *audioEventItem
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach adds a new audio listener to the end of the current list of listeners.
|
// Attach adds a new audio listener to the end of the current list of listeners.
|
||||||
func (e *AudioListeners) Attach(listener AudioListener) Detacher {
|
func (e *AudioListeners) Attach(listener AudioListener) Detacher {
|
||||||
|
e.mu.Lock()
|
||||||
|
defer e.mu.Unlock()
|
||||||
item := &audioEventItem{
|
item := &audioEventItem{
|
||||||
parent: e,
|
parent: e,
|
||||||
prev: e.tail,
|
prev: e.tail,
|
||||||
|
|||||||
+30
-13
@@ -6,6 +6,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -70,8 +71,15 @@ type Client struct {
|
|||||||
VoiceTarget *VoiceTarget
|
VoiceTarget *VoiceTarget
|
||||||
|
|
||||||
// UDP transport for audio (lower latency than TCP-tunneled audio).
|
// UDP transport for audio (lower latency than TCP-tunneled audio).
|
||||||
|
udpMu sync.RWMutex
|
||||||
|
udpWriteMu sync.Mutex
|
||||||
udpConn *net.UDPConn
|
udpConn *net.UDPConn
|
||||||
udpActive bool
|
udpActive bool
|
||||||
|
udpCryptoOut *cryptState15
|
||||||
|
udpCryptoIn *cryptState15
|
||||||
|
udpFrameNumber uint64
|
||||||
|
udpFallbackLogged atomic.Bool
|
||||||
|
udpFirstRecv atomic.Bool
|
||||||
cryptOut cryptState // client→server encryption
|
cryptOut cryptState // client→server encryption
|
||||||
cryptIn cryptState // server→client encryption
|
cryptIn cryptState // server→client encryption
|
||||||
|
|
||||||
@@ -258,7 +266,10 @@ func (c *Client) readRoutine() {
|
|||||||
}
|
}
|
||||||
// When UDP audio is active, ignore TCP-tunneled audio
|
// When UDP audio is active, ignore TCP-tunneled audio
|
||||||
// (packet type 1) to avoid double-processing packets.
|
// (packet type 1) to avoid double-processing packets.
|
||||||
if pType == 1 && c.udpActive {
|
c.udpMu.RLock()
|
||||||
|
udpActive := c.udpActive
|
||||||
|
c.udpMu.RUnlock()
|
||||||
|
if pType == 1 && udpActive {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if int(pType) < len(handlers) {
|
if int(pType) < len(handlers) {
|
||||||
@@ -270,11 +281,15 @@ func (c *Client) readRoutine() {
|
|||||||
atomic.StoreUint32(&c.state, uint32(StateDisconnected))
|
atomic.StoreUint32(&c.state, uint32(StateDisconnected))
|
||||||
close(c.end)
|
close(c.end)
|
||||||
|
|
||||||
// Clean up UDP connection
|
// Clean up UDP connection.
|
||||||
if c.udpConn != nil {
|
c.udpMu.Lock()
|
||||||
log.Debug("closing UDP connection")
|
udpConn := c.udpConn
|
||||||
c.udpConn.Close()
|
|
||||||
c.udpConn = nil
|
c.udpConn = nil
|
||||||
|
c.udpActive = false
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
if udpConn != nil {
|
||||||
|
log.Debug("closing UDP connection")
|
||||||
|
udpConn.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
if wasSynced {
|
if wasSynced {
|
||||||
@@ -331,27 +346,29 @@ func (c *Client) EnableStereoEncoder() {
|
|||||||
|
|
||||||
// WriteAudio writes an audio packet, preferring UDP when encryption is
|
// WriteAudio writes an audio packet, preferring UDP when encryption is
|
||||||
// set up. Falls back to TCP-tunneled audio when UDP is unavailable.
|
// set up. Falls back to TCP-tunneled audio when UDP is unavailable.
|
||||||
var udpFallbackLogged bool
|
|
||||||
|
|
||||||
func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, data []byte, X, Y, Z *float32) error {
|
func (c *Client) WriteAudio(format, target byte, sequence int64, final bool, data []byte, X, Y, Z *float32) error {
|
||||||
// Try Mumble 1.5 native UDP first (unless disabled)
|
// Try Mumble 1.5 native UDP first (unless disabled)
|
||||||
if !c.Config.DisableUDP {
|
if !c.Config.DisableUDP {
|
||||||
if sent, err := c.WriteAudioUDP15(data, final); sent {
|
if sent, err := c.WriteAudioUDP15(uint32(target), data, final); sent {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("UDP15 send error: %v", err)
|
log.Error("UDP15 send error: %v", err)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Fall back to TCP tunnel — log once per process
|
// Fall back to the TCP tunnel.
|
||||||
if !udpFallbackLogged {
|
c.udpMu.RLock()
|
||||||
udpFallbackLogged = true
|
udpConn := c.udpConn
|
||||||
|
udpCryptoOut := c.udpCryptoOut
|
||||||
|
c.udpMu.RUnlock()
|
||||||
|
if !c.udpFallbackLogged.Swap(true) {
|
||||||
if c.Config.DisableUDP {
|
if c.Config.DisableUDP {
|
||||||
log.Info("UDP disabled, audio using TCP tunnel")
|
log.Info("UDP disabled, audio using TCP tunnel")
|
||||||
} else if c.udpConn == nil {
|
} else if udpConn == nil {
|
||||||
log.Info("no UDP socket, audio using TCP tunnel")
|
log.Info("no UDP socket, audio using TCP tunnel")
|
||||||
} else if udp15Client == nil || !udp15Client.initialized {
|
} else if udpCryptoOut == nil {
|
||||||
log.Info("1.5 crypto not ready, audio using TCP tunnel")
|
log.Info("UDP crypto not ready, audio using TCP tunnel")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z)
|
return c.Conn.WriteAudio(format, target, sequence, final, data, X, Y, Z)
|
||||||
|
|||||||
+16
-10
@@ -253,11 +253,13 @@ func (cs *cryptState) setup(key, iv []byte) error {
|
|||||||
|
|
||||||
cs.initialized = true
|
cs.initialized = true
|
||||||
|
|
||||||
log.Info("cryptState setup: key=%s iv=%s encIV=%s nonce_prefix=%s",
|
if log.Enabled(log.LevelDebug) {
|
||||||
|
log.Debug("cryptState setup: key=%s iv=%s encIV=%s nonce_prefix=%s",
|
||||||
hex.EncodeToString(cs.key[:]),
|
hex.EncodeToString(cs.key[:]),
|
||||||
hex.EncodeToString(iv),
|
hex.EncodeToString(iv),
|
||||||
hex.EncodeToString(encIV[:]),
|
hex.EncodeToString(encIV[:]),
|
||||||
hex.EncodeToString(cs.nonce[:]))
|
hex.EncodeToString(cs.nonce[:]))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -302,8 +304,10 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
|
|||||||
c.cryptOut.setup(packet.Key, packet.ClientNonce)
|
c.cryptOut.setup(packet.Key, packet.ClientNonce)
|
||||||
c.cryptIn.setup(packet.Key, packet.ServerNonce)
|
c.cryptIn.setup(packet.Key, packet.ServerNonce)
|
||||||
|
|
||||||
// Also set up Mumble 1.5 native UDP crypto.
|
// Also set up per-client Mumble 1.5 native UDP crypto.
|
||||||
SetUDP15Crypto(packet.Key, packet.ClientNonce, packet.ServerNonce)
|
if err := c.setUDP15Crypto(packet.Key, packet.ClientNonce, packet.ServerNonce); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if wasInit {
|
if wasInit {
|
||||||
log.Debug("CryptSetup updated (key rotation)")
|
log.Debug("CryptSetup updated (key rotation)")
|
||||||
@@ -316,17 +320,19 @@ func (c *Client) handleCryptSetup(buffer []byte) error {
|
|||||||
log.Debug("received CryptSetup with incomplete fields, waiting for full key exchange")
|
log.Debug("received CryptSetup with incomplete fields, waiting for full key exchange")
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.cryptOut.initialized && c.udpConn != nil && !c.udpActive {
|
c.udpMu.Lock()
|
||||||
// Only start UDP reader if 1.5 crypto is also ready.
|
udpReady := c.udpCryptoOut != nil
|
||||||
if udp15Client != nil && udp15Client.initialized {
|
startUDP := c.cryptOut.initialized && c.udpConn != nil && !c.udpActive && udpReady
|
||||||
|
if startUDP {
|
||||||
c.udpActive = true
|
c.udpActive = true
|
||||||
|
}
|
||||||
|
noUDPConn := c.udpConn == nil
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
if startUDP {
|
||||||
log.Info("UDP crypto ready (1.5 native), starting UDP reader and pinger")
|
log.Info("UDP crypto ready (1.5 native), starting UDP reader and pinger")
|
||||||
go c.udpReadRoutine()
|
go c.udpReadRoutine()
|
||||||
go c.udpPingRoutine()
|
go c.udpPingRoutine()
|
||||||
} else {
|
} else if c.cryptOut.initialized && noUDPConn {
|
||||||
log.Info("legacy crypto ready, waiting for 1.5 crypto before starting UDP")
|
|
||||||
}
|
|
||||||
} else if c.cryptOut.initialized && c.udpConn == nil {
|
|
||||||
log.Warn("crypto ready but no UDP socket — audio will use TCP tunnel")
|
log.Warn("crypto ready but no UDP socket — audio will use TCP tunnel")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-14
@@ -10,8 +10,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
|
"git.stormux.org/storm/barnard/gumble/gumble/MumbleProto"
|
||||||
"git.stormux.org/storm/barnard/log"
|
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble/varint"
|
"git.stormux.org/storm/barnard/gumble/gumble/varint"
|
||||||
|
"git.stormux.org/storm/barnard/log"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -231,25 +231,41 @@ func (c *Client) dispatchPLC(user *User, audioTarget byte, decoder AudioDecoder)
|
|||||||
|
|
||||||
// dispatchAudio sends an audio packet to all registered audio listeners.
|
// dispatchAudio sends an audio packet to all registered audio listeners.
|
||||||
func (c *Client) dispatchAudio(user *User, packet *AudioPacket) {
|
func (c *Client) dispatchAudio(user *User, packet *AudioPacket) {
|
||||||
c.volatile.Lock()
|
listeners := &c.Config.AudioListeners
|
||||||
for item := c.Config.AudioListeners.head; item != nil; item = item.next {
|
listeners.mu.Lock()
|
||||||
c.volatile.Unlock()
|
type delivery struct {
|
||||||
|
listener AudioListener
|
||||||
|
ch chan *AudioPacket
|
||||||
|
new bool
|
||||||
|
}
|
||||||
|
var deliveries []delivery
|
||||||
|
for item := listeners.head; item != nil; item = item.next {
|
||||||
ch := item.streams[user]
|
ch := item.streams[user]
|
||||||
if ch == nil {
|
newStream := ch == nil
|
||||||
ch = make(chan *AudioPacket)
|
if newStream {
|
||||||
|
bufferSize := c.Config.Buffers
|
||||||
|
if bufferSize < 1 {
|
||||||
|
bufferSize = 1
|
||||||
|
}
|
||||||
|
ch = make(chan *AudioPacket, bufferSize)
|
||||||
item.streams[user] = ch
|
item.streams[user] = ch
|
||||||
|
}
|
||||||
|
deliveries = append(deliveries, delivery{item.listener, ch, newStream})
|
||||||
|
}
|
||||||
|
listeners.mu.Unlock()
|
||||||
|
|
||||||
|
for _, delivery := range deliveries {
|
||||||
|
if delivery.new {
|
||||||
log.Debug("new audio stream from %s (session=%d)", user.Name, user.Session)
|
log.Debug("new audio stream from %s (session=%d)", user.Name, user.Session)
|
||||||
streamEvent := AudioStreamEvent{
|
delivery.listener.OnAudioStream(&AudioStreamEvent{Client: c, User: user, C: delivery.ch})
|
||||||
Client: c,
|
|
||||||
User: user,
|
|
||||||
C: ch,
|
|
||||||
}
|
}
|
||||||
item.listener.OnAudioStream(&streamEvent)
|
select {
|
||||||
|
case delivery.ch <- packet:
|
||||||
|
default:
|
||||||
|
// Never allow a slow listener to block protocol processing.
|
||||||
|
log.Debug("dropping buffered audio for slow listener (session=%d)", user.Session)
|
||||||
}
|
}
|
||||||
ch <- packet
|
|
||||||
c.volatile.Lock()
|
|
||||||
}
|
}
|
||||||
c.volatile.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) handleAuthenticate(buffer []byte) error {
|
func (c *Client) handleAuthenticate(buffer []byte) error {
|
||||||
|
|||||||
+19
-6
@@ -32,7 +32,9 @@ func (c *Client) startUDP() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.udpMu.Lock()
|
||||||
c.udpConn = conn
|
c.udpConn = conn
|
||||||
|
c.udpMu.Unlock()
|
||||||
log.Info("UDP socket connected to %s", conn.RemoteAddr())
|
log.Info("UDP socket connected to %s", conn.RemoteAddr())
|
||||||
// The UDP reader and pinger will be started once CryptSetup is received.
|
// The UDP reader and pinger will be started once CryptSetup is received.
|
||||||
return nil
|
return nil
|
||||||
@@ -44,15 +46,23 @@ func (c *Client) udpReadRoutine() {
|
|||||||
log.Info("UDP reader started (1.5 native format)")
|
log.Info("UDP reader started (1.5 native format)")
|
||||||
buf := make([]byte, maxUDPPacketSize)
|
buf := make([]byte, maxUDPPacketSize)
|
||||||
var packetCount uint64
|
var packetCount uint64
|
||||||
|
c.udpMu.RLock()
|
||||||
|
udpConn := c.udpConn
|
||||||
|
c.udpMu.RUnlock()
|
||||||
|
if udpConn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
for {
|
for {
|
||||||
n, addr, err := c.udpConn.ReadFromUDP(buf)
|
n, addr, err := udpConn.ReadFromUDP(buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn("UDP read error (stopping reader): %v", err)
|
log.Warn("UDP read error (stopping reader): %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
packetCount++
|
packetCount++
|
||||||
log.Info("UDP recv #%d: %d bytes from %s hex=%s",
|
if log.Enabled(log.LevelDebug) {
|
||||||
|
log.Debug("UDP recv #%d: %d bytes from %s hex=%s",
|
||||||
packetCount, n, addr, hex.EncodeToString(buf[:n]))
|
packetCount, n, addr, hex.EncodeToString(buf[:n]))
|
||||||
|
}
|
||||||
packet := make([]byte, n)
|
packet := make([]byte, n)
|
||||||
copy(packet, buf[:n])
|
copy(packet, buf[:n])
|
||||||
c.HandleUDPPacket15(packet, packetCount)
|
c.HandleUDPPacket15(packet, packetCount)
|
||||||
@@ -79,8 +89,12 @@ func (c *Client) udpPingRoutine() {
|
|||||||
// sendUDPPing sends a Mumble 1.5 native UDP ping.
|
// sendUDPPing sends a Mumble 1.5 native UDP ping.
|
||||||
// Uses standard protobuf varint encoding (not Mumble's custom varint).
|
// Uses standard protobuf varint encoding (not Mumble's custom varint).
|
||||||
func (c *Client) sendUDPPing() {
|
func (c *Client) sendUDPPing() {
|
||||||
cs := udp15Client
|
c.udpWriteMu.Lock()
|
||||||
if cs == nil || !cs.initialized || c.udpConn == nil {
|
defer c.udpWriteMu.Unlock()
|
||||||
|
c.udpMu.RLock()
|
||||||
|
cs, udpConn := c.udpCryptoOut, c.udpConn
|
||||||
|
c.udpMu.RUnlock()
|
||||||
|
if cs == nil || udpConn == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Type byte 0x01 = UDPPing, field 1 = timestamp (protobuf varint, milliseconds).
|
// Type byte 0x01 = UDPPing, field 1 = timestamp (protobuf varint, milliseconds).
|
||||||
@@ -98,6 +112,5 @@ func (c *Client) sendUDPPing() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.udpConn.Write(encrypted)
|
udpConn.Write(encrypted)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+106
-93
@@ -3,8 +3,11 @@ package gumble
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/aes"
|
"crypto/aes"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
|
"math"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"git.stormux.org/storm/barnard/gumble/gumble/varint"
|
"git.stormux.org/storm/barnard/gumble/gumble/varint"
|
||||||
"git.stormux.org/storm/barnard/log"
|
"git.stormux.org/storm/barnard/log"
|
||||||
@@ -35,13 +38,14 @@ func pbEncodeVarint(buf []byte, v uint64) int {
|
|||||||
// and the number of bytes consumed (0 on error).
|
// and the number of bytes consumed (0 on error).
|
||||||
func pbDecodeVarint(buf []byte) (uint64, int) {
|
func pbDecodeVarint(buf []byte) (uint64, int) {
|
||||||
var v uint64
|
var v uint64
|
||||||
var s uint
|
|
||||||
for i, b := range buf {
|
for i, b := range buf {
|
||||||
v |= uint64(b&0x7F) << s
|
if i == 10 || (i == 9 && b > 1) {
|
||||||
|
return 0, 0 // overflow
|
||||||
|
}
|
||||||
|
v |= uint64(b&0x7F) << (7 * i)
|
||||||
if b < 0x80 {
|
if b < 0x80 {
|
||||||
return v, i + 1
|
return v, i + 1
|
||||||
}
|
}
|
||||||
s += 7
|
|
||||||
}
|
}
|
||||||
return 0, 0 // truncated
|
return 0, 0 // truncated
|
||||||
}
|
}
|
||||||
@@ -59,23 +63,16 @@ func pbDecodeVarint(buf []byte) (uint64, int) {
|
|||||||
// encodeUDPAudio builds a MumbleUDP.Audio protobuf message.
|
// encodeUDPAudio builds a MumbleUDP.Audio protobuf message.
|
||||||
// If session == 0, sender_session is omitted (used for outbound).
|
// If session == 0, sender_session is omitted (used for outbound).
|
||||||
// Uses standard protobuf varint encoding, not Mumble's custom varint.
|
// Uses standard protobuf varint encoding, not Mumble's custom varint.
|
||||||
func encodeUDPAudio(session uint32, frameNumber uint32, opusData []byte, terminator bool) []byte {
|
func encodeUDPAudio(target uint32, frameNumber uint64, opusData []byte, terminator bool) []byte {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
var tmp [10]byte // max protobuf varint size
|
var tmp [10]byte // max protobuf varint size
|
||||||
|
|
||||||
// Field 1: target = 0 (normal speech). Always encoded so the server
|
// Field 1 selects the target header oneof.
|
||||||
// sees the oneof Header choice, matching the reference client.
|
|
||||||
n := pbEncodeVarint(tmp[:], uint64((1<<3)|0))
|
n := pbEncodeVarint(tmp[:], uint64((1<<3)|0))
|
||||||
buf.Write(tmp[:n])
|
buf.Write(tmp[:n])
|
||||||
n = pbEncodeVarint(tmp[:], 0)
|
n = pbEncodeVarint(tmp[:], uint64(target))
|
||||||
buf.Write(tmp[:n])
|
buf.Write(tmp[:n])
|
||||||
|
|
||||||
if session != 0 {
|
|
||||||
n := pbEncodeVarint(tmp[:], uint64((3<<3)|0))
|
|
||||||
buf.Write(tmp[:n])
|
|
||||||
n = pbEncodeVarint(tmp[:], uint64(session))
|
|
||||||
buf.Write(tmp[:n])
|
|
||||||
}
|
|
||||||
n = pbEncodeVarint(tmp[:], uint64((4<<3)|0))
|
n = pbEncodeVarint(tmp[:], uint64((4<<3)|0))
|
||||||
buf.Write(tmp[:n])
|
buf.Write(tmp[:n])
|
||||||
n = pbEncodeVarint(tmp[:], uint64(frameNumber))
|
n = pbEncodeVarint(tmp[:], uint64(frameNumber))
|
||||||
@@ -99,7 +96,9 @@ func encodeUDPAudio(session uint32, frameNumber uint32, opusData []byte, termina
|
|||||||
|
|
||||||
// decodeUDPAudio parses a MumbleUDP.Audio protobuf message.
|
// decodeUDPAudio parses a MumbleUDP.Audio protobuf message.
|
||||||
// Uses standard protobuf varint decoding, not Mumble's custom varint.
|
// Uses standard protobuf varint decoding, not Mumble's custom varint.
|
||||||
func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData []byte, terminator bool) {
|
func decodeUDPAudio(data []byte) (session uint32, frameNumber uint64, opusData []byte, terminator bool, context uint32, position *[3]float32, volumeAdjustment float32) {
|
||||||
|
var positionValues [3]float32
|
||||||
|
positionCount := 0
|
||||||
pos := 0
|
pos := 0
|
||||||
for pos < len(data) {
|
for pos < len(data) {
|
||||||
key, n := pbDecodeVarint(data[pos:])
|
key, n := pbDecodeVarint(data[pos:])
|
||||||
@@ -118,10 +117,12 @@ func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData [
|
|||||||
}
|
}
|
||||||
pos += n
|
pos += n
|
||||||
switch fieldNum {
|
switch fieldNum {
|
||||||
|
case 2:
|
||||||
|
context = uint32(val)
|
||||||
case 3:
|
case 3:
|
||||||
session = uint32(val)
|
session = uint32(val)
|
||||||
case 4:
|
case 4:
|
||||||
frameNumber = uint32(val)
|
frameNumber = val
|
||||||
case 16:
|
case 16:
|
||||||
terminator = val != 0
|
terminator = val != 0
|
||||||
}
|
}
|
||||||
@@ -131,18 +132,43 @@ func decodeUDPAudio(data []byte) (session uint32, frameNumber uint32, opusData [
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
pos += n
|
pos += n
|
||||||
if int(length) > len(data)-pos {
|
if length > uint64(len(data)-pos) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
end := pos + int(length)
|
||||||
if fieldNum == 5 {
|
if fieldNum == 5 {
|
||||||
opusData = make([]byte, length)
|
opusData = append(opusData[:0], data[pos:end]...)
|
||||||
copy(opusData, data[pos:pos+int(length)])
|
} else if fieldNum == 6 && length == 12 {
|
||||||
|
for i := range positionValues {
|
||||||
|
positionValues[i] = math.Float32frombits(binary.LittleEndian.Uint32(data[pos+i*4:]))
|
||||||
}
|
}
|
||||||
pos += int(length)
|
positionCount = 3
|
||||||
|
}
|
||||||
|
pos = end
|
||||||
|
case 1:
|
||||||
|
if len(data)-pos < 8 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pos += 8
|
||||||
|
case 5:
|
||||||
|
if len(data)-pos < 4 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
value := math.Float32frombits(binary.LittleEndian.Uint32(data[pos:]))
|
||||||
|
if fieldNum == 6 && positionCount < len(positionValues) {
|
||||||
|
positionValues[positionCount] = value
|
||||||
|
positionCount++
|
||||||
|
} else if fieldNum == 7 {
|
||||||
|
volumeAdjustment = value
|
||||||
|
}
|
||||||
|
pos += 4
|
||||||
default:
|
default:
|
||||||
return // unknown wire type, bail
|
return // invalid wire type
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if positionCount == len(positionValues) {
|
||||||
|
position = &positionValues
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +186,7 @@ const (
|
|||||||
|
|
||||||
// cryptState15 implements Mumble 1.5 native UDP encryption.
|
// cryptState15 implements Mumble 1.5 native UDP encryption.
|
||||||
type cryptState15 struct {
|
type cryptState15 struct {
|
||||||
|
mu sync.Mutex
|
||||||
key [16]byte
|
key [16]byte
|
||||||
encryptIV [16]byte
|
encryptIV [16]byte
|
||||||
decryptIV [16]byte
|
decryptIV [16]byte
|
||||||
@@ -170,6 +197,8 @@ type cryptState15 struct {
|
|||||||
// setup15 initializes 1.5-style crypto from CryptSetup.
|
// setup15 initializes 1.5-style crypto from CryptSetup.
|
||||||
// clientNonce → encryptIV, serverNonce → decryptIV.
|
// clientNonce → encryptIV, serverNonce → decryptIV.
|
||||||
func (cs *cryptState15) setup15(key, clientNonce, serverNonce []byte) error {
|
func (cs *cryptState15) setup15(key, clientNonce, serverNonce []byte) error {
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
if len(key) != 16 || len(clientNonce) != 16 || len(serverNonce) != 16 {
|
if len(key) != 16 || len(clientNonce) != 16 || len(serverNonce) != 16 {
|
||||||
return errors.New("gumble: invalid crypto key/nonce")
|
return errors.New("gumble: invalid crypto key/nonce")
|
||||||
}
|
}
|
||||||
@@ -178,10 +207,12 @@ func (cs *cryptState15) setup15(key, clientNonce, serverNonce []byte) error {
|
|||||||
copy(cs.decryptIV[:], serverNonce)
|
copy(cs.decryptIV[:], serverNonce)
|
||||||
cs.initialized = true
|
cs.initialized = true
|
||||||
|
|
||||||
log.Info("cryptState15 setup: key=%s encryptIV=%s decryptIV=%s",
|
if log.Enabled(log.LevelDebug) {
|
||||||
|
log.Debug("cryptState15 setup: key=%s encryptIV=%s decryptIV=%s",
|
||||||
hex.EncodeToString(cs.key[:]),
|
hex.EncodeToString(cs.key[:]),
|
||||||
hex.EncodeToString(cs.encryptIV[:]),
|
hex.EncodeToString(cs.encryptIV[:]),
|
||||||
hex.EncodeToString(cs.decryptIV[:]))
|
hex.EncodeToString(cs.decryptIV[:]))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -189,6 +220,8 @@ func (cs *cryptState15) setup15(key, clientNonce, serverNonce []byte) error {
|
|||||||
// encrypt15 encrypts plaintext for Mumble 1.5 native UDP.
|
// encrypt15 encrypts plaintext for Mumble 1.5 native UDP.
|
||||||
// Returns [iv_byte(1)][tag(3)][ciphertext].
|
// Returns [iv_byte(1)][tag(3)][ciphertext].
|
||||||
func (cs *cryptState15) encrypt15(plaintext []byte) ([]byte, error) {
|
func (cs *cryptState15) encrypt15(plaintext []byte) ([]byte, error) {
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
if !cs.initialized {
|
if !cs.initialized {
|
||||||
return nil, errors.New("gumble: crypto not initialized")
|
return nil, errors.New("gumble: crypto not initialized")
|
||||||
}
|
}
|
||||||
@@ -210,6 +243,8 @@ func (cs *cryptState15) encrypt15(plaintext []byte) ([]byte, error) {
|
|||||||
// decrypt15 decrypts a Mumble 1.5 native UDP packet.
|
// decrypt15 decrypts a Mumble 1.5 native UDP packet.
|
||||||
// Matches wumble's decrypt: advances IV when decrypt_iv[0]+1 == iv_byte.
|
// Matches wumble's decrypt: advances IV when decrypt_iv[0]+1 == iv_byte.
|
||||||
func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) {
|
func (cs *cryptState15) decrypt15(packet []byte) ([]byte, error) {
|
||||||
|
cs.mu.Lock()
|
||||||
|
defer cs.mu.Unlock()
|
||||||
if !cs.initialized {
|
if !cs.initialized {
|
||||||
return nil, errors.New("gumble: crypto not initialized")
|
return nil, errors.New("gumble: crypto not initialized")
|
||||||
}
|
}
|
||||||
@@ -309,8 +344,6 @@ func backupIV(iv []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// OCB variant for Mumble 1.5 native UDP.
|
// OCB variant for Mumble 1.5 native UDP.
|
||||||
// Matches the implementation in Wumble's crypt_state.cr.
|
// Matches the implementation in Wumble's crypt_state.cr.
|
||||||
@@ -480,70 +513,46 @@ func xorBytes(dst, a, b []byte) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// setUDP15Crypto installs per-client 1.5 UDP crypto state from CryptSetup.
|
||||||
// Global 1.5 crypto state.
|
func (c *Client) setUDP15Crypto(key, clientNonce, serverNonce []byte) error {
|
||||||
// ---------------------------------------------------------------------------
|
outbound := &cryptState15{}
|
||||||
|
if err := outbound.setup15(key, clientNonce, serverNonce); err != nil {
|
||||||
var udp15Client *cryptState15
|
return err
|
||||||
var udp15Server *cryptState15
|
|
||||||
|
|
||||||
// SetUDP15Crypto installs the 1.5 native UDP crypto state from CryptSetup.
|
|
||||||
// udp15Client: encrypts with client_nonce (outbound).
|
|
||||||
// udp15Server: decrypts with server_nonce (inbound).
|
|
||||||
func SetUDP15Crypto(key, clientNonce, serverNonce []byte) {
|
|
||||||
csClient := &cryptState15{}
|
|
||||||
if err := csClient.setup15(key, clientNonce, serverNonce); err != nil {
|
|
||||||
log.Error("SetUDP15Crypto client: %v", err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
udp15Client = csClient
|
inbound := &cryptState15{}
|
||||||
|
// Server-to-client packets use serverNonce as decryptIV.
|
||||||
// Server state: we only use decryptIV (to decrypt server→client packets).
|
if err := inbound.setup15(key, clientNonce, serverNonce); err != nil {
|
||||||
// The server encrypts with server_nonce, so our decryptIV must be server_nonce.
|
return err
|
||||||
csServer := &cryptState15{}
|
}
|
||||||
copy(csServer.key[:], key)
|
c.udpWriteMu.Lock()
|
||||||
copy(csServer.decryptIV[:], serverNonce)
|
c.udpMu.Lock()
|
||||||
csServer.initialized = true
|
c.udpCryptoOut = outbound
|
||||||
udp15Server = csServer
|
c.udpCryptoIn = inbound
|
||||||
|
c.udpFrameNumber = 0
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
c.udpWriteMu.Unlock()
|
||||||
log.Info("Mumble 1.5 native UDP crypto initialized")
|
log.Info("Mumble 1.5 native UDP crypto initialized")
|
||||||
}
|
return nil
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// Frame number tracking.
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
var udp15FrameNumber uint32
|
|
||||||
|
|
||||||
// nextFrameNumber returns the next frame number for outbound 1.5 UDP audio.
|
|
||||||
func nextFrameNumber() uint32 {
|
|
||||||
n := udp15FrameNumber
|
|
||||||
udp15FrameNumber++
|
|
||||||
return n
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// WriteAudioUDP15 writes an encrypted audio packet using Mumble 1.5 native UDP.
|
// WriteAudioUDP15 writes an encrypted audio packet using Mumble 1.5 native UDP.
|
||||||
// Returns true if sent, false if TCP should be used.
|
// Returns true if sent, false if TCP should be used.
|
||||||
func (c *Client) WriteAudioUDP15(data []byte, final bool) (bool, error) {
|
func (c *Client) WriteAudioUDP15(target uint32, data []byte, final bool) (bool, error) {
|
||||||
cs := udp15Client
|
// Encryption and socket writes must remain ordered: otherwise a later
|
||||||
if cs == nil || !cs.initialized {
|
// packet can reach the server before the packet with the preceding IV.
|
||||||
|
c.udpWriteMu.Lock()
|
||||||
|
defer c.udpWriteMu.Unlock()
|
||||||
|
c.udpMu.Lock()
|
||||||
|
cs, udpConn := c.udpCryptoOut, c.udpConn
|
||||||
|
frameNum := c.udpFrameNumber
|
||||||
|
c.udpFrameNumber++
|
||||||
|
c.udpMu.Unlock()
|
||||||
|
if cs == nil || udpConn == nil {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Snapshot udpConn to avoid race with disconnect cleanup nil'ing it.
|
payload := append([]byte{0x00}, encodeUDPAudio(target, frameNum, data, final)...)
|
||||||
udpConn := c.udpConn
|
|
||||||
if udpConn == nil {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
frameNum := nextFrameNumber()
|
|
||||||
|
|
||||||
// Build MumbleUDP.Audio: type byte 0x00 + protobuf.
|
|
||||||
// Matches reference client: 0x00 + target=0 + frame_number + opus_data [+ is_terminator]
|
|
||||||
payload := append([]byte{0x00}, encodeUDPAudio(0, frameNum, data, final)...)
|
|
||||||
|
|
||||||
// Encrypt with 1.5 format.
|
|
||||||
encrypted, err := cs.encrypt15(payload)
|
encrypted, err := cs.encrypt15(payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("UDP15 encrypt failed: %v", err)
|
log.Error("UDP15 encrypt failed: %v", err)
|
||||||
@@ -562,21 +571,20 @@ func (c *Client) WriteAudioUDP15(data []byte, final bool) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleUDPPacket15 processes an incoming Mumble 1.5 native UDP packet.
|
// HandleUDPPacket15 processes an incoming Mumble 1.5 native UDP packet.
|
||||||
var firstUDP15Recv bool
|
|
||||||
|
|
||||||
func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
|
func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
|
||||||
if len(packet) < udp15HeaderSize {
|
if len(packet) < udp15HeaderSize {
|
||||||
log.Warn("UDP15 #%d: packet too short (%d bytes)", pktNum, len(packet))
|
log.Warn("UDP15 #%d: packet too short (%d bytes)", pktNum, len(packet))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !firstUDP15Recv {
|
if !c.udpFirstRecv.Swap(true) && log.Enabled(log.LevelDebug) {
|
||||||
firstUDP15Recv = true
|
log.Debug("UDP15 #%d: first packet received! hex=%s", pktNum, hex.EncodeToString(packet))
|
||||||
log.Info("UDP15 #%d: first packet received! hex=%s", pktNum, hex.EncodeToString(packet))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cs := udp15Server
|
c.udpMu.RLock()
|
||||||
if cs == nil || !cs.initialized {
|
cs := c.udpCryptoIn
|
||||||
|
c.udpMu.RUnlock()
|
||||||
|
if cs == nil {
|
||||||
log.Warn("UDP15 #%d: crypto not initialized", pktNum)
|
log.Warn("UDP15 #%d: crypto not initialized", pktNum)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -587,7 +595,7 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("UDP15 #%d: decrypt OK, plaintext_len=%d", pktNum, len(plaintext))
|
log.Debug("UDP15 #%d: decrypt OK, plaintext_len=%d", pktNum, len(plaintext))
|
||||||
|
|
||||||
// Check type byte (0x00 = Audio, 0x01 = Ping)
|
// Check type byte (0x00 = Audio, 0x01 = Ping)
|
||||||
if len(plaintext) < 1 {
|
if len(plaintext) < 1 {
|
||||||
@@ -604,8 +612,8 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
|
|||||||
|
|
||||||
// MumbleUDP.Audio protobuf format (1.5 native).
|
// MumbleUDP.Audio protobuf format (1.5 native).
|
||||||
if msgType == 0x00 {
|
if msgType == 0x00 {
|
||||||
session, frameNum, opusData, terminator := decodeUDPAudio(plaintext)
|
session, frameNum, opusData, terminator, context, position, volumeAdjustment := decodeUDPAudio(plaintext)
|
||||||
c.dispatchOpus15(pktNum, session, int64(frameNum), opusData, terminator)
|
c.dispatchOpus15(pktNum, session, int64(frameNum), opusData, terminator, context, position, volumeAdjustment)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -620,7 +628,7 @@ func (c *Client) HandleUDPPacket15(packet []byte, pktNum uint64) {
|
|||||||
|
|
||||||
// dispatchOpus15 processes a decoded MumbleUDP.Audio frame and dispatches
|
// dispatchOpus15 processes a decoded MumbleUDP.Audio frame and dispatches
|
||||||
// the decoded PCM to audio listeners.
|
// the decoded PCM to audio listeners.
|
||||||
func (c *Client) dispatchOpus15(pktNum uint64, session uint32, frameNum int64, opusData []byte, terminator bool) {
|
func (c *Client) dispatchOpus15(pktNum uint64, session uint32, frameNum int64, opusData []byte, terminator bool, context uint32, position *[3]float32, volumeAdjustment float32) {
|
||||||
if len(opusData) == 0 && !terminator {
|
if len(opusData) == 0 && !terminator {
|
||||||
log.Info("UDP15 #%d: no opus data (session=%d frame=%d), skipping", pktNum, session, frameNum)
|
log.Info("UDP15 #%d: no opus data (session=%d frame=%d), skipping", pktNum, session, frameNum)
|
||||||
return
|
return
|
||||||
@@ -654,7 +662,7 @@ func (c *Client) dispatchOpus15(pktNum uint64, session uint32, frameNum int64, o
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.decodeAndDispatch(pktNum, user, decoder, frameNum, opusData)
|
c.decodeAndDispatch(pktNum, user, decoder, frameNum, opusData, context, position, volumeAdjustment)
|
||||||
}
|
}
|
||||||
|
|
||||||
// handleLegacyUDPVoice parses the legacy UDPVoice format (type byte 0x80)
|
// handleLegacyUDPVoice parses the legacy UDPVoice format (type byte 0x80)
|
||||||
@@ -699,11 +707,11 @@ func (c *Client) handleLegacyUDPVoice(pktNum uint64, data []byte) {
|
|||||||
log.Info("UDP15 #%d: legacy voice session=%d seq=%d opus_len=%d term=%v",
|
log.Info("UDP15 #%d: legacy voice session=%d seq=%d opus_len=%d term=%v",
|
||||||
pktNum, session, seq, len(opusData), terminator)
|
pktNum, session, seq, len(opusData), terminator)
|
||||||
|
|
||||||
c.dispatchOpus15(pktNum, uint32(session), seq, opusData, terminator)
|
c.dispatchOpus15(pktNum, uint32(session), seq, opusData, terminator, 0, nil, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// decodeAndDispatch decodes an Opus frame and dispatches PCM to audio listeners.
|
// decodeAndDispatch decodes an Opus frame and dispatches PCM to audio listeners.
|
||||||
func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecoder, frameNum int64, opusData []byte) {
|
func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecoder, frameNum int64, opusData []byte, context uint32, position *[3]float32, volumeAdjustment float32) {
|
||||||
// Detect sequence gaps.
|
// Detect sequence gaps.
|
||||||
if user.audioSequenceValid {
|
if user.audioSequenceValid {
|
||||||
gap := frameNum - user.audioSequence
|
gap := frameNum - user.audioSequence
|
||||||
@@ -732,14 +740,19 @@ func (c *Client) decodeAndDispatch(pktNum uint64, user *User, decoder AudioDecod
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("UDP15 #%d: Opus OK for %s, pcm_samples=%d", pktNum, user.Name, len(pcm))
|
log.Debug("UDP15 #%d: Opus OK for %s, pcm_samples=%d", pktNum, user.Name, len(pcm))
|
||||||
|
|
||||||
event := AudioPacket{
|
event := AudioPacket{
|
||||||
Client: c,
|
Client: c,
|
||||||
Sender: user,
|
Sender: user,
|
||||||
Target: &VoiceTarget{ID: 0},
|
Target: &VoiceTarget{ID: context},
|
||||||
Sequence: frameNum,
|
Sequence: frameNum,
|
||||||
AudioBuffer: AudioBuffer(pcm),
|
AudioBuffer: AudioBuffer(pcm),
|
||||||
|
VolumeAdjustment: volumeAdjustment,
|
||||||
|
}
|
||||||
|
if position != nil {
|
||||||
|
event.HasPosition = true
|
||||||
|
event.X, event.Y, event.Z = position[0], position[1], position[2]
|
||||||
}
|
}
|
||||||
c.dispatchAudio(user, &event)
|
c.dispatchAudio(user, &event)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package gumble
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -140,13 +142,13 @@ func TestUDPAudioProtobuf(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
encoded := encodeUDPAudio(tt.session, tt.frameNumber, tt.opusData, tt.terminator)
|
encoded := encodeUDPAudio(uint32(tt.session), uint64(tt.frameNumber), tt.opusData, tt.terminator)
|
||||||
session, frameNum, opusData, terminator := decodeUDPAudio(encoded)
|
session, frameNum, opusData, terminator, _, _, _ := decodeUDPAudio(encoded)
|
||||||
|
|
||||||
if session != tt.session {
|
if session != 0 {
|
||||||
t.Errorf("session: got %d, want %d", session, tt.session)
|
t.Errorf("outbound packet unexpectedly contains session %d", session)
|
||||||
}
|
}
|
||||||
if frameNum != tt.frameNumber {
|
if frameNum != uint64(tt.frameNumber) {
|
||||||
t.Errorf("frameNumber: got %d, want %d", frameNum, tt.frameNumber)
|
t.Errorf("frameNumber: got %d, want %d", frameNum, tt.frameNumber)
|
||||||
}
|
}
|
||||||
if !bytes.Equal(opusData, tt.opusData) {
|
if !bytes.Equal(opusData, tt.opusData) {
|
||||||
@@ -158,6 +160,39 @@ func TestUDPAudioProtobuf(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUDPAudioProtobufIncomingFields(t *testing.T) {
|
||||||
|
var packet bytes.Buffer
|
||||||
|
writeVarint := func(v uint64) {
|
||||||
|
var buf [10]byte
|
||||||
|
n := pbEncodeVarint(buf[:], v)
|
||||||
|
packet.Write(buf[:n])
|
||||||
|
}
|
||||||
|
writeVarint(2<<3 | 0) // context
|
||||||
|
writeVarint(3)
|
||||||
|
writeVarint(3<<3 | 0) // sender_session
|
||||||
|
writeVarint(123)
|
||||||
|
writeVarint(4<<3 | 0) // frame_number
|
||||||
|
writeVarint(1 << 32)
|
||||||
|
writeVarint(5<<3 | 2) // opus_data
|
||||||
|
writeVarint(2)
|
||||||
|
packet.Write([]byte{0xaa, 0xbb})
|
||||||
|
writeVarint(6<<3 | 2) // packed positional_data
|
||||||
|
writeVarint(12)
|
||||||
|
for _, f := range []uint32{math.Float32bits(1), math.Float32bits(2), math.Float32bits(3)} {
|
||||||
|
var buf [4]byte
|
||||||
|
binary.LittleEndian.PutUint32(buf[:], f)
|
||||||
|
packet.Write(buf[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
session, frame, opusData, terminator, context, position, _ := decodeUDPAudio(packet.Bytes())
|
||||||
|
if session != 123 || frame != 1<<32 || !bytes.Equal(opusData, []byte{0xaa, 0xbb}) || terminator || context != 3 {
|
||||||
|
t.Fatalf("decoded unexpected audio: session=%d frame=%d opus=%x terminator=%v context=%d", session, frame, opusData, terminator, context)
|
||||||
|
}
|
||||||
|
if position == nil || *position != [3]float32{1, 2, 3} {
|
||||||
|
t.Fatalf("position = %v, want [1 2 3]", position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func mustDecodeHex(s string) []byte {
|
func mustDecodeHex(s string) []byte {
|
||||||
b, err := hex.DecodeString(s)
|
b, err := hex.DecodeString(s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -536,7 +536,7 @@ func (s *Stream) processAudioPacket(packet *gumble.AudioPacket, user *gumble.Use
|
|||||||
rawPtr := 0
|
rawPtr := 0
|
||||||
if isStereo {
|
if isStereo {
|
||||||
// Process stereo samples as pairs
|
// Process stereo samples as pairs
|
||||||
for i := 0; i < samples*2; i += 2 {
|
for i := 0; i < samples; i += 2 {
|
||||||
// Process left channel with saturation protection
|
// Process left channel with saturation protection
|
||||||
sample := packet.AudioBuffer[i]
|
sample := packet.AudioBuffer[i]
|
||||||
if boost > 1 {
|
if boost > 1 {
|
||||||
|
|||||||
+39
-25
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -40,10 +41,16 @@ type Logger interface {
|
|||||||
Log(level Level, format string, args ...interface{})
|
Log(level Level, format string, args ...interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
type loggerState struct {
|
||||||
mu sync.Mutex
|
logger Logger
|
||||||
logger Logger = &nopLogger{}
|
level Level
|
||||||
)
|
}
|
||||||
|
|
||||||
|
var logger atomic.Pointer[loggerState]
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
logger.Store(&loggerState{logger: &nopLogger{}, level: LevelError + 1})
|
||||||
|
}
|
||||||
|
|
||||||
type nopLogger struct{}
|
type nopLogger struct{}
|
||||||
|
|
||||||
@@ -51,13 +58,20 @@ func (n *nopLogger) Log(level Level, format string, args ...interface{}) {}
|
|||||||
|
|
||||||
// SetLogger sets the destination for log messages. Pass nil to disable.
|
// SetLogger sets the destination for log messages. Pass nil to disable.
|
||||||
func SetLogger(l Logger) {
|
func SetLogger(l Logger) {
|
||||||
mu.Lock()
|
state := &loggerState{logger: l, level: LevelDebug}
|
||||||
defer mu.Unlock()
|
|
||||||
if l == nil {
|
if l == nil {
|
||||||
logger = &nopLogger{}
|
state.logger = &nopLogger{}
|
||||||
} else {
|
state.level = LevelError + 1
|
||||||
logger = l
|
} else if writer, ok := l.(*WriterLogger); ok {
|
||||||
|
state.level = writer.level
|
||||||
}
|
}
|
||||||
|
logger.Store(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled reports whether messages at level will be emitted. Callers should
|
||||||
|
// use it to avoid computing expensive log arguments when logging is disabled.
|
||||||
|
func Enabled(level Level) bool {
|
||||||
|
return level >= logger.Load().level
|
||||||
}
|
}
|
||||||
|
|
||||||
// WriterLogger is a simple Logger that writes to an io.Writer.
|
// WriterLogger is a simple Logger that writes to an io.Writer.
|
||||||
@@ -88,29 +102,29 @@ func (wl *WriterLogger) Log(level Level, format string, args ...interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Debug(format string, args ...interface{}) {
|
func Debug(format string, args ...interface{}) {
|
||||||
mu.Lock()
|
state := logger.Load()
|
||||||
l := logger
|
if LevelDebug >= state.level {
|
||||||
mu.Unlock()
|
state.logger.Log(LevelDebug, format, args...)
|
||||||
l.Log(LevelDebug, format, args...)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Info(format string, args ...interface{}) {
|
func Info(format string, args ...interface{}) {
|
||||||
mu.Lock()
|
state := logger.Load()
|
||||||
l := logger
|
if LevelInfo >= state.level {
|
||||||
mu.Unlock()
|
state.logger.Log(LevelInfo, format, args...)
|
||||||
l.Log(LevelInfo, format, args...)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Warn(format string, args ...interface{}) {
|
func Warn(format string, args ...interface{}) {
|
||||||
mu.Lock()
|
state := logger.Load()
|
||||||
l := logger
|
if LevelWarn >= state.level {
|
||||||
mu.Unlock()
|
state.logger.Log(LevelWarn, format, args...)
|
||||||
l.Log(LevelWarn, format, args...)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Error(format string, args ...interface{}) {
|
func Error(format string, args ...interface{}) {
|
||||||
mu.Lock()
|
state := logger.Load()
|
||||||
l := logger
|
if LevelError >= state.level {
|
||||||
mu.Unlock()
|
state.logger.Log(LevelError, format, args...)
|
||||||
l.Log(LevelError, format, args...)
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user