mirror of https://github.com/jetkvm/kvm.git
245 lines
7.4 KiB
Go
245 lines
7.4 KiB
Go
package audio
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/coder/websocket/wsjson"
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// AudioEventType represents different types of audio events
|
|
type AudioEventType string
|
|
|
|
const (
|
|
AudioEventMuteChanged AudioEventType = "audio-mute-changed"
|
|
AudioEventMicrophoneState AudioEventType = "microphone-state-changed"
|
|
AudioEventDeviceChanged AudioEventType = "audio-device-changed"
|
|
)
|
|
|
|
// AudioEvent represents a WebSocket audio event
|
|
type AudioEvent struct {
|
|
Type AudioEventType `json:"type"`
|
|
Data interface{} `json:"data"`
|
|
}
|
|
|
|
// AudioMuteData represents audio mute state change data
|
|
type AudioMuteData struct {
|
|
Muted bool `json:"muted"`
|
|
}
|
|
|
|
// MicrophoneStateData represents microphone state data
|
|
type MicrophoneStateData struct {
|
|
Running bool `json:"running"`
|
|
SessionActive bool `json:"session_active"`
|
|
}
|
|
|
|
// AudioDeviceChangedData represents audio device configuration change data
|
|
type AudioDeviceChangedData struct {
|
|
Enabled bool `json:"enabled"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
// AudioEventSubscriber represents a WebSocket connection subscribed to audio events
|
|
type AudioEventSubscriber struct {
|
|
conn *websocket.Conn
|
|
ctx context.Context
|
|
logger *zerolog.Logger
|
|
}
|
|
|
|
// AudioEventBroadcaster manages audio event subscriptions and broadcasting
|
|
type AudioEventBroadcaster struct {
|
|
subscribers map[string]*AudioEventSubscriber
|
|
mutex sync.RWMutex
|
|
logger *zerolog.Logger
|
|
}
|
|
|
|
var (
|
|
audioEventBroadcaster *AudioEventBroadcaster
|
|
audioEventOnce sync.Once
|
|
)
|
|
|
|
// initializeBroadcaster creates and initializes the audio event broadcaster
|
|
func initializeBroadcaster() {
|
|
l := logging.GetDefaultLogger().With().Str("component", "audio-events").Logger()
|
|
audioEventBroadcaster = &AudioEventBroadcaster{
|
|
subscribers: make(map[string]*AudioEventSubscriber),
|
|
logger: &l,
|
|
}
|
|
}
|
|
|
|
// InitializeAudioEventBroadcaster initializes the global audio event broadcaster
|
|
func InitializeAudioEventBroadcaster() {
|
|
audioEventOnce.Do(initializeBroadcaster)
|
|
}
|
|
|
|
// GetAudioEventBroadcaster returns the singleton audio event broadcaster
|
|
func GetAudioEventBroadcaster() *AudioEventBroadcaster {
|
|
audioEventOnce.Do(initializeBroadcaster)
|
|
return audioEventBroadcaster
|
|
}
|
|
|
|
// Subscribe adds a WebSocket connection to receive audio events
|
|
func (aeb *AudioEventBroadcaster) Subscribe(connectionID string, conn *websocket.Conn, ctx context.Context, logger *zerolog.Logger) {
|
|
aeb.mutex.Lock()
|
|
defer aeb.mutex.Unlock()
|
|
|
|
// Check if there's already a subscription for this connectionID
|
|
if _, exists := aeb.subscribers[connectionID]; exists {
|
|
aeb.logger.Debug().Str("connectionID", connectionID).Msg("duplicate audio events subscription detected; replacing existing entry")
|
|
// Do NOT close the existing WebSocket connection here because it's shared
|
|
// with the signaling channel. Just replace the subscriber map entry.
|
|
delete(aeb.subscribers, connectionID)
|
|
}
|
|
|
|
aeb.subscribers[connectionID] = &AudioEventSubscriber{
|
|
conn: conn,
|
|
ctx: ctx,
|
|
logger: logger,
|
|
}
|
|
|
|
aeb.logger.Debug().Str("connectionID", connectionID).Msg("audio events subscription added")
|
|
|
|
// Send initial state to new subscriber
|
|
go aeb.sendInitialState(connectionID)
|
|
}
|
|
|
|
// Unsubscribe removes a WebSocket connection from audio events
|
|
func (aeb *AudioEventBroadcaster) Unsubscribe(connectionID string) {
|
|
aeb.mutex.Lock()
|
|
defer aeb.mutex.Unlock()
|
|
|
|
delete(aeb.subscribers, connectionID)
|
|
aeb.logger.Debug().Str("connectionID", connectionID).Msg("audio events subscription removed")
|
|
}
|
|
|
|
// BroadcastAudioMuteChanged broadcasts audio mute state changes
|
|
func (aeb *AudioEventBroadcaster) BroadcastAudioMuteChanged(muted bool) {
|
|
event := createAudioEvent(AudioEventMuteChanged, AudioMuteData{Muted: muted})
|
|
aeb.broadcast(event)
|
|
}
|
|
|
|
// BroadcastMicrophoneStateChanged broadcasts microphone state changes
|
|
func (aeb *AudioEventBroadcaster) BroadcastMicrophoneStateChanged(running, sessionActive bool) {
|
|
event := createAudioEvent(AudioEventMicrophoneState, MicrophoneStateData{
|
|
Running: running,
|
|
SessionActive: sessionActive,
|
|
})
|
|
aeb.broadcast(event)
|
|
}
|
|
|
|
// BroadcastAudioDeviceChanged broadcasts audio device configuration changes
|
|
func (aeb *AudioEventBroadcaster) BroadcastAudioDeviceChanged(enabled bool, reason string) {
|
|
event := createAudioEvent(AudioEventDeviceChanged, AudioDeviceChangedData{
|
|
Enabled: enabled,
|
|
Reason: reason,
|
|
})
|
|
aeb.broadcast(event)
|
|
}
|
|
|
|
// sendInitialState sends current audio state to a new subscriber
|
|
func (aeb *AudioEventBroadcaster) sendInitialState(connectionID string) {
|
|
aeb.mutex.RLock()
|
|
subscriber, exists := aeb.subscribers[connectionID]
|
|
aeb.mutex.RUnlock()
|
|
|
|
if !exists {
|
|
return
|
|
}
|
|
|
|
// Send current audio mute state
|
|
muteEvent := AudioEvent{
|
|
Type: AudioEventMuteChanged,
|
|
Data: AudioMuteData{Muted: IsAudioMuted()},
|
|
}
|
|
aeb.sendToSubscriber(subscriber, muteEvent)
|
|
|
|
// Send current microphone state using session provider
|
|
sessionProvider := GetSessionProvider()
|
|
sessionActive := sessionProvider.IsSessionActive()
|
|
var running bool
|
|
if sessionActive {
|
|
if inputManager := sessionProvider.GetAudioInputManager(); inputManager != nil {
|
|
running = inputManager.IsRunning()
|
|
}
|
|
}
|
|
|
|
micStateEvent := AudioEvent{
|
|
Type: AudioEventMicrophoneState,
|
|
Data: MicrophoneStateData{
|
|
Running: running,
|
|
SessionActive: sessionActive,
|
|
},
|
|
}
|
|
aeb.sendToSubscriber(subscriber, micStateEvent)
|
|
}
|
|
|
|
// createAudioEvent creates an AudioEvent
|
|
func createAudioEvent(eventType AudioEventType, data interface{}) AudioEvent {
|
|
return AudioEvent{
|
|
Type: eventType,
|
|
Data: data,
|
|
}
|
|
}
|
|
|
|
// broadcast sends an event to all subscribers
|
|
func (aeb *AudioEventBroadcaster) broadcast(event AudioEvent) {
|
|
aeb.mutex.RLock()
|
|
// Create a copy of subscribers to avoid holding the lock during sending
|
|
subscribersCopy := make(map[string]*AudioEventSubscriber)
|
|
for id, sub := range aeb.subscribers {
|
|
subscribersCopy[id] = sub
|
|
}
|
|
aeb.mutex.RUnlock()
|
|
|
|
// Track failed subscribers to remove them after sending
|
|
var failedSubscribers []string
|
|
|
|
// Send to all subscribers without holding the lock
|
|
for connectionID, subscriber := range subscribersCopy {
|
|
if !aeb.sendToSubscriber(subscriber, event) {
|
|
failedSubscribers = append(failedSubscribers, connectionID)
|
|
}
|
|
}
|
|
|
|
// Remove failed subscribers if any
|
|
if len(failedSubscribers) > 0 {
|
|
aeb.mutex.Lock()
|
|
for _, connectionID := range failedSubscribers {
|
|
delete(aeb.subscribers, connectionID)
|
|
aeb.logger.Warn().Str("connectionID", connectionID).Msg("removed failed audio events subscriber")
|
|
}
|
|
aeb.mutex.Unlock()
|
|
}
|
|
}
|
|
|
|
// sendToSubscriber sends an event to a specific subscriber
|
|
func (aeb *AudioEventBroadcaster) sendToSubscriber(subscriber *AudioEventSubscriber, event AudioEvent) bool {
|
|
// Check if subscriber context is already cancelled
|
|
if subscriber.ctx.Err() != nil {
|
|
return false
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(subscriber.ctx, time.Duration(GetConfig().EventTimeoutSeconds)*time.Second)
|
|
defer cancel()
|
|
|
|
err := wsjson.Write(ctx, subscriber.conn, event)
|
|
if err != nil {
|
|
// Don't log network errors for closed connections as warnings, they're expected
|
|
if strings.Contains(err.Error(), "use of closed network connection") ||
|
|
strings.Contains(err.Error(), "connection reset by peer") ||
|
|
strings.Contains(err.Error(), "context canceled") {
|
|
subscriber.logger.Debug().Err(err).Msg("websocket connection closed during audio event send")
|
|
} else {
|
|
subscriber.logger.Warn().Err(err).Msg("failed to send audio event to subscriber")
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|