feat(tui): show voice activity with green color and speaker icon when users talk

This commit is contained in:
Jose Luis Montañes Ojados
2026-01-16 19:50:44 +01:00
parent 13f444193d
commit c55ace0c00
4 changed files with 129 additions and 14 deletions

View File

@@ -29,6 +29,10 @@ type Client struct {
selfInfo *SelfInfo
channelsMu sync.RWMutex
clientsMu sync.RWMutex
// Voice activity tracking
talkingClients map[uint16]time.Time // ClientID -> last voice packet time
talkingMu sync.RWMutex
}
// New creates a new TeamSpeak client
@@ -51,11 +55,12 @@ func New(address string, config Config) *Client {
}
return &Client{
address: address,
config: config,
handlers: make(map[EventType][]any),
channels: make(map[uint64]*Channel),
clients: make(map[uint16]*ClientInfo),
address: address,
config: config,
handlers: make(map[EventType][]any),
channels: make(map[uint64]*Channel),
clients: make(map[uint16]*ClientInfo),
talkingClients: make(map[uint16]time.Time),
}
}
@@ -115,6 +120,10 @@ func (c *Client) emit(event EventType, data any) {
if fn, ok := h.(func(*ErrorEvent)); ok {
fn(data.(*ErrorEvent))
}
case EventTalkingStatus:
if fn, ok := h.(func(*TalkingStatusEvent)); ok {
fn(data.(*TalkingStatusEvent))
}
}
}
}
@@ -127,6 +136,9 @@ func (c *Client) Connect() error {
// Set event callback on internal client
c.internal.SetEventHandler(c.handleInternalEvent)
// Start talking timeout checker goroutine
go c.talkingTimeoutChecker()
log.Printf("Connecting to %s as %s...", c.address, c.config.Nickname)
err := c.internal.Connect(c.address)
@@ -140,6 +152,40 @@ func (c *Client) Connect() error {
return nil
}
// talkingTimeoutChecker runs in background and emits TalkingStatus=false when clients stop talking
func (c *Client) talkingTimeoutChecker() {
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
const talkingTimeout = 300 * time.Millisecond
for range ticker.C {
if !c.connected {
continue
}
now := time.Now()
var stoppedTalking []uint16
c.talkingMu.Lock()
for clientID, lastTime := range c.talkingClients {
if now.Sub(lastTime) > talkingTimeout {
stoppedTalking = append(stoppedTalking, clientID)
delete(c.talkingClients, clientID)
}
}
c.talkingMu.Unlock()
// Emit events for clients who stopped talking
for _, clientID := range stoppedTalking {
c.emit(EventTalkingStatus, &TalkingStatusEvent{
ClientID: clientID,
Talking: false,
})
}
}
}
// ConnectAsync connects in the background and returns immediately
func (c *Client) ConnectAsync() <-chan error {
errChan := make(chan error, 1)
@@ -321,8 +367,24 @@ func (c *Client) handleInternalEvent(eventType string, data map[string]any) {
}
case "audio":
senderID := getUint16(data, "senderID")
// Track talking status
c.talkingMu.Lock()
_, wasTalking := c.talkingClients[senderID]
c.talkingClients[senderID] = time.Now()
c.talkingMu.Unlock()
// Emit talking start event if this is a new speaker
if !wasTalking {
c.emit(EventTalkingStatus, &TalkingStatusEvent{
ClientID: senderID,
Talking: true,
})
}
c.emit(EventAudio, &AudioEvent{
SenderID: getUint16(data, "senderID"),
SenderID: senderID,
Codec: AudioCodec(getInt(data, "codec")),
PCM: getPCM(data, "pcm"),
Channels: getInt(data, "channels"),