feat(tui): show voice activity with green color and speaker icon when users talk
This commit is contained in:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user