feat: refactor client into reusable ts3client library

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-15 22:06:35 +01:00
parent 7878ad3d5b
commit 02318b1490
10 changed files with 1050 additions and 47 deletions

View File

@@ -2,6 +2,7 @@ package client
import (
"encoding/binary"
"fmt"
"log"
"go-ts/pkg/protocol"
@@ -66,7 +67,7 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
// Only process Opus packets
if codec != CodecOpusVoice && codec != CodecOpusMusic {
log.Printf("Received non-Opus voice packet (Codec=%d). Ignoring echo.", codec)
log.Printf("Received non-Opus voice packet (Codec=%d). Ignoring.", codec)
return
}
@@ -97,26 +98,32 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
}
pcm = pcm[:n*channels]
if n != 960 {
log.Printf("WARNING: Unusual Opus frame size: %d samples (expected 960 for 20ms)", n)
// 3. Emit audio event instead of auto-echo
c.emitEvent("audio", map[string]any{
"senderID": vid,
"codec": int(codec),
"pcm": pcm,
"channels": channels,
})
}
// SendVoice sends PCM audio data to the server
// PCM must be 48kHz, 960 samples for 20ms frame (mono)
func (c *Client) SendVoice(pcm []int16) error {
if c.Conn == nil {
return fmt.Errorf("not connected")
}
// 3. Process PCM: Reduce Volume (divide by 4)
for i := range pcm {
pcm[i] = pcm[i] / 10
}
channels := 1
codec := uint8(CodecOpusVoice)
// 4. Get or Create Encoder
// Get or Create Encoder
if c.VoiceEncoder == nil {
var err error
app := opus.AppVoIP
if channels == 2 {
app = opus.AppAudio
}
encoder, err := opus.NewEncoder(48000, channels, app)
if err != nil {
log.Printf("Failed to create Opus encoder: %v", err)
return
return fmt.Errorf("failed to create Opus encoder: %w", err)
}
// Optimize Quality
@@ -126,31 +133,28 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
c.VoiceEncoder = encoder
}
// 5. Encode PCM to Opus
// Encode PCM to Opus
encoded := make([]byte, 1024)
nEnc, err := c.VoiceEncoder.Encode(pcm, encoded)
if err != nil {
log.Printf("Opus encode error: %v", err)
return
return fmt.Errorf("opus encode error: %w", err)
}
encoded = encoded[:nEnc]
// log.Printf("Voice Processed (CGO): VID=%d, In=%d bytes, PCM=%d samples, Out=%d bytes", vid, len(voiceData), n, nEnc)
// 6. Build echo packet (Client -> Server)
// Build voice packet (Client -> Server)
// Payload format: [VId(2)] [Codec(1)] [Data...]
echoData := make([]byte, 2+1+len(encoded))
voiceData := make([]byte, 2+1+len(encoded))
c.VoicePacketID++ // Increment counter before using it
// Correctly set VId in Payload to be the Sequence Number (not ClientID)
binary.BigEndian.PutUint16(echoData[0:2], c.VoicePacketID)
echoData[2] = codec
copy(echoData[3:], encoded)
// Set VId in Payload to be the Sequence Number (not ClientID)
binary.BigEndian.PutUint16(voiceData[0:2], c.VoicePacketID)
voiceData[2] = codec
copy(voiceData[3:], encoded)
echoPkt := protocol.NewPacket(protocol.PacketTypeVoice, echoData)
echoPkt.Header.PacketID = c.VoicePacketID
echoPkt.Header.ClientID = c.ClientID
pkt := protocol.NewPacket(protocol.PacketTypeVoice, voiceData)
pkt.Header.PacketID = c.VoicePacketID
pkt.Header.ClientID = c.ClientID
// Encrypt voice packet
if c.Handshake != nil && len(c.Handshake.SharedIV) > 0 {
@@ -159,27 +163,26 @@ func (c *Client) handleVoice(pkt *protocol.Packet) {
SharedMac: c.Handshake.SharedMac,
GenerationID: 0,
}
key, nonce := crypto.GenerateKeyNonce(&echoPkt.Header, true)
key, nonce := crypto.GenerateKeyNonce(&pkt.Header, true)
meta := make([]byte, 5)
binary.BigEndian.PutUint16(meta[0:2], echoPkt.Header.PacketID)
binary.BigEndian.PutUint16(meta[2:4], echoPkt.Header.ClientID)
meta[4] = echoPkt.Header.Type
binary.BigEndian.PutUint16(meta[0:2], pkt.Header.PacketID)
binary.BigEndian.PutUint16(meta[2:4], pkt.Header.ClientID)
meta[4] = pkt.Header.Type
encData, mac, err := protocol.EncryptEAX(key, nonce, meta, echoPkt.Data)
encData, mac, err := protocol.EncryptEAX(key, nonce, meta, pkt.Data)
if err != nil {
log.Printf("Voice encryption failed: %v", err)
return
return fmt.Errorf("voice encryption failed: %w", err)
}
echoPkt.Data = encData
copy(echoPkt.Header.MAC[:], mac)
pkt.Data = encData
copy(pkt.Header.MAC[:], mac)
} else {
if c.Handshake != nil && len(c.Handshake.SharedMac) > 0 {
copy(echoPkt.Header.MAC[:], c.Handshake.SharedMac)
copy(pkt.Header.MAC[:], c.Handshake.SharedMac)
} else {
echoPkt.Header.MAC = protocol.HandshakeMac
pkt.Header.MAC = protocol.HandshakeMac
}
}
c.Conn.SendPacket(echoPkt)
return c.Conn.SendPacket(pkt)
}