Compare commits

..

No commits in common. "02acee0c75876bf9a4814aa4988982d211bf7202" and "6890f17a5429c7bf42b370194ae47d2247c6793c" have entirely different histories.

55 changed files with 3297 additions and 3232 deletions

View File

@ -22,14 +22,6 @@ func initAudioControlService() {
audio.SetCurrentSessionCallback(func() audio.AudioTrackWriter { audio.SetCurrentSessionCallback(func() audio.AudioTrackWriter {
return GetCurrentSessionAudioTrack() return GetCurrentSessionAudioTrack()
}) })
// Set up callback for audio relay to replace WebRTC audio track
audio.SetTrackReplacementCallback(func(newTrack audio.AudioTrackWriter) error {
if track, ok := newTrack.(*webrtc.TrackLocalStaticSample); ok {
return ReplaceCurrentSessionAudioTrack(track)
}
return nil
})
} }
} }
@ -100,60 +92,6 @@ func ConnectRelayToCurrentSession() error {
return nil return nil
} }
// ReplaceCurrentSessionAudioTrack replaces the audio track in the current WebRTC session
func ReplaceCurrentSessionAudioTrack(newTrack *webrtc.TrackLocalStaticSample) error {
if currentSession == nil {
return nil // No session to update
}
err := currentSession.ReplaceAudioTrack(newTrack)
if err != nil {
logger.Error().Err(err).Msg("failed to replace audio track in current session")
return err
}
logger.Info().Msg("successfully replaced audio track in current session")
return nil
}
// SetAudioQuality is a global helper to set audio output quality
func SetAudioQuality(quality audio.AudioQuality) error {
initAudioControlService()
audioControlService.SetAudioQuality(quality)
return nil
}
// SetMicrophoneQuality is a global helper to set microphone quality
func SetMicrophoneQuality(quality audio.AudioQuality) error {
initAudioControlService()
audioControlService.SetMicrophoneQuality(quality)
return nil
}
// GetAudioQualityPresets is a global helper to get available audio quality presets
func GetAudioQualityPresets() map[audio.AudioQuality]audio.AudioConfig {
initAudioControlService()
return audioControlService.GetAudioQualityPresets()
}
// GetMicrophoneQualityPresets is a global helper to get available microphone quality presets
func GetMicrophoneQualityPresets() map[audio.AudioQuality]audio.AudioConfig {
initAudioControlService()
return audioControlService.GetMicrophoneQualityPresets()
}
// GetCurrentAudioQuality is a global helper to get current audio quality configuration
func GetCurrentAudioQuality() audio.AudioConfig {
initAudioControlService()
return audioControlService.GetCurrentAudioQuality()
}
// GetCurrentMicrophoneQuality is a global helper to get current microphone quality configuration
func GetCurrentMicrophoneQuality() audio.AudioConfig {
initAudioControlService()
return audioControlService.GetCurrentMicrophoneQuality()
}
// handleAudioMute handles POST /audio/mute requests // handleAudioMute handles POST /audio/mute requests
func handleAudioMute(c *gin.Context) { func handleAudioMute(c *gin.Context) {
type muteReq struct { type muteReq struct {
@ -264,8 +202,10 @@ func handleAudioStatus(c *gin.Context) {
// handleAudioQuality handles GET requests for audio quality presets // handleAudioQuality handles GET requests for audio quality presets
func handleAudioQuality(c *gin.Context) { func handleAudioQuality(c *gin.Context) {
presets := GetAudioQualityPresets() initAudioControlService()
current := GetCurrentAudioQuality()
presets := audioControlService.GetAudioQualityPresets()
current := audioControlService.GetCurrentAudioQuality()
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"presets": presets, "presets": presets,
@ -284,24 +224,16 @@ func handleSetAudioQuality(c *gin.Context) {
return return
} }
// Check if audio output is active before attempting quality change initAudioControlService()
// This prevents race conditions where quality changes are attempted before initialization
if !IsAudioOutputActive() {
c.JSON(503, gin.H{"error": "audio output not active - please wait for initialization to complete"})
return
}
// Convert int to AudioQuality type // Convert int to AudioQuality type
quality := audio.AudioQuality(req.Quality) quality := audio.AudioQuality(req.Quality)
// Set the audio quality using global convenience function // Set the audio quality
if err := SetAudioQuality(quality); err != nil { audioControlService.SetAudioQuality(quality)
c.JSON(500, gin.H{"error": err.Error()})
return
}
// Return the updated configuration // Return the updated configuration
current := GetCurrentAudioQuality() current := audioControlService.GetCurrentAudioQuality()
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"success": true, "success": true,
"config": current, "config": current,
@ -310,9 +242,9 @@ func handleSetAudioQuality(c *gin.Context) {
// handleMicrophoneQuality handles GET requests for microphone quality presets // handleMicrophoneQuality handles GET requests for microphone quality presets
func handleMicrophoneQuality(c *gin.Context) { func handleMicrophoneQuality(c *gin.Context) {
presets := GetMicrophoneQualityPresets() initAudioControlService()
current := GetCurrentMicrophoneQuality() presets := audioControlService.GetMicrophoneQualityPresets()
current := audioControlService.GetCurrentMicrophoneQuality()
c.JSON(200, gin.H{ c.JSON(200, gin.H{
"presets": presets, "presets": presets,
"current": current, "current": current,
@ -326,22 +258,21 @@ func handleSetMicrophoneQuality(c *gin.Context) {
} }
if err := c.ShouldBindJSON(&req); err != nil { if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()}) c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return return
} }
initAudioControlService()
// Convert int to AudioQuality type // Convert int to AudioQuality type
quality := audio.AudioQuality(req.Quality) quality := audio.AudioQuality(req.Quality)
// Set the microphone quality using global convenience function // Set the microphone quality
if err := SetMicrophoneQuality(quality); err != nil { audioControlService.SetMicrophoneQuality(quality)
c.JSON(500, gin.H{"error": err.Error()})
return
}
// Return the updated configuration // Return the updated configuration
current := GetCurrentMicrophoneQuality() current := audioControlService.GetCurrentMicrophoneQuality()
c.JSON(200, gin.H{ c.JSON(http.StatusOK, gin.H{
"success": true, "success": true,
"config": current, "config": current,
}) })

View File

@ -57,25 +57,25 @@ type AdaptiveBufferConfig struct {
func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig { func DefaultAdaptiveBufferConfig() AdaptiveBufferConfig {
return AdaptiveBufferConfig{ return AdaptiveBufferConfig{
// Conservative buffer sizes for 256MB RAM constraint // Conservative buffer sizes for 256MB RAM constraint
MinBufferSize: Config.AdaptiveMinBufferSize, MinBufferSize: GetConfig().AdaptiveMinBufferSize,
MaxBufferSize: Config.AdaptiveMaxBufferSize, MaxBufferSize: GetConfig().AdaptiveMaxBufferSize,
DefaultBufferSize: Config.AdaptiveDefaultBufferSize, DefaultBufferSize: GetConfig().AdaptiveDefaultBufferSize,
// CPU thresholds optimized for single-core ARM Cortex A7 under load // CPU thresholds optimized for single-core ARM Cortex A7 under load
LowCPUThreshold: Config.LowCPUThreshold * 100, // Below 20% CPU LowCPUThreshold: GetConfig().LowCPUThreshold * 100, // Below 20% CPU
HighCPUThreshold: Config.HighCPUThreshold * 100, // Above 60% CPU (lowered to be more responsive) HighCPUThreshold: GetConfig().HighCPUThreshold * 100, // Above 60% CPU (lowered to be more responsive)
// Memory thresholds for 256MB total RAM // Memory thresholds for 256MB total RAM
LowMemoryThreshold: Config.LowMemoryThreshold * 100, // Below 35% memory usage LowMemoryThreshold: GetConfig().LowMemoryThreshold * 100, // Below 35% memory usage
HighMemoryThreshold: Config.HighMemoryThreshold * 100, // Above 75% memory usage (lowered for earlier response) HighMemoryThreshold: GetConfig().HighMemoryThreshold * 100, // Above 75% memory usage (lowered for earlier response)
// Latency targets // Latency targets
TargetLatency: Config.AdaptiveBufferTargetLatency, // Target 20ms latency TargetLatency: GetConfig().AdaptiveBufferTargetLatency, // Target 20ms latency
MaxLatency: Config.MaxLatencyThreshold, // Max acceptable latency MaxLatency: GetConfig().LatencyMonitorTarget, // Max acceptable latency
// Adaptation settings // Adaptation settings
AdaptationInterval: Config.BufferUpdateInterval, // Check every 500ms AdaptationInterval: GetConfig().BufferUpdateInterval, // Check every 500ms
SmoothingFactor: Config.SmoothingFactor, // Moderate responsiveness SmoothingFactor: GetConfig().SmoothingFactor, // Moderate responsiveness
} }
} }
@ -89,8 +89,9 @@ type AdaptiveBufferManager struct {
systemMemoryPercent int64 // System memory percentage * 100 (atomic) systemMemoryPercent int64 // System memory percentage * 100 (atomic)
adaptationCount int64 // Metrics tracking (atomic) adaptationCount int64 // Metrics tracking (atomic)
config AdaptiveBufferConfig config AdaptiveBufferConfig
logger zerolog.Logger logger zerolog.Logger
processMonitor *ProcessMonitor
// Control channels // Control channels
ctx context.Context ctx context.Context
@ -118,10 +119,10 @@ func NewAdaptiveBufferManager(config AdaptiveBufferConfig) *AdaptiveBufferManage
currentOutputBufferSize: int64(config.DefaultBufferSize), currentOutputBufferSize: int64(config.DefaultBufferSize),
config: config, config: config,
logger: logger, logger: logger,
processMonitor: GetProcessMonitor(),
ctx: ctx, ctx: ctx,
cancel: cancel, cancel: cancel,
lastAdaptation: time.Now(), lastAdaptation: time.Now(),
} }
} }
@ -151,42 +152,6 @@ func (abm *AdaptiveBufferManager) GetOutputBufferSize() int {
// UpdateLatency updates the current latency measurement // UpdateLatency updates the current latency measurement
func (abm *AdaptiveBufferManager) UpdateLatency(latency time.Duration) { func (abm *AdaptiveBufferManager) UpdateLatency(latency time.Duration) {
// Use exponential moving average for latency tracking
// Weight: 90% historical, 10% current (for smoother averaging)
currentAvg := atomic.LoadInt64(&abm.averageLatency)
newLatencyNs := latency.Nanoseconds()
if currentAvg == 0 {
// First measurement
atomic.StoreInt64(&abm.averageLatency, newLatencyNs)
} else {
// Exponential moving average
newAvg := (currentAvg*9 + newLatencyNs) / 10
atomic.StoreInt64(&abm.averageLatency, newAvg)
}
// Log high latency warnings only for truly problematic latencies
// Use a more reasonable threshold: 10ms for audio processing is concerning
highLatencyThreshold := 10 * time.Millisecond
if latency > highLatencyThreshold {
abm.logger.Debug().
Dur("latency_ms", latency/time.Millisecond).
Dur("threshold_ms", highLatencyThreshold/time.Millisecond).
Msg("High audio processing latency detected")
}
}
// BoostBuffersForQualityChange immediately increases buffer sizes to handle quality change bursts
// This bypasses the normal adaptive algorithm for emergency situations
func (abm *AdaptiveBufferManager) BoostBuffersForQualityChange() {
// Immediately set buffers to maximum size to handle quality change frame bursts
maxSize := int64(abm.config.MaxBufferSize)
atomic.StoreInt64(&abm.currentInputBufferSize, maxSize)
atomic.StoreInt64(&abm.currentOutputBufferSize, maxSize)
abm.logger.Info().
Int("buffer_size", int(maxSize)).
Msg("Boosted buffers to maximum size for quality change")
} }
// adaptationLoop is the main loop that adjusts buffer sizes // adaptationLoop is the main loop that adjusts buffer sizes
@ -234,9 +199,30 @@ func (abm *AdaptiveBufferManager) adaptationLoop() {
// The algorithm runs periodically and only applies changes when the adaptation interval // The algorithm runs periodically and only applies changes when the adaptation interval
// has elapsed, preventing excessive adjustments that could destabilize the audio pipeline. // has elapsed, preventing excessive adjustments that could destabilize the audio pipeline.
func (abm *AdaptiveBufferManager) adaptBufferSizes() { func (abm *AdaptiveBufferManager) adaptBufferSizes() {
// Use fixed system metrics for stability // Collect current system metrics
systemCPU := 50.0 // Assume moderate CPU usage metrics := abm.processMonitor.GetCurrentMetrics()
systemMemory := 60.0 // Assume moderate memory usage if len(metrics) == 0 {
return // No metrics available
}
// Calculate system-wide CPU and memory usage
totalCPU := 0.0
totalMemory := 0.0
processCount := 0
for _, metric := range metrics {
totalCPU += metric.CPUPercent
totalMemory += metric.MemoryPercent
processCount++
}
if processCount == 0 {
return
}
// Store system metrics atomically
systemCPU := totalCPU // Total CPU across all monitored processes
systemMemory := totalMemory / float64(processCount) // Average memory usage
atomic.StoreInt64(&abm.systemCPUPercent, int64(systemCPU*100)) atomic.StoreInt64(&abm.systemCPUPercent, int64(systemCPU*100))
atomic.StoreInt64(&abm.systemMemoryPercent, int64(systemMemory*100)) atomic.StoreInt64(&abm.systemMemoryPercent, int64(systemMemory*100))
@ -251,7 +237,7 @@ func (abm *AdaptiveBufferManager) adaptBufferSizes() {
latencyFactor := abm.calculateLatencyFactor(currentLatency) latencyFactor := abm.calculateLatencyFactor(currentLatency)
// Combine factors with weights (CPU has highest priority for KVM coexistence) // Combine factors with weights (CPU has highest priority for KVM coexistence)
combinedFactor := Config.CPUMemoryWeight*cpuFactor + Config.MemoryWeight*memoryFactor + Config.LatencyWeight*latencyFactor combinedFactor := GetConfig().CPUMemoryWeight*cpuFactor + GetConfig().MemoryWeight*memoryFactor + GetConfig().LatencyWeight*latencyFactor
// Apply adaptation with smoothing // Apply adaptation with smoothing
currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize)) currentInput := float64(atomic.LoadInt64(&abm.currentInputBufferSize))
@ -415,8 +401,8 @@ func (abm *AdaptiveBufferManager) GetStats() map[string]interface{} {
"input_buffer_size": abm.GetInputBufferSize(), "input_buffer_size": abm.GetInputBufferSize(),
"output_buffer_size": abm.GetOutputBufferSize(), "output_buffer_size": abm.GetOutputBufferSize(),
"average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6, "average_latency_ms": float64(atomic.LoadInt64(&abm.averageLatency)) / 1e6,
"system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / Config.PercentageMultiplier, "system_cpu_percent": float64(atomic.LoadInt64(&abm.systemCPUPercent)) / GetConfig().PercentageMultiplier,
"system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / Config.PercentageMultiplier, "system_memory_percent": float64(atomic.LoadInt64(&abm.systemMemoryPercent)) / GetConfig().PercentageMultiplier,
"adaptation_count": atomic.LoadInt64(&abm.adaptationCount), "adaptation_count": atomic.LoadInt64(&abm.adaptationCount),
"last_adaptation": lastAdaptation, "last_adaptation": lastAdaptation,
} }

View File

@ -82,16 +82,20 @@ type batchWriteResult struct {
// NewBatchAudioProcessor creates a new batch audio processor // NewBatchAudioProcessor creates a new batch audio processor
func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAudioProcessor { func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAudioProcessor {
// Get cached config to avoid GetConfig() calls
cache := GetCachedConfig()
cache.Update()
// Validate input parameters with minimal overhead // Validate input parameters with minimal overhead
if batchSize <= 0 || batchSize > 1000 { if batchSize <= 0 || batchSize > 1000 {
batchSize = Config.BatchProcessorFramesPerBatch batchSize = cache.BatchProcessorFramesPerBatch
} }
if batchDuration <= 0 { if batchDuration <= 0 {
batchDuration = Config.BatchProcessingDelay batchDuration = cache.BatchProcessingDelay
} }
// Use optimized queue sizes from configuration // Use optimized queue sizes from configuration
queueSize := Config.BatchProcessorMaxQueueSize queueSize := cache.BatchProcessorMaxQueueSize
if queueSize <= 0 { if queueSize <= 0 {
queueSize = batchSize * 2 // Fallback to double batch size queueSize = batchSize * 2 // Fallback to double batch size
} }
@ -100,7 +104,8 @@ func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAu
// Pre-allocate logger to avoid repeated allocations // Pre-allocate logger to avoid repeated allocations
logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger() logger := logging.GetDefaultLogger().With().Str("component", "batch-audio").Logger()
frameSize := Config.MinReadEncodeBuffer // Pre-calculate frame size to avoid repeated GetConfig() calls
frameSize := cache.GetMinReadEncodeBuffer()
if frameSize == 0 { if frameSize == 0 {
frameSize = 1500 // Safe fallback frameSize = 1500 // Safe fallback
} }
@ -115,11 +120,13 @@ func NewBatchAudioProcessor(batchSize int, batchDuration time.Duration) *BatchAu
writeQueue: make(chan batchWriteRequest, queueSize), writeQueue: make(chan batchWriteRequest, queueSize),
readBufPool: &sync.Pool{ readBufPool: &sync.Pool{
New: func() interface{} { New: func() interface{} {
// Use pre-calculated frame size to avoid GetConfig() calls
return make([]byte, 0, frameSize) return make([]byte, 0, frameSize)
}, },
}, },
writeBufPool: &sync.Pool{ writeBufPool: &sync.Pool{
New: func() interface{} { New: func() interface{} {
// Use pre-calculated frame size to avoid GetConfig() calls
return make([]byte, 0, frameSize) return make([]byte, 0, frameSize)
}, },
}, },
@ -159,13 +166,17 @@ func (bap *BatchAudioProcessor) Stop() {
bap.cancel() bap.cancel()
// Wait for processing to complete // Wait for processing to complete
time.Sleep(bap.batchDuration + Config.BatchProcessingDelay) time.Sleep(bap.batchDuration + GetConfig().BatchProcessingDelay)
bap.logger.Info().Msg("batch audio processor stopped") bap.logger.Info().Msg("batch audio processor stopped")
} }
// BatchReadEncode performs batched audio read and encode operations // BatchReadEncode performs batched audio read and encode operations
func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) { func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
// Get cached config to avoid GetConfig() calls in hot path
cache := GetCachedConfig()
cache.Update()
// Validate buffer before processing // Validate buffer before processing
if err := ValidateBufferSize(len(buffer)); err != nil { if err := ValidateBufferSize(len(buffer)); err != nil {
// Only log validation errors in debug mode to reduce overhead // Only log validation errors in debug mode to reduce overhead
@ -210,7 +221,7 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
select { select {
case result := <-resultChan: case result := <-resultChan:
return result.length, result.err return result.length, result.err
case <-time.After(Config.BatchProcessorTimeout): case <-time.After(cache.BatchProcessingTimeout):
// Timeout, fallback to single operation // Timeout, fallback to single operation
// Use sampling to reduce atomic operations overhead // Use sampling to reduce atomic operations overhead
if atomic.LoadInt64(&bap.stats.SingleReads)%10 == 0 { if atomic.LoadInt64(&bap.stats.SingleReads)%10 == 0 {
@ -224,6 +235,10 @@ func (bap *BatchAudioProcessor) BatchReadEncode(buffer []byte) (int, error) {
// BatchDecodeWrite performs batched audio decode and write operations // BatchDecodeWrite performs batched audio decode and write operations
// This is the legacy version that uses a single buffer // This is the legacy version that uses a single buffer
func (bap *BatchAudioProcessor) BatchDecodeWrite(buffer []byte) (int, error) { func (bap *BatchAudioProcessor) BatchDecodeWrite(buffer []byte) (int, error) {
// Get cached config to avoid GetConfig() calls in hot path
cache := GetCachedConfig()
cache.Update()
// Validate buffer before processing // Validate buffer before processing
if err := ValidateBufferSize(len(buffer)); err != nil { if err := ValidateBufferSize(len(buffer)); err != nil {
// Only log validation errors in debug mode to reduce overhead // Only log validation errors in debug mode to reduce overhead
@ -268,7 +283,7 @@ func (bap *BatchAudioProcessor) BatchDecodeWrite(buffer []byte) (int, error) {
select { select {
case result := <-resultChan: case result := <-resultChan:
return result.length, result.err return result.length, result.err
case <-time.After(Config.BatchProcessorTimeout): case <-time.After(cache.BatchProcessingTimeout):
// Use sampling to reduce atomic operations overhead // Use sampling to reduce atomic operations overhead
if atomic.LoadInt64(&bap.stats.SingleWrites)%10 == 0 { if atomic.LoadInt64(&bap.stats.SingleWrites)%10 == 0 {
atomic.AddInt64(&bap.stats.SingleWrites, 10) atomic.AddInt64(&bap.stats.SingleWrites, 10)
@ -280,6 +295,10 @@ func (bap *BatchAudioProcessor) BatchDecodeWrite(buffer []byte) (int, error) {
// BatchDecodeWriteWithBuffers performs batched audio decode and write operations with separate opus and PCM buffers // BatchDecodeWriteWithBuffers performs batched audio decode and write operations with separate opus and PCM buffers
func (bap *BatchAudioProcessor) BatchDecodeWriteWithBuffers(opusData []byte, pcmBuffer []byte) (int, error) { func (bap *BatchAudioProcessor) BatchDecodeWriteWithBuffers(opusData []byte, pcmBuffer []byte) (int, error) {
// Get cached config to avoid GetConfig() calls in hot path
cache := GetCachedConfig()
cache.Update()
// Validate buffers before processing // Validate buffers before processing
if len(opusData) == 0 { if len(opusData) == 0 {
return 0, fmt.Errorf("empty opus data buffer") return 0, fmt.Errorf("empty opus data buffer")
@ -320,7 +339,7 @@ func (bap *BatchAudioProcessor) BatchDecodeWriteWithBuffers(opusData []byte, pcm
select { select {
case result := <-resultChan: case result := <-resultChan:
return result.length, result.err return result.length, result.err
case <-time.After(Config.BatchProcessorTimeout): case <-time.After(cache.BatchProcessingTimeout):
atomic.AddInt64(&bap.stats.SingleWrites, 1) atomic.AddInt64(&bap.stats.SingleWrites, 1)
atomic.AddInt64(&bap.stats.WriteFrames, 1) atomic.AddInt64(&bap.stats.WriteFrames, 1)
// Use the optimized function with separate buffers // Use the optimized function with separate buffers
@ -407,9 +426,11 @@ func (bap *BatchAudioProcessor) processBatchRead(batch []batchReadRequest) {
return return
} }
threadPinningThreshold := Config.BatchProcessorThreadPinningThreshold // Get cached config once - avoid repeated calls
cache := GetCachedConfig()
threadPinningThreshold := cache.BatchProcessorThreadPinningThreshold
if threadPinningThreshold == 0 { if threadPinningThreshold == 0 {
threadPinningThreshold = Config.MinBatchSizeForThreadPinning // Fallback threadPinningThreshold = cache.MinBatchSizeForThreadPinning // Fallback
} }
// Only pin to OS thread for large batches to reduce thread contention // Only pin to OS thread for large batches to reduce thread contention
@ -458,9 +479,11 @@ func (bap *BatchAudioProcessor) processBatchWrite(batch []batchWriteRequest) {
return return
} }
threadPinningThreshold := Config.BatchProcessorThreadPinningThreshold // Get cached config to avoid GetConfig() calls in hot path
cache := GetCachedConfig()
threadPinningThreshold := cache.BatchProcessorThreadPinningThreshold
if threadPinningThreshold == 0 { if threadPinningThreshold == 0 {
threadPinningThreshold = Config.MinBatchSizeForThreadPinning // Fallback threadPinningThreshold = cache.MinBatchSizeForThreadPinning // Fallback
} }
// Only pin to OS thread for large batches to reduce thread contention // Only pin to OS thread for large batches to reduce thread contention
@ -562,7 +585,11 @@ func GetBatchAudioProcessor() *BatchAudioProcessor {
// Initialize on first use // Initialize on first use
if atomic.CompareAndSwapInt32(&batchProcessorInitialized, 0, 1) { if atomic.CompareAndSwapInt32(&batchProcessorInitialized, 0, 1) {
processor := NewBatchAudioProcessor(Config.BatchProcessorFramesPerBatch, Config.BatchProcessorTimeout) // Get cached config to avoid GetConfig() calls
cache := GetCachedConfig()
cache.Update()
processor := NewBatchAudioProcessor(cache.BatchProcessorFramesPerBatch, cache.BatchProcessorTimeout)
atomic.StorePointer(&globalBatchProcessor, unsafe.Pointer(processor)) atomic.StorePointer(&globalBatchProcessor, unsafe.Pointer(processor))
return processor return processor
} }
@ -574,7 +601,8 @@ func GetBatchAudioProcessor() *BatchAudioProcessor {
} }
// Fallback: create a new processor (should rarely happen) // Fallback: create a new processor (should rarely happen)
return NewBatchAudioProcessor(Config.BatchProcessorFramesPerBatch, Config.BatchProcessorTimeout) config := GetConfig()
return NewBatchAudioProcessor(config.BatchProcessorFramesPerBatch, config.BatchProcessorTimeout)
} }
// EnableBatchAudioProcessing enables the global batch processor // EnableBatchAudioProcessing enables the global batch processor

View File

@ -1,331 +0,0 @@
//go:build cgo
package audio
import (
"errors"
"sync"
"sync/atomic"
"unsafe"
)
// BatchReferenceManager handles batch reference counting operations
// to reduce atomic operation overhead for high-frequency frame operations
type BatchReferenceManager struct {
// Batch operations queue
batchQueue chan batchRefOperation
workerPool chan struct{} // Worker pool semaphore
running int32
wg sync.WaitGroup
// Statistics
batchedOps int64
singleOps int64
batchSavings int64 // Number of atomic operations saved
}
type batchRefOperation struct {
frames []*ZeroCopyAudioFrame
operation refOperationType
resultCh chan batchRefResult
}
type refOperationType int
const (
refOpAddRef refOperationType = iota
refOpRelease
refOpMixed // For operations with mixed AddRef/Release
)
// Errors
var (
ErrUnsupportedOperation = errors.New("unsupported batch reference operation")
)
type batchRefResult struct {
finalReleases []bool // For Release operations, indicates which frames had final release
err error
}
// Global batch reference manager
var (
globalBatchRefManager *BatchReferenceManager
batchRefOnce sync.Once
)
// GetBatchReferenceManager returns the global batch reference manager
func GetBatchReferenceManager() *BatchReferenceManager {
batchRefOnce.Do(func() {
globalBatchRefManager = NewBatchReferenceManager()
globalBatchRefManager.Start()
})
return globalBatchRefManager
}
// NewBatchReferenceManager creates a new batch reference manager
func NewBatchReferenceManager() *BatchReferenceManager {
return &BatchReferenceManager{
batchQueue: make(chan batchRefOperation, 256), // Buffered for high throughput
workerPool: make(chan struct{}, 4), // 4 workers for parallel processing
}
}
// Start starts the batch reference manager workers
func (brm *BatchReferenceManager) Start() {
if !atomic.CompareAndSwapInt32(&brm.running, 0, 1) {
return // Already running
}
// Start worker goroutines
for i := 0; i < cap(brm.workerPool); i++ {
brm.wg.Add(1)
go brm.worker()
}
}
// Stop stops the batch reference manager
func (brm *BatchReferenceManager) Stop() {
if !atomic.CompareAndSwapInt32(&brm.running, 1, 0) {
return // Already stopped
}
close(brm.batchQueue)
brm.wg.Wait()
}
// worker processes batch reference operations
func (brm *BatchReferenceManager) worker() {
defer brm.wg.Done()
for op := range brm.batchQueue {
brm.processBatchOperation(op)
}
}
// processBatchOperation processes a batch of reference operations
func (brm *BatchReferenceManager) processBatchOperation(op batchRefOperation) {
result := batchRefResult{}
switch op.operation {
case refOpAddRef:
// Batch AddRef operations
for _, frame := range op.frames {
if frame != nil {
atomic.AddInt32(&frame.refCount, 1)
}
}
atomic.AddInt64(&brm.batchedOps, int64(len(op.frames)))
atomic.AddInt64(&brm.batchSavings, int64(len(op.frames)-1)) // Saved ops vs individual calls
case refOpRelease:
// Batch Release operations
result.finalReleases = make([]bool, len(op.frames))
for i, frame := range op.frames {
if frame != nil {
newCount := atomic.AddInt32(&frame.refCount, -1)
if newCount == 0 {
result.finalReleases[i] = true
// Return to pool if pooled
if frame.pooled {
globalZeroCopyPool.Put(frame)
}
}
}
}
atomic.AddInt64(&brm.batchedOps, int64(len(op.frames)))
atomic.AddInt64(&brm.batchSavings, int64(len(op.frames)-1))
case refOpMixed:
// Handle mixed operations (not implemented in this version)
result.err = ErrUnsupportedOperation
}
// Send result back
if op.resultCh != nil {
op.resultCh <- result
close(op.resultCh)
}
}
// BatchAddRef performs AddRef on multiple frames in a single batch
func (brm *BatchReferenceManager) BatchAddRef(frames []*ZeroCopyAudioFrame) error {
if len(frames) == 0 {
return nil
}
// For small batches, use direct operations to avoid overhead
if len(frames) <= 2 {
for _, frame := range frames {
if frame != nil {
frame.AddRef()
}
}
atomic.AddInt64(&brm.singleOps, int64(len(frames)))
return nil
}
// Use batch processing for larger sets
if atomic.LoadInt32(&brm.running) == 0 {
// Fallback to individual operations if batch manager not running
for _, frame := range frames {
if frame != nil {
frame.AddRef()
}
}
atomic.AddInt64(&brm.singleOps, int64(len(frames)))
return nil
}
resultCh := make(chan batchRefResult, 1)
op := batchRefOperation{
frames: frames,
operation: refOpAddRef,
resultCh: resultCh,
}
select {
case brm.batchQueue <- op:
// Wait for completion
<-resultCh
return nil
default:
// Queue full, fallback to individual operations
for _, frame := range frames {
if frame != nil {
frame.AddRef()
}
}
atomic.AddInt64(&brm.singleOps, int64(len(frames)))
return nil
}
}
// BatchRelease performs Release on multiple frames in a single batch
// Returns a slice indicating which frames had their final reference released
func (brm *BatchReferenceManager) BatchRelease(frames []*ZeroCopyAudioFrame) ([]bool, error) {
if len(frames) == 0 {
return nil, nil
}
// For small batches, use direct operations
if len(frames) <= 2 {
finalReleases := make([]bool, len(frames))
for i, frame := range frames {
if frame != nil {
finalReleases[i] = frame.Release()
}
}
atomic.AddInt64(&brm.singleOps, int64(len(frames)))
return finalReleases, nil
}
// Use batch processing for larger sets
if atomic.LoadInt32(&brm.running) == 0 {
// Fallback to individual operations
finalReleases := make([]bool, len(frames))
for i, frame := range frames {
if frame != nil {
finalReleases[i] = frame.Release()
}
}
atomic.AddInt64(&brm.singleOps, int64(len(frames)))
return finalReleases, nil
}
resultCh := make(chan batchRefResult, 1)
op := batchRefOperation{
frames: frames,
operation: refOpRelease,
resultCh: resultCh,
}
select {
case brm.batchQueue <- op:
// Wait for completion
result := <-resultCh
return result.finalReleases, result.err
default:
// Queue full, fallback to individual operations
finalReleases := make([]bool, len(frames))
for i, frame := range frames {
if frame != nil {
finalReleases[i] = frame.Release()
}
}
atomic.AddInt64(&brm.singleOps, int64(len(frames)))
return finalReleases, nil
}
}
// GetStats returns batch reference counting statistics
func (brm *BatchReferenceManager) GetStats() (batchedOps, singleOps, savings int64) {
return atomic.LoadInt64(&brm.batchedOps),
atomic.LoadInt64(&brm.singleOps),
atomic.LoadInt64(&brm.batchSavings)
}
// Convenience functions for global batch reference manager
// BatchAddRefFrames performs batch AddRef on multiple frames
func BatchAddRefFrames(frames []*ZeroCopyAudioFrame) error {
return GetBatchReferenceManager().BatchAddRef(frames)
}
// BatchReleaseFrames performs batch Release on multiple frames
func BatchReleaseFrames(frames []*ZeroCopyAudioFrame) ([]bool, error) {
return GetBatchReferenceManager().BatchRelease(frames)
}
// GetBatchReferenceStats returns global batch reference statistics
func GetBatchReferenceStats() (batchedOps, singleOps, savings int64) {
return GetBatchReferenceManager().GetStats()
}
// ZeroCopyFrameSlice provides utilities for working with slices of zero-copy frames
type ZeroCopyFrameSlice []*ZeroCopyAudioFrame
// AddRefAll performs batch AddRef on all frames in the slice
func (zfs ZeroCopyFrameSlice) AddRefAll() error {
return BatchAddRefFrames(zfs)
}
// ReleaseAll performs batch Release on all frames in the slice
func (zfs ZeroCopyFrameSlice) ReleaseAll() ([]bool, error) {
return BatchReleaseFrames(zfs)
}
// FilterNonNil returns a new slice with only non-nil frames
func (zfs ZeroCopyFrameSlice) FilterNonNil() ZeroCopyFrameSlice {
filtered := make(ZeroCopyFrameSlice, 0, len(zfs))
for _, frame := range zfs {
if frame != nil {
filtered = append(filtered, frame)
}
}
return filtered
}
// Len returns the number of frames in the slice
func (zfs ZeroCopyFrameSlice) Len() int {
return len(zfs)
}
// Get returns the frame at the specified index
func (zfs ZeroCopyFrameSlice) Get(index int) *ZeroCopyAudioFrame {
if index < 0 || index >= len(zfs) {
return nil
}
return zfs[index]
}
// UnsafePointers returns unsafe pointers for all frames (for CGO batch operations)
func (zfs ZeroCopyFrameSlice) UnsafePointers() []unsafe.Pointer {
pointers := make([]unsafe.Pointer, len(zfs))
for i, frame := range zfs {
if frame != nil {
pointers[i] = frame.UnsafePointer()
}
}
return pointers
}

View File

@ -1,415 +0,0 @@
//go:build cgo
package audio
import (
"sync"
"sync/atomic"
"time"
)
// BatchZeroCopyProcessor handles batch operations on zero-copy audio frames
// with optimized reference counting and memory management
type BatchZeroCopyProcessor struct {
// Configuration
maxBatchSize int
batchTimeout time.Duration
processingDelay time.Duration
adaptiveThreshold float64
// Processing queues
readEncodeQueue chan *batchZeroCopyRequest
decodeWriteQueue chan *batchZeroCopyRequest
// Worker management
workerPool chan struct{}
running int32
wg sync.WaitGroup
// Statistics
batchedFrames int64
singleFrames int64
batchSavings int64
processingTimeUs int64
adaptiveHits int64
adaptiveMisses int64
}
type batchZeroCopyRequest struct {
frames []*ZeroCopyAudioFrame
operation batchZeroCopyOperation
resultCh chan batchZeroCopyResult
timestamp time.Time
}
type batchZeroCopyOperation int
const (
batchOpReadEncode batchZeroCopyOperation = iota
batchOpDecodeWrite
batchOpMixed
)
type batchZeroCopyResult struct {
encodedData [][]byte // For read-encode operations
processedCount int // Number of successfully processed frames
err error
}
// Global batch zero-copy processor
var (
globalBatchZeroCopyProcessor *BatchZeroCopyProcessor
batchZeroCopyOnce sync.Once
)
// GetBatchZeroCopyProcessor returns the global batch zero-copy processor
func GetBatchZeroCopyProcessor() *BatchZeroCopyProcessor {
batchZeroCopyOnce.Do(func() {
globalBatchZeroCopyProcessor = NewBatchZeroCopyProcessor()
globalBatchZeroCopyProcessor.Start()
})
return globalBatchZeroCopyProcessor
}
// NewBatchZeroCopyProcessor creates a new batch zero-copy processor
func NewBatchZeroCopyProcessor() *BatchZeroCopyProcessor {
cache := Config
return &BatchZeroCopyProcessor{
maxBatchSize: cache.BatchProcessorFramesPerBatch,
batchTimeout: cache.BatchProcessorTimeout,
processingDelay: cache.BatchProcessingDelay,
adaptiveThreshold: cache.BatchProcessorAdaptiveThreshold,
readEncodeQueue: make(chan *batchZeroCopyRequest, cache.BatchProcessorMaxQueueSize),
decodeWriteQueue: make(chan *batchZeroCopyRequest, cache.BatchProcessorMaxQueueSize),
workerPool: make(chan struct{}, 4), // 4 workers for parallel processing
}
}
// Start starts the batch zero-copy processor workers
func (bzcp *BatchZeroCopyProcessor) Start() {
if !atomic.CompareAndSwapInt32(&bzcp.running, 0, 1) {
return // Already running
}
// Start worker goroutines for read-encode operations
for i := 0; i < cap(bzcp.workerPool)/2; i++ {
bzcp.wg.Add(1)
go bzcp.readEncodeWorker()
}
// Start worker goroutines for decode-write operations
for i := 0; i < cap(bzcp.workerPool)/2; i++ {
bzcp.wg.Add(1)
go bzcp.decodeWriteWorker()
}
}
// Stop stops the batch zero-copy processor
func (bzcp *BatchZeroCopyProcessor) Stop() {
if !atomic.CompareAndSwapInt32(&bzcp.running, 1, 0) {
return // Already stopped
}
close(bzcp.readEncodeQueue)
close(bzcp.decodeWriteQueue)
bzcp.wg.Wait()
}
// readEncodeWorker processes batch read-encode operations
func (bzcp *BatchZeroCopyProcessor) readEncodeWorker() {
defer bzcp.wg.Done()
for req := range bzcp.readEncodeQueue {
bzcp.processBatchReadEncode(req)
}
}
// decodeWriteWorker processes batch decode-write operations
func (bzcp *BatchZeroCopyProcessor) decodeWriteWorker() {
defer bzcp.wg.Done()
for req := range bzcp.decodeWriteQueue {
bzcp.processBatchDecodeWrite(req)
}
}
// processBatchReadEncode processes a batch of read-encode operations
func (bzcp *BatchZeroCopyProcessor) processBatchReadEncode(req *batchZeroCopyRequest) {
startTime := time.Now()
result := batchZeroCopyResult{}
// Batch AddRef all frames first
err := BatchAddRefFrames(req.frames)
if err != nil {
result.err = err
if req.resultCh != nil {
req.resultCh <- result
close(req.resultCh)
}
return
}
// Process frames using existing batch read-encode logic
encodedData, err := BatchReadEncode(len(req.frames))
if err != nil {
// Batch release frames on error
if _, releaseErr := BatchReleaseFrames(req.frames); releaseErr != nil {
// Log release error but preserve original error
_ = releaseErr
}
result.err = err
} else {
result.encodedData = encodedData
result.processedCount = len(encodedData)
// Batch release frames after successful processing
if _, releaseErr := BatchReleaseFrames(req.frames); releaseErr != nil {
// Log release error but don't fail the operation
_ = releaseErr
}
}
// Update statistics
atomic.AddInt64(&bzcp.batchedFrames, int64(len(req.frames)))
atomic.AddInt64(&bzcp.batchSavings, int64(len(req.frames)-1))
atomic.AddInt64(&bzcp.processingTimeUs, time.Since(startTime).Microseconds())
// Send result back
if req.resultCh != nil {
req.resultCh <- result
close(req.resultCh)
}
}
// processBatchDecodeWrite processes a batch of decode-write operations
func (bzcp *BatchZeroCopyProcessor) processBatchDecodeWrite(req *batchZeroCopyRequest) {
startTime := time.Now()
result := batchZeroCopyResult{}
// Batch AddRef all frames first
err := BatchAddRefFrames(req.frames)
if err != nil {
result.err = err
if req.resultCh != nil {
req.resultCh <- result
close(req.resultCh)
}
return
}
// Extract data from zero-copy frames for batch processing
frameData := make([][]byte, len(req.frames))
for i, frame := range req.frames {
if frame != nil {
// Get data from zero-copy frame
frameData[i] = frame.Data()[:frame.Length()]
}
}
// Process frames using existing batch decode-write logic
err = BatchDecodeWrite(frameData)
if err != nil {
result.err = err
} else {
result.processedCount = len(req.frames)
}
// Batch release frames
if _, releaseErr := BatchReleaseFrames(req.frames); releaseErr != nil {
// Log release error but don't override processing error
_ = releaseErr
}
// Update statistics
atomic.AddInt64(&bzcp.batchedFrames, int64(len(req.frames)))
atomic.AddInt64(&bzcp.batchSavings, int64(len(req.frames)-1))
atomic.AddInt64(&bzcp.processingTimeUs, time.Since(startTime).Microseconds())
// Send result back
if req.resultCh != nil {
req.resultCh <- result
close(req.resultCh)
}
}
// BatchReadEncodeZeroCopy performs batch read-encode on zero-copy frames
func (bzcp *BatchZeroCopyProcessor) BatchReadEncodeZeroCopy(frames []*ZeroCopyAudioFrame) ([][]byte, error) {
if len(frames) == 0 {
return nil, nil
}
// For small batches, use direct operations to avoid overhead
if len(frames) <= 2 {
atomic.AddInt64(&bzcp.singleFrames, int64(len(frames)))
return bzcp.processSingleReadEncode(frames)
}
// Use adaptive threshold to determine batch vs single processing
batchedFrames := atomic.LoadInt64(&bzcp.batchedFrames)
singleFrames := atomic.LoadInt64(&bzcp.singleFrames)
totalFrames := batchedFrames + singleFrames
if totalFrames > 100 { // Only apply adaptive logic after some samples
batchRatio := float64(batchedFrames) / float64(totalFrames)
if batchRatio < bzcp.adaptiveThreshold {
// Batch processing not effective, use single processing
atomic.AddInt64(&bzcp.adaptiveMisses, 1)
atomic.AddInt64(&bzcp.singleFrames, int64(len(frames)))
return bzcp.processSingleReadEncode(frames)
}
atomic.AddInt64(&bzcp.adaptiveHits, 1)
}
// Use batch processing
if atomic.LoadInt32(&bzcp.running) == 0 {
// Fallback to single processing if batch processor not running
atomic.AddInt64(&bzcp.singleFrames, int64(len(frames)))
return bzcp.processSingleReadEncode(frames)
}
resultCh := make(chan batchZeroCopyResult, 1)
req := &batchZeroCopyRequest{
frames: frames,
operation: batchOpReadEncode,
resultCh: resultCh,
timestamp: time.Now(),
}
select {
case bzcp.readEncodeQueue <- req:
// Wait for completion
result := <-resultCh
return result.encodedData, result.err
default:
// Queue full, fallback to single processing
atomic.AddInt64(&bzcp.singleFrames, int64(len(frames)))
return bzcp.processSingleReadEncode(frames)
}
}
// BatchDecodeWriteZeroCopy performs batch decode-write on zero-copy frames
func (bzcp *BatchZeroCopyProcessor) BatchDecodeWriteZeroCopy(frames []*ZeroCopyAudioFrame) error {
if len(frames) == 0 {
return nil
}
// For small batches, use direct operations
if len(frames) <= 2 {
atomic.AddInt64(&bzcp.singleFrames, int64(len(frames)))
return bzcp.processSingleDecodeWrite(frames)
}
// Use adaptive threshold
batchedFrames := atomic.LoadInt64(&bzcp.batchedFrames)
singleFrames := atomic.LoadInt64(&bzcp.singleFrames)
totalFrames := batchedFrames + singleFrames
if totalFrames > 100 {
batchRatio := float64(batchedFrames) / float64(totalFrames)
if batchRatio < bzcp.adaptiveThreshold {
atomic.AddInt64(&bzcp.adaptiveMisses, 1)
atomic.AddInt64(&bzcp.singleFrames, int64(len(frames)))
return bzcp.processSingleDecodeWrite(frames)
}
atomic.AddInt64(&bzcp.adaptiveHits, 1)
}
// Use batch processing
if atomic.LoadInt32(&bzcp.running) == 0 {
atomic.AddInt64(&bzcp.singleFrames, int64(len(frames)))
return bzcp.processSingleDecodeWrite(frames)
}
resultCh := make(chan batchZeroCopyResult, 1)
req := &batchZeroCopyRequest{
frames: frames,
operation: batchOpDecodeWrite,
resultCh: resultCh,
timestamp: time.Now(),
}
select {
case bzcp.decodeWriteQueue <- req:
// Wait for completion
result := <-resultCh
return result.err
default:
// Queue full, fallback to single processing
atomic.AddInt64(&bzcp.singleFrames, int64(len(frames)))
return bzcp.processSingleDecodeWrite(frames)
}
}
// processSingleReadEncode processes frames individually for read-encode
func (bzcp *BatchZeroCopyProcessor) processSingleReadEncode(frames []*ZeroCopyAudioFrame) ([][]byte, error) {
// Extract data and use existing batch processing
frameData := make([][]byte, 0, len(frames))
for _, frame := range frames {
if frame != nil {
frame.AddRef()
frameData = append(frameData, frame.Data()[:frame.Length()])
}
}
// Use existing batch read-encode
result, err := BatchReadEncode(len(frameData))
// Release frames
for _, frame := range frames {
if frame != nil {
frame.Release()
}
}
return result, err
}
// processSingleDecodeWrite processes frames individually for decode-write
func (bzcp *BatchZeroCopyProcessor) processSingleDecodeWrite(frames []*ZeroCopyAudioFrame) error {
// Extract data and use existing batch processing
frameData := make([][]byte, 0, len(frames))
for _, frame := range frames {
if frame != nil {
frame.AddRef()
frameData = append(frameData, frame.Data()[:frame.Length()])
}
}
// Use existing batch decode-write
err := BatchDecodeWrite(frameData)
// Release frames
for _, frame := range frames {
if frame != nil {
frame.Release()
}
}
return err
}
// GetBatchZeroCopyStats returns batch zero-copy processing statistics
func (bzcp *BatchZeroCopyProcessor) GetBatchZeroCopyStats() (batchedFrames, singleFrames, savings, processingTimeUs, adaptiveHits, adaptiveMisses int64) {
return atomic.LoadInt64(&bzcp.batchedFrames),
atomic.LoadInt64(&bzcp.singleFrames),
atomic.LoadInt64(&bzcp.batchSavings),
atomic.LoadInt64(&bzcp.processingTimeUs),
atomic.LoadInt64(&bzcp.adaptiveHits),
atomic.LoadInt64(&bzcp.adaptiveMisses)
}
// Convenience functions for global batch zero-copy processor
// BatchReadEncodeZeroCopyFrames performs batch read-encode on zero-copy frames
func BatchReadEncodeZeroCopyFrames(frames []*ZeroCopyAudioFrame) ([][]byte, error) {
return GetBatchZeroCopyProcessor().BatchReadEncodeZeroCopy(frames)
}
// BatchDecodeWriteZeroCopyFrames performs batch decode-write on zero-copy frames
func BatchDecodeWriteZeroCopyFrames(frames []*ZeroCopyAudioFrame) error {
return GetBatchZeroCopyProcessor().BatchDecodeWriteZeroCopy(frames)
}
// GetGlobalBatchZeroCopyStats returns global batch zero-copy processing statistics
func GetGlobalBatchZeroCopyStats() (batchedFrames, singleFrames, savings, processingTimeUs, adaptiveHits, adaptiveMisses int64) {
return GetBatchZeroCopyProcessor().GetBatchZeroCopyStats()
}

View File

@ -14,15 +14,12 @@ import (
/* /*
#cgo CFLAGS: -I$HOME/.jetkvm/audio-libs/alsa-lib-$ALSA_VERSION/include -I$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/include -I$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/celt #cgo CFLAGS: -I$HOME/.jetkvm/audio-libs/alsa-lib-$ALSA_VERSION/include -I$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/include -I$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/celt
#cgo LDFLAGS: -L$HOME/.jetkvm/audio-libs/alsa-lib-$ALSA_VERSION/src/.libs -lasound -L$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/.libs -lopus -lm -ldl -static #cgo LDFLAGS: -L$HOME/.jetkvm/audio-libs/alsa-lib-$ALSA_VERSION/src/.libs -lasound -L$HOME/.jetkvm/audio-libs/opus-$OPUS_VERSION/.libs -lopus -lm -ldl -static
#include <alsa/asoundlib.h> #include <alsa/asoundlib.h>
#include <opus.h> #include <opus.h>
#include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <unistd.h>
#include <errno.h> #include <errno.h>
#include <sys/mman.h> #include <unistd.h>
// C state for ALSA/Opus with safety flags // C state for ALSA/Opus with safety flags
static snd_pcm_t *pcm_handle = NULL; static snd_pcm_t *pcm_handle = NULL;
@ -30,33 +27,25 @@ static snd_pcm_t *pcm_playback_handle = NULL;
static OpusEncoder *encoder = NULL; static OpusEncoder *encoder = NULL;
static OpusDecoder *decoder = NULL; static OpusDecoder *decoder = NULL;
// Opus encoder settings - initialized from Go configuration // Opus encoder settings - initialized from Go configuration
static int opus_bitrate = 96000; // Will be set from Config.CGOOpusBitrate static int opus_bitrate = 96000; // Will be set from GetConfig().CGOOpusBitrate
static int opus_complexity = 3; // Will be set from Config.CGOOpusComplexity static int opus_complexity = 3; // Will be set from GetConfig().CGOOpusComplexity
static int opus_vbr = 1; // Will be set from Config.CGOOpusVBR static int opus_vbr = 1; // Will be set from GetConfig().CGOOpusVBR
static int opus_vbr_constraint = 1; // Will be set from Config.CGOOpusVBRConstraint static int opus_vbr_constraint = 1; // Will be set from GetConfig().CGOOpusVBRConstraint
static int opus_signal_type = 3; // Will be set from Config.CGOOpusSignalType static int opus_signal_type = 3; // Will be set from GetConfig().CGOOpusSignalType
static int opus_bandwidth = 1105; // OPUS_BANDWIDTH_WIDEBAND for compatibility (was 1101) static int opus_bandwidth = 1105; // OPUS_BANDWIDTH_WIDEBAND for compatibility (was 1101)
static int opus_dtx = 0; // Will be set from Config.CGOOpusDTX static int opus_dtx = 0; // Will be set from GetConfig().CGOOpusDTX
static int opus_lsb_depth = 16; // LSB depth for improved bit allocation on constrained hardware static int opus_lsb_depth = 16; // LSB depth for improved bit allocation on constrained hardware
static int sample_rate = 48000; // Will be set from Config.CGOSampleRate static int sample_rate = 48000; // Will be set from GetConfig().CGOSampleRate
static int channels = 2; // Will be set from Config.CGOChannels static int channels = 2; // Will be set from GetConfig().CGOChannels
static int frame_size = 960; // Will be set from Config.CGOFrameSize static int frame_size = 960; // Will be set from GetConfig().CGOFrameSize
static int max_packet_size = 1500; // Will be set from Config.CGOMaxPacketSize static int max_packet_size = 1500; // Will be set from GetConfig().CGOMaxPacketSize
static int sleep_microseconds = 1000; // Will be set from Config.CGOUsleepMicroseconds static int sleep_microseconds = 1000; // Will be set from GetConfig().CGOUsleepMicroseconds
static int max_attempts_global = 5; // Will be set from Config.CGOMaxAttempts static int max_attempts_global = 5; // Will be set from GetConfig().CGOMaxAttempts
static int max_backoff_us_global = 500000; // Will be set from Config.CGOMaxBackoffMicroseconds static int max_backoff_us_global = 500000; // Will be set from GetConfig().CGOMaxBackoffMicroseconds
// Hardware optimization flags for constrained environments // Hardware optimization flags for constrained environments
static int use_mmap_access = 0; // Disable MMAP for compatibility (was 1) static int use_mmap_access = 0; // Disable MMAP for compatibility (was 1)
static int optimized_buffer_size = 0; // Disable optimized buffer sizing for stability (was 1) static int optimized_buffer_size = 0; // Disable optimized buffer sizing for stability (was 1)
// C function declarations (implementations are below)
int jetkvm_audio_init();
void jetkvm_audio_close();
int jetkvm_audio_read_encode(void *opus_buf);
int jetkvm_audio_decode_write(void *opus_buf, int opus_size);
int jetkvm_audio_playback_init();
void jetkvm_audio_playback_close();
// Function to update constants from Go configuration // Function to update constants from Go configuration
void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint, void update_audio_constants(int bitrate, int complexity, int vbr, int vbr_constraint,
int signal_type, int bandwidth, int dtx, int lsb_depth, int sr, int ch, int signal_type, int bandwidth, int dtx, int lsb_depth, int sr, int ch,
@ -87,10 +76,8 @@ static volatile int playback_initialized = 0;
// Function to dynamically update Opus encoder parameters // Function to dynamically update Opus encoder parameters
int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint, int update_opus_encoder_params(int bitrate, int complexity, int vbr, int vbr_constraint,
int signal_type, int bandwidth, int dtx) { int signal_type, int bandwidth, int dtx) {
// This function works for both audio input and output encoder parameters if (!encoder || !capture_initialized) {
// Require either capture (output) or playback (input) initialization return -1; // Encoder not initialized
if (!encoder || (!capture_initialized && !playback_initialized)) {
return -1; // Audio encoder not initialized
} }
// Update the static variables // Update the static variables
@ -711,9 +698,9 @@ func cgoAudioInit() error {
C.int(cache.channels.Load()), C.int(cache.channels.Load()),
C.int(cache.frameSize.Load()), C.int(cache.frameSize.Load()),
C.int(cache.maxPacketSize.Load()), C.int(cache.maxPacketSize.Load()),
C.int(Config.CGOUsleepMicroseconds), C.int(GetConfig().CGOUsleepMicroseconds),
C.int(Config.CGOMaxAttempts), C.int(GetConfig().CGOMaxAttempts),
C.int(Config.CGOMaxBackoffMicroseconds), C.int(GetConfig().CGOMaxBackoffMicroseconds),
) )
result := C.jetkvm_audio_init() result := C.jetkvm_audio_init()
@ -728,6 +715,7 @@ func cgoAudioClose() {
} }
// AudioConfigCache provides a comprehensive caching system for audio configuration // AudioConfigCache provides a comprehensive caching system for audio configuration
// to minimize GetConfig() calls in the hot path
type AudioConfigCache struct { type AudioConfigCache struct {
// Atomic int64 fields MUST be first for ARM32 alignment (8-byte alignment required) // Atomic int64 fields MUST be first for ARM32 alignment (8-byte alignment required)
minFrameDuration atomic.Int64 // Store as nanoseconds minFrameDuration atomic.Int64 // Store as nanoseconds
@ -816,50 +804,52 @@ func (c *AudioConfigCache) Update() {
// Double-check after acquiring lock // Double-check after acquiring lock
if !c.initialized.Load() || time.Since(c.lastUpdate) > c.cacheExpiry { if !c.initialized.Load() || time.Since(c.lastUpdate) > c.cacheExpiry {
config := GetConfig() // Call GetConfig() only once
// Update atomic values for lock-free access - CGO values // Update atomic values for lock-free access - CGO values
c.minReadEncodeBuffer.Store(int32(Config.MinReadEncodeBuffer)) c.minReadEncodeBuffer.Store(int32(config.MinReadEncodeBuffer))
c.maxDecodeWriteBuffer.Store(int32(Config.MaxDecodeWriteBuffer)) c.maxDecodeWriteBuffer.Store(int32(config.MaxDecodeWriteBuffer))
c.maxPacketSize.Store(int32(Config.CGOMaxPacketSize)) c.maxPacketSize.Store(int32(config.CGOMaxPacketSize))
c.maxPCMBufferSize.Store(int32(Config.MaxPCMBufferSize)) c.maxPCMBufferSize.Store(int32(config.MaxPCMBufferSize))
c.opusBitrate.Store(int32(Config.CGOOpusBitrate)) c.opusBitrate.Store(int32(config.CGOOpusBitrate))
c.opusComplexity.Store(int32(Config.CGOOpusComplexity)) c.opusComplexity.Store(int32(config.CGOOpusComplexity))
c.opusVBR.Store(int32(Config.CGOOpusVBR)) c.opusVBR.Store(int32(config.CGOOpusVBR))
c.opusVBRConstraint.Store(int32(Config.CGOOpusVBRConstraint)) c.opusVBRConstraint.Store(int32(config.CGOOpusVBRConstraint))
c.opusSignalType.Store(int32(Config.CGOOpusSignalType)) c.opusSignalType.Store(int32(config.CGOOpusSignalType))
c.opusBandwidth.Store(int32(Config.CGOOpusBandwidth)) c.opusBandwidth.Store(int32(config.CGOOpusBandwidth))
c.opusDTX.Store(int32(Config.CGOOpusDTX)) c.opusDTX.Store(int32(config.CGOOpusDTX))
c.sampleRate.Store(int32(Config.CGOSampleRate)) c.sampleRate.Store(int32(config.CGOSampleRate))
c.channels.Store(int32(Config.CGOChannels)) c.channels.Store(int32(config.CGOChannels))
c.frameSize.Store(int32(Config.CGOFrameSize)) c.frameSize.Store(int32(config.CGOFrameSize))
// Update additional validation values // Update additional validation values
c.maxAudioFrameSize.Store(int32(Config.MaxAudioFrameSize)) c.maxAudioFrameSize.Store(int32(config.MaxAudioFrameSize))
c.maxChannels.Store(int32(Config.MaxChannels)) c.maxChannels.Store(int32(config.MaxChannels))
c.minFrameDuration.Store(int64(Config.MinFrameDuration)) c.minFrameDuration.Store(int64(config.MinFrameDuration))
c.maxFrameDuration.Store(int64(Config.MaxFrameDuration)) c.maxFrameDuration.Store(int64(config.MaxFrameDuration))
c.minOpusBitrate.Store(int32(Config.MinOpusBitrate)) c.minOpusBitrate.Store(int32(config.MinOpusBitrate))
c.maxOpusBitrate.Store(int32(Config.MaxOpusBitrate)) c.maxOpusBitrate.Store(int32(config.MaxOpusBitrate))
// Update batch processing related values // Update batch processing related values
c.BatchProcessingTimeout = 100 * time.Millisecond // Fixed timeout for batch processing c.BatchProcessingTimeout = 100 * time.Millisecond // Fixed timeout for batch processing
c.BatchProcessorFramesPerBatch = Config.BatchProcessorFramesPerBatch c.BatchProcessorFramesPerBatch = config.BatchProcessorFramesPerBatch
c.BatchProcessorTimeout = Config.BatchProcessorTimeout c.BatchProcessorTimeout = config.BatchProcessorTimeout
c.BatchProcessingDelay = Config.BatchProcessingDelay c.BatchProcessingDelay = config.BatchProcessingDelay
c.MinBatchSizeForThreadPinning = Config.MinBatchSizeForThreadPinning c.MinBatchSizeForThreadPinning = config.MinBatchSizeForThreadPinning
c.BatchProcessorMaxQueueSize = Config.BatchProcessorMaxQueueSize c.BatchProcessorMaxQueueSize = config.BatchProcessorMaxQueueSize
c.BatchProcessorAdaptiveThreshold = Config.BatchProcessorAdaptiveThreshold c.BatchProcessorAdaptiveThreshold = config.BatchProcessorAdaptiveThreshold
c.BatchProcessorThreadPinningThreshold = Config.BatchProcessorThreadPinningThreshold c.BatchProcessorThreadPinningThreshold = config.BatchProcessorThreadPinningThreshold
// Pre-allocate common errors // Pre-allocate common errors
c.bufferTooSmallReadEncode = newBufferTooSmallError(0, Config.MinReadEncodeBuffer) c.bufferTooSmallReadEncode = newBufferTooSmallError(0, config.MinReadEncodeBuffer)
c.bufferTooLargeDecodeWrite = newBufferTooLargeError(Config.MaxDecodeWriteBuffer+1, Config.MaxDecodeWriteBuffer) c.bufferTooLargeDecodeWrite = newBufferTooLargeError(config.MaxDecodeWriteBuffer+1, config.MaxDecodeWriteBuffer)
c.lastUpdate = time.Now() c.lastUpdate = time.Now()
c.initialized.Store(true) c.initialized.Store(true)
// Update the global validation cache as well // Update the global validation cache as well
if cachedMaxFrameSize != 0 { if cachedMaxFrameSize != 0 {
cachedMaxFrameSize = Config.MaxAudioFrameSize cachedMaxFrameSize = config.MaxAudioFrameSize
} }
} }
} }
@ -911,28 +901,46 @@ func updateCacheIfNeeded(cache *AudioConfigCache) {
} }
func cgoAudioReadEncode(buf []byte) (int, error) { func cgoAudioReadEncode(buf []byte) (int, error) {
// Minimal buffer validation - assume caller provides correct size cache := GetCachedConfig()
if len(buf) == 0 { updateCacheIfNeeded(cache)
return 0, errEmptyBuffer
// Fast validation with cached values - avoid lock with atomic access
minRequired := cache.GetMinReadEncodeBuffer()
// Buffer validation - use pre-allocated error for common case
if len(buf) < minRequired {
// Use pre-allocated error for common case, only create custom error for edge cases
if len(buf) > 0 {
return 0, newBufferTooSmallError(len(buf), minRequired)
}
return 0, cache.GetBufferTooSmallError()
} }
// Direct CGO call - hotpath optimization // Skip initialization check for now to avoid CGO compilation issues
// Direct CGO call with minimal overhead - unsafe.Pointer(&slice[0]) is safe for validated non-empty buffers
n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0])) n := C.jetkvm_audio_read_encode(unsafe.Pointer(&buf[0]))
// Fast path for success // Fast path for success case
if n > 0 { if n > 0 {
return int(n), nil return int(n), nil
} }
// Error handling with static errors // Handle error cases - use static error codes to reduce allocations
if n < 0 { if n < 0 {
if n == -1 { // Common error cases
switch n {
case -1:
return 0, errAudioInitFailed return 0, errAudioInitFailed
case -2:
return 0, errAudioReadEncode
default:
return 0, newAudioReadEncodeError(int(n))
} }
return 0, errAudioReadEncode
} }
return 0, nil // n == 0 case
return 0, nil // No data available
} }
// Audio playback functions // Audio playback functions
@ -954,25 +962,58 @@ func cgoAudioPlaybackClose() {
C.jetkvm_audio_playback_close() C.jetkvm_audio_playback_close()
} }
func cgoAudioDecodeWrite(buf []byte) (int, error) { func cgoAudioDecodeWrite(buf []byte) (n int, err error) {
// Minimal validation - assume caller provides correct size // Fast validation with AudioConfigCache
cache := GetCachedConfig()
// Only update cache if expired - avoid unnecessary overhead
// Use proper locking to avoid race condition
if cache.initialized.Load() {
cache.mutex.RLock()
cacheExpired := time.Since(cache.lastUpdate) > cache.cacheExpiry
cache.mutex.RUnlock()
if cacheExpired {
cache.Update()
}
} else {
cache.Update()
}
// Optimized buffer validation
if len(buf) == 0 { if len(buf) == 0 {
return 0, errEmptyBuffer return 0, errEmptyBuffer
} }
// Direct CGO call - hotpath optimization // Use cached max buffer size with atomic access
n := int(C.jetkvm_audio_decode_write(unsafe.Pointer(&buf[0]), C.int(len(buf)))) maxAllowed := cache.GetMaxDecodeWriteBuffer()
if len(buf) > maxAllowed {
// Use pre-allocated error for common case
if len(buf) == maxAllowed+1 {
return 0, cache.GetBufferTooLargeError()
}
return 0, newBufferTooLargeError(len(buf), maxAllowed)
}
// Fast path for success // Direct CGO call with minimal overhead - unsafe.Pointer(&slice[0]) is safe for validated non-empty buffers
n = int(C.jetkvm_audio_decode_write(unsafe.Pointer(&buf[0]), C.int(len(buf))))
// Fast path for success case
if n >= 0 { if n >= 0 {
return n, nil return n, nil
} }
// Error handling with static errors // Handle error cases with static error codes
if n == -1 { switch n {
return 0, errAudioInitFailed case -1:
n = 0
err = errAudioInitFailed
case -2:
n = 0
err = errAudioDecodeWrite
default:
n = 0
err = newAudioDecodeWriteError(n)
} }
return 0, errAudioDecodeWrite return
} }
// updateOpusEncoderParams dynamically updates OPUS encoder parameters // updateOpusEncoderParams dynamically updates OPUS encoder parameters
@ -995,7 +1036,7 @@ func updateOpusEncoderParams(bitrate, complexity, vbr, vbrConstraint, signalType
// Buffer pool for reusing buffers in CGO functions // Buffer pool for reusing buffers in CGO functions
var ( var (
// Using SizedBufferPool for better memory management // Using SizedBufferPool for better memory management
// Track buffer pool usage // Track buffer pool usage for monitoring
cgoBufferPoolGets atomic.Int64 cgoBufferPoolGets atomic.Int64
cgoBufferPoolPuts atomic.Int64 cgoBufferPoolPuts atomic.Int64
// Batch processing statistics - only enabled in debug builds // Batch processing statistics - only enabled in debug builds
@ -1058,24 +1099,70 @@ func DecodeWriteWithPooledBuffer(data []byte) (int, error) {
} }
// BatchReadEncode reads and encodes multiple audio frames in a single batch // BatchReadEncode reads and encodes multiple audio frames in a single batch
// with optimized zero-copy frame management and batch reference counting
func BatchReadEncode(batchSize int) ([][]byte, error) { func BatchReadEncode(batchSize int) ([][]byte, error) {
// Simple batch processing without complex overhead cache := GetCachedConfig()
frames := make([][]byte, 0, batchSize) updateCacheIfNeeded(cache)
frameSize := 4096 // Fixed frame size for performance
// Calculate total buffer size needed for batch
frameSize := cache.GetMinReadEncodeBuffer()
totalSize := frameSize * batchSize
// Get a single large buffer for all frames
batchBuffer := GetBufferFromPool(totalSize)
defer ReturnBufferToPool(batchBuffer)
// Pre-allocate frame result buffers from pool to avoid allocations in loop
frameBuffers := make([][]byte, 0, batchSize)
for i := 0; i < batchSize; i++ { for i := 0; i < batchSize; i++ {
buf := make([]byte, frameSize) frameBuffers = append(frameBuffers, GetBufferFromPool(frameSize))
n, err := cgoAudioReadEncode(buf) }
defer func() {
// Return all frame buffers to pool
for _, buf := range frameBuffers {
ReturnBufferToPool(buf)
}
}()
// Track batch processing statistics - only if enabled
var startTime time.Time
// Batch time tracking removed
trackTime := false
if trackTime {
startTime = time.Now()
}
batchProcessingCount.Add(1)
// Process frames in batch
frames := make([][]byte, 0, batchSize)
for i := 0; i < batchSize; i++ {
// Calculate offset for this frame in the batch buffer
offset := i * frameSize
frameBuf := batchBuffer[offset : offset+frameSize]
// Process this frame
n, err := cgoAudioReadEncode(frameBuf)
if err != nil { if err != nil {
// Return partial batch on error
if i > 0 { if i > 0 {
return frames, nil // Return partial batch batchFrameCount.Add(int64(i))
if trackTime {
batchProcessingTime.Add(time.Since(startTime).Microseconds())
}
return frames, nil
} }
return nil, err return nil, err
} }
if n > 0 {
frames = append(frames, buf[:n]) // Reuse pre-allocated buffer instead of make([]byte, n)
} frameCopy := frameBuffers[i][:n] // Slice to actual size
copy(frameCopy, frameBuf[:n])
frames = append(frames, frameCopy)
}
// Update statistics
batchFrameCount.Add(int64(len(frames)))
if trackTime {
batchProcessingTime.Add(time.Since(startTime).Microseconds())
} }
return frames, nil return frames, nil
@ -1083,39 +1170,12 @@ func BatchReadEncode(batchSize int) ([][]byte, error) {
// BatchDecodeWrite decodes and writes multiple audio frames in a single batch // BatchDecodeWrite decodes and writes multiple audio frames in a single batch
// This reduces CGO call overhead by processing multiple frames at once // This reduces CGO call overhead by processing multiple frames at once
// with optimized zero-copy frame management and batch reference counting
func BatchDecodeWrite(frames [][]byte) error { func BatchDecodeWrite(frames [][]byte) error {
// Validate input // Validate input
if len(frames) == 0 { if len(frames) == 0 {
return nil return nil
} }
// Convert to zero-copy frames for optimized processing
zeroCopyFrames := make([]*ZeroCopyAudioFrame, 0, len(frames))
for _, frameData := range frames {
if len(frameData) > 0 {
frame := GetZeroCopyFrame()
frame.SetDataDirect(frameData) // Direct assignment without copy
zeroCopyFrames = append(zeroCopyFrames, frame)
}
}
// Use batch reference counting for efficient management
if len(zeroCopyFrames) > 0 {
// Batch AddRef all frames at once
err := BatchAddRefFrames(zeroCopyFrames)
if err != nil {
return err
}
// Ensure cleanup with batch release
defer func() {
if _, err := BatchReleaseFrames(zeroCopyFrames); err != nil {
// Log release error but don't fail the operation
_ = err
}
}()
}
// Get cached config // Get cached config
cache := GetCachedConfig() cache := GetCachedConfig()
// Only update cache if expired - avoid unnecessary overhead // Only update cache if expired - avoid unnecessary overhead
@ -1144,17 +1204,16 @@ func BatchDecodeWrite(frames [][]byte) error {
pcmBuffer := GetBufferFromPool(cache.GetMaxPCMBufferSize()) pcmBuffer := GetBufferFromPool(cache.GetMaxPCMBufferSize())
defer ReturnBufferToPool(pcmBuffer) defer ReturnBufferToPool(pcmBuffer)
// Process each zero-copy frame with optimized batch processing // Process each frame
frameCount := 0 frameCount := 0
for _, zcFrame := range zeroCopyFrames { for _, frame := range frames {
// Get frame data from zero-copy frame // Skip empty frames
frameData := zcFrame.Data()[:zcFrame.Length()] if len(frame) == 0 {
if len(frameData) == 0 {
continue continue
} }
// Process this frame using optimized implementation // Process this frame using optimized implementation
_, err := CGOAudioDecodeWrite(frameData, pcmBuffer) _, err := CGOAudioDecodeWrite(frame, pcmBuffer)
if err != nil { if err != nil {
// Update statistics before returning error // Update statistics before returning error
batchFrameCount.Add(int64(frameCount)) batchFrameCount.Add(int64(frameCount))

View File

@ -4,12 +4,12 @@ import "time"
// GetMetricsUpdateInterval returns the current metrics update interval from centralized config // GetMetricsUpdateInterval returns the current metrics update interval from centralized config
func GetMetricsUpdateInterval() time.Duration { func GetMetricsUpdateInterval() time.Duration {
return Config.MetricsUpdateInterval return GetConfig().MetricsUpdateInterval
} }
// SetMetricsUpdateInterval sets the metrics update interval in centralized config // SetMetricsUpdateInterval sets the metrics update interval in centralized config
func SetMetricsUpdateInterval(interval time.Duration) { func SetMetricsUpdateInterval(interval time.Duration) {
config := Config config := GetConfig()
config.MetricsUpdateInterval = interval config.MetricsUpdateInterval = interval
UpdateConfig(config) UpdateConfig(config)
} }

View File

@ -117,6 +117,7 @@ type AudioConfigConstants struct {
// Buffer Management // Buffer Management
PreallocSize int
MaxPoolSize int MaxPoolSize int
MessagePoolSize int MessagePoolSize int
OptimalSocketBuffer int OptimalSocketBuffer int
@ -130,27 +131,27 @@ type AudioConfigConstants struct {
MinReadEncodeBuffer int MinReadEncodeBuffer int
MaxDecodeWriteBuffer int MaxDecodeWriteBuffer int
MinBatchSizeForThreadPinning int MinBatchSizeForThreadPinning int
GoroutineMonitorInterval time.Duration
MagicNumber uint32 MagicNumber uint32
MaxFrameSize int MaxFrameSize int
WriteTimeout time.Duration WriteTimeout time.Duration
HeaderSize int HeaderSize int
MetricsUpdateInterval time.Duration MetricsUpdateInterval time.Duration
WarmupSamples int WarmupSamples int
MetricsChannelBuffer int MetricsChannelBuffer int
LatencyHistorySize int LatencyHistorySize int
MaxCPUPercent float64 MaxCPUPercent float64
MinCPUPercent float64 MinCPUPercent float64
DefaultClockTicks float64 DefaultClockTicks float64
DefaultMemoryGB int DefaultMemoryGB int
MaxWarmupSamples int MaxWarmupSamples int
WarmupCPUSamples int WarmupCPUSamples int
LogThrottleIntervalSec int LogThrottleIntervalSec int
MinValidClockTicks int MinValidClockTicks int
MaxValidClockTicks int MaxValidClockTicks int
CPUFactor float64 CPUFactor float64
MemoryFactor float64 MemoryFactor float64
LatencyFactor float64 LatencyFactor float64
// Adaptive Buffer Configuration // Adaptive Buffer Configuration
AdaptiveMinBufferSize int // Minimum buffer size in frames for adaptive buffering AdaptiveMinBufferSize int // Minimum buffer size in frames for adaptive buffering
@ -171,25 +172,28 @@ type AudioConfigConstants struct {
OutputSupervisorTimeout time.Duration // 5s OutputSupervisorTimeout time.Duration // 5s
BatchProcessingDelay time.Duration // 10ms BatchProcessingDelay time.Duration // 10ms
AdaptiveOptimizerStability time.Duration // 10s
LatencyMonitorTarget time.Duration // 50ms
// Adaptive Buffer Configuration // Adaptive Buffer Configuration
// LowCPUThreshold defines CPU usage threshold for buffer size reduction. // LowCPUThreshold defines CPU usage threshold for buffer size reduction.
LowCPUThreshold float64 // 20% CPU threshold for buffer optimization LowCPUThreshold float64 // 20% CPU threshold for buffer optimization
// HighCPUThreshold defines CPU usage threshold for buffer size increase. // HighCPUThreshold defines CPU usage threshold for buffer size increase.
HighCPUThreshold float64 // 60% CPU threshold HighCPUThreshold float64 // 60% CPU threshold
LowMemoryThreshold float64 // 50% memory threshold LowMemoryThreshold float64 // 50% memory threshold
HighMemoryThreshold float64 // 75% memory threshold HighMemoryThreshold float64 // 75% memory threshold
AdaptiveBufferTargetLatency time.Duration // 20ms target latency AdaptiveBufferTargetLatency time.Duration // 20ms target latency
CooldownPeriod time.Duration // 30s cooldown period CooldownPeriod time.Duration // 30s cooldown period
RollbackThreshold time.Duration // 300ms rollback threshold RollbackThreshold time.Duration // 300ms rollback threshold
AdaptiveOptimizerLatencyTarget time.Duration // 50ms latency target
MaxLatencyThreshold time.Duration // 200ms max latency MaxLatencyThreshold time.Duration // 200ms max latency
JitterThreshold time.Duration // 20ms jitter threshold JitterThreshold time.Duration // 20ms jitter threshold
LatencyOptimizationInterval time.Duration // 5s optimization interval LatencyOptimizationInterval time.Duration // 5s optimization interval
LatencyAdaptiveThreshold float64 // 0.8 adaptive threshold LatencyAdaptiveThreshold float64 // 0.8 adaptive threshold
MicContentionTimeout time.Duration // 200ms contention timeout MicContentionTimeout time.Duration // 200ms contention timeout
PreallocPercentage int // 20% preallocation percentage PreallocPercentage int // 20% preallocation percentage
BackoffStart time.Duration // 50ms initial backoff BackoffStart time.Duration // 50ms initial backoff
InputMagicNumber uint32 // Magic number for input IPC messages (0x4A4B4D49 "JKMI") InputMagicNumber uint32 // Magic number for input IPC messages (0x4A4B4D49 "JKMI")
@ -210,8 +214,29 @@ type AudioConfigConstants struct {
// CGO Audio Processing Constants // CGO Audio Processing Constants
CGOUsleepMicroseconds int // Sleep duration for CGO usleep calls (1000μs) CGOUsleepMicroseconds int // Sleep duration for CGO usleep calls (1000μs)
CGOPCMBufferSize int // PCM buffer size for CGO audio processing CGOPCMBufferSize int // PCM buffer size for CGO audio processing
CGONanosecondsPerSecond float64 // Nanoseconds per second conversion CGONanosecondsPerSecond float64 // Nanoseconds per second conversion
FrontendOperationDebounceMS int // Frontend operation debounce delay
FrontendSyncDebounceMS int // Frontend sync debounce delay
FrontendSampleRate int // Frontend sample rate
FrontendRetryDelayMS int // Frontend retry delay
FrontendShortDelayMS int // Frontend short delay
FrontendLongDelayMS int // Frontend long delay
FrontendSyncDelayMS int // Frontend sync delay
FrontendMaxRetryAttempts int // Frontend max retry attempts
FrontendAudioLevelUpdateMS int // Frontend audio level update interval
FrontendFFTSize int // Frontend FFT size
FrontendAudioLevelMax int // Frontend max audio level
FrontendReconnectIntervalMS int // Frontend reconnect interval
FrontendSubscriptionDelayMS int // Frontend subscription delay
FrontendDebugIntervalMS int // Frontend debug interval
// Process Monitoring Constants
ProcessMonitorDefaultMemoryGB int // Default memory size for fallback (4GB)
ProcessMonitorKBToBytes int // KB to bytes conversion factor (1024)
ProcessMonitorDefaultClockHz float64 // Default system clock frequency (250.0 Hz)
ProcessMonitorFallbackClockHz float64 // Fallback clock frequency (1000.0 Hz)
ProcessMonitorTraditionalHz float64 // Traditional system clock frequency (100.0 Hz)
// Batch Processing Constants // Batch Processing Constants
BatchProcessorFramesPerBatch int // Frames processed per batch (4) BatchProcessorFramesPerBatch int // Frames processed per batch (4)
@ -247,21 +272,14 @@ type AudioConfigConstants struct {
LatencyPercentile50 int LatencyPercentile50 int
LatencyPercentile95 int LatencyPercentile95 int
LatencyPercentile99 int LatencyPercentile99 int
BufferPoolMaxOperations int
// Buffer Pool Configuration HitRateCalculationBase float64
BufferPoolDefaultSize int // Default buffer pool size when MaxPoolSize is invalid MaxLatency time.Duration
BufferPoolControlSize int // Control buffer pool size MinMetricsUpdateInterval time.Duration
ZeroCopyPreallocSizeBytes int // Zero-copy frame pool preallocation size in bytes MaxMetricsUpdateInterval time.Duration
ZeroCopyMinPreallocFrames int // Minimum preallocated frames for zero-copy pool MinSampleRate int
BufferPoolHitRateBase float64 // Base for hit rate percentage calculation MaxSampleRate int
MaxChannels int
HitRateCalculationBase float64
MaxLatency time.Duration
MinMetricsUpdateInterval time.Duration
MaxMetricsUpdateInterval time.Duration
MinSampleRate int
MaxSampleRate int
MaxChannels int
// CGO Constants // CGO Constants
CGOMaxBackoffMicroseconds int // Maximum CGO backoff time (500ms) CGOMaxBackoffMicroseconds int // Maximum CGO backoff time (500ms)
@ -295,22 +313,6 @@ type AudioConfigConstants struct {
AudioProcessorQueueSize int AudioProcessorQueueSize int
AudioReaderQueueSize int AudioReaderQueueSize int
WorkerMaxIdleTime time.Duration WorkerMaxIdleTime time.Duration
// Connection Retry Configuration
MaxConnectionAttempts int // Maximum connection retry attempts
ConnectionRetryDelay time.Duration // Initial connection retry delay
MaxConnectionRetryDelay time.Duration // Maximum connection retry delay
ConnectionBackoffFactor float64 // Connection retry backoff factor
ConnectionTimeoutDelay time.Duration // Connection timeout for each attempt
ReconnectionInterval time.Duration // Interval for automatic reconnection attempts
HealthCheckInterval time.Duration // Health check interval for connections
// Quality Change Timeout Configuration
QualityChangeSupervisorTimeout time.Duration // Timeout for supervisor stop during quality changes
QualityChangeTickerInterval time.Duration // Ticker interval for supervisor stop polling
QualityChangeSettleDelay time.Duration // Delay for quality change to settle
QualityChangeRecoveryDelay time.Duration // Delay before attempting recovery
} }
// DefaultAudioConfig returns the default configuration constants // DefaultAudioConfig returns the default configuration constants
@ -420,31 +422,31 @@ func DefaultAudioConfig() *AudioConfigConstants {
MaxRestartDelay: 30 * time.Second, // Maximum delay for exponential backoff MaxRestartDelay: 30 * time.Second, // Maximum delay for exponential backoff
// Buffer Management // Buffer Management
PreallocSize: 1024 * 1024, // 1MB buffer preallocation
MaxPoolSize: 100, // Maximum object pool size MaxPoolSize: 100, // Maximum object pool size
MessagePoolSize: 1024, // Significantly increased message pool for quality change bursts MessagePoolSize: 256, // Message pool size for IPC
OptimalSocketBuffer: 262144, // 256KB optimal socket buffer OptimalSocketBuffer: 262144, // 256KB optimal socket buffer
MaxSocketBuffer: 1048576, // 1MB maximum socket buffer MaxSocketBuffer: 1048576, // 1MB maximum socket buffer
MinSocketBuffer: 8192, // 8KB minimum socket buffer MinSocketBuffer: 8192, // 8KB minimum socket buffer
ChannelBufferSize: 2048, // Significantly increased channel buffer for quality change bursts ChannelBufferSize: 500, // Inter-goroutine channel buffer size
AudioFramePoolSize: 1500, // Audio frame object pool size AudioFramePoolSize: 1500, // Audio frame object pool size
PageSize: 4096, // Memory page size for alignment PageSize: 4096, // Memory page size for alignment
InitialBufferFrames: 1000, // Increased initial buffer size during startup InitialBufferFrames: 500, // Initial buffer size during startup
BytesToMBDivisor: 1024 * 1024, // Byte to megabyte conversion BytesToMBDivisor: 1024 * 1024, // Byte to megabyte conversion
MinReadEncodeBuffer: 1276, // Minimum CGO read/encode buffer MinReadEncodeBuffer: 1276, // Minimum CGO read/encode buffer
MaxDecodeWriteBuffer: 4096, // Maximum CGO decode/write buffer MaxDecodeWriteBuffer: 4096, // Maximum CGO decode/write buffer
// IPC Configuration - Balanced for stability // IPC Configuration
MagicNumber: 0xDEADBEEF, // IPC message validation header MagicNumber: 0xDEADBEEF, // IPC message validation header
MaxFrameSize: 4096, // Maximum audio frame size (4KB) MaxFrameSize: 4096, // Maximum audio frame size (4KB)
WriteTimeout: 1000 * time.Millisecond, // Further increased timeout to handle quality change bursts WriteTimeout: 100 * time.Millisecond, // IPC write operation timeout
HeaderSize: 8, // IPC message header size HeaderSize: 8, // IPC message header size
// Monitoring and Metrics - Balanced for stability // Monitoring and Metrics
MetricsUpdateInterval: 1000 * time.Millisecond, // Stable metrics collection frequency MetricsUpdateInterval: 1000 * time.Millisecond, // Metrics collection frequency
WarmupSamples: 10, // Adequate warmup samples for accuracy WarmupSamples: 10, // Warmup samples for metrics accuracy
MetricsChannelBuffer: 100, // Adequate metrics data channel buffer MetricsChannelBuffer: 100, // Metrics data channel buffer size
LatencyHistorySize: 100, // Adequate latency measurements to keep LatencyHistorySize: 100, // Number of latency measurements to keep
// Process Monitoring Constants // Process Monitoring Constants
MaxCPUPercent: 100.0, // Maximum CPU percentage MaxCPUPercent: 100.0, // Maximum CPU percentage
@ -468,50 +470,41 @@ func DefaultAudioConfig() *AudioConfigConstants {
BackoffMultiplier: 2.0, // Exponential backoff multiplier BackoffMultiplier: 2.0, // Exponential backoff multiplier
MaxConsecutiveErrors: 5, // Consecutive error threshold MaxConsecutiveErrors: 5, // Consecutive error threshold
// Connection Retry Configuration // Timing Constants
MaxConnectionAttempts: 15, // Maximum connection retry attempts DefaultSleepDuration: 100 * time.Millisecond, // Standard polling interval
ConnectionRetryDelay: 50 * time.Millisecond, // Initial connection retry delay ShortSleepDuration: 10 * time.Millisecond, // High-frequency polling
MaxConnectionRetryDelay: 2 * time.Second, // Maximum connection retry delay LongSleepDuration: 200 * time.Millisecond, // Background tasks
ConnectionBackoffFactor: 1.5, // Connection retry backoff factor DefaultTickerInterval: 100 * time.Millisecond, // Periodic task interval
ConnectionTimeoutDelay: 5 * time.Second, // Connection timeout for each attempt BufferUpdateInterval: 500 * time.Millisecond, // Buffer status updates
ReconnectionInterval: 30 * time.Second, // Interval for automatic reconnection attempts InputSupervisorTimeout: 5 * time.Second, // Input monitoring timeout
HealthCheckInterval: 10 * time.Second, // Health check interval for connections OutputSupervisorTimeout: 5 * time.Second, // Output monitoring timeout
BatchProcessingDelay: 10 * time.Millisecond, // Batch processing delay
AdaptiveOptimizerStability: 10 * time.Second, // Adaptive stability period
// Quality Change Timeout Configuration LatencyMonitorTarget: 50 * time.Millisecond, // Target latency for monitoring
QualityChangeSupervisorTimeout: 5 * time.Second, // Timeout for supervisor stop during quality changes
QualityChangeTickerInterval: 100 * time.Millisecond, // Ticker interval for supervisor stop polling
QualityChangeSettleDelay: 2 * time.Second, // Delay for quality change to settle
QualityChangeRecoveryDelay: 1 * time.Second, // Delay before attempting recovery
// Timing Constants - Optimized for quality change stability // Adaptive Buffer Configuration
DefaultSleepDuration: 100 * time.Millisecond, // Balanced polling interval LowCPUThreshold: 0.20,
ShortSleepDuration: 10 * time.Millisecond, // Balanced high-frequency polling HighCPUThreshold: 0.60,
LongSleepDuration: 200 * time.Millisecond, // Balanced background task delay LowMemoryThreshold: 0.50,
DefaultTickerInterval: 100 * time.Millisecond, // Balanced periodic task interval HighMemoryThreshold: 0.75,
BufferUpdateInterval: 250 * time.Millisecond, // Faster buffer size update frequency AdaptiveBufferTargetLatency: 20 * time.Millisecond,
InputSupervisorTimeout: 5 * time.Second, // Input monitoring timeout
OutputSupervisorTimeout: 5 * time.Second, // Output monitoring timeout
BatchProcessingDelay: 5 * time.Millisecond, // Reduced batch processing delay
// Adaptive Buffer Configuration - Optimized for single-core RV1106G3 // Adaptive Buffer Size Configuration
LowCPUThreshold: 0.40, // Adjusted for single-core ARM system AdaptiveMinBufferSize: 3, // Minimum 3 frames for stability
HighCPUThreshold: 0.75, // Adjusted for single-core RV1106G3 (current load ~64%) AdaptiveMaxBufferSize: 20, // Maximum 20 frames for high load
LowMemoryThreshold: 0.60, AdaptiveDefaultBufferSize: 6, // Balanced buffer size (6 frames)
HighMemoryThreshold: 0.85, // Adjusted for 200MB total memory system
AdaptiveBufferTargetLatency: 10 * time.Millisecond, // Aggressive target latency for responsiveness
// Adaptive Buffer Size Configuration - Optimized for quality change bursts // Adaptive Optimizer Configuration
AdaptiveMinBufferSize: 256, // Further increased minimum to prevent emergency mode CooldownPeriod: 30 * time.Second,
AdaptiveMaxBufferSize: 1024, // Much higher maximum for quality changes RollbackThreshold: 300 * time.Millisecond,
AdaptiveDefaultBufferSize: 512, // Higher default for stability during bursts AdaptiveOptimizerLatencyTarget: 50 * time.Millisecond,
CooldownPeriod: 15 * time.Second, // Reduced cooldown period // Latency Monitor Configuration
RollbackThreshold: 200 * time.Millisecond, // Lower rollback threshold MaxLatencyThreshold: 200 * time.Millisecond,
JitterThreshold: 20 * time.Millisecond,
MaxLatencyThreshold: 150 * time.Millisecond, // Lower max latency threshold LatencyOptimizationInterval: 5 * time.Second,
JitterThreshold: 15 * time.Millisecond, // Reduced jitter threshold LatencyAdaptiveThreshold: 0.8,
LatencyOptimizationInterval: 3 * time.Second, // More frequent optimization
LatencyAdaptiveThreshold: 0.7, // More aggressive adaptive threshold
// Microphone Contention Configuration // Microphone Contention Configuration
MicContentionTimeout: 200 * time.Millisecond, MicContentionTimeout: 200 * time.Millisecond,
@ -539,25 +532,48 @@ func DefaultAudioConfig() *AudioConfigConstants {
LatencyScalingFactor: 2.0, // Latency ratio scaling factor LatencyScalingFactor: 2.0, // Latency ratio scaling factor
OptimizerAggressiveness: 0.7, // Optimizer aggressiveness factor OptimizerAggressiveness: 0.7, // Optimizer aggressiveness factor
// CGO Audio Processing Constants - Balanced for stability // CGO Audio Processing Constants
CGOUsleepMicroseconds: 1000, // 1000 microseconds (1ms) for stable CGO usleep calls CGOUsleepMicroseconds: 1000, // 1000 microseconds (1ms) for CGO usleep calls
CGOPCMBufferSize: 1920, // 1920 samples for PCM buffer (max 2ch*960) CGOPCMBufferSize: 1920, // 1920 samples for PCM buffer (max 2ch*960)
CGONanosecondsPerSecond: 1000000000.0, // 1000000000.0 for nanosecond conversions CGONanosecondsPerSecond: 1000000000.0, // 1000000000.0 for nanosecond conversions
// Batch Processing Constants - Optimized for quality change bursts // Frontend Constants
BatchProcessorFramesPerBatch: 16, // Larger batches for quality changes FrontendOperationDebounceMS: 1000, // 1000ms debounce for frontend operations
BatchProcessorTimeout: 20 * time.Millisecond, // Longer timeout for bursts FrontendSyncDebounceMS: 1000, // 1000ms debounce for sync operations
BatchProcessorMaxQueueSize: 64, // Larger queue for quality changes FrontendSampleRate: 48000, // 48000Hz sample rate for frontend audio
BatchProcessorAdaptiveThreshold: 0.6, // Lower threshold for faster adaptation FrontendRetryDelayMS: 500, // 500ms retry delay
BatchProcessorThreadPinningThreshold: 8, // Lower threshold for better performance FrontendShortDelayMS: 200, // 200ms short delay
FrontendLongDelayMS: 300, // 300ms long delay
FrontendSyncDelayMS: 500, // 500ms sync delay
FrontendMaxRetryAttempts: 3, // 3 maximum retry attempts
FrontendAudioLevelUpdateMS: 100, // 100ms audio level update interval
FrontendFFTSize: 256, // 256 FFT size for audio analysis
FrontendAudioLevelMax: 100, // 100 maximum audio level
FrontendReconnectIntervalMS: 3000, // 3000ms reconnect interval
FrontendSubscriptionDelayMS: 100, // 100ms subscription delay
FrontendDebugIntervalMS: 5000, // 5000ms debug interval
// Output Streaming Constants - Balanced for stability // Process Monitor Constants
OutputStreamingFrameIntervalMS: 20, // 20ms frame interval (50 FPS) for stability ProcessMonitorDefaultMemoryGB: 4, // 4GB default memory for fallback
ProcessMonitorKBToBytes: 1024, // 1024 conversion factor
ProcessMonitorDefaultClockHz: 250.0, // 250.0 Hz default for ARM systems
ProcessMonitorFallbackClockHz: 1000.0, // 1000.0 Hz fallback clock
ProcessMonitorTraditionalHz: 100.0, // 100.0 Hz traditional clock
// Batch Processing Constants
BatchProcessorFramesPerBatch: 4, // 4 frames per batch
BatchProcessorTimeout: 5 * time.Millisecond, // 5ms timeout
BatchProcessorMaxQueueSize: 16, // 16 max queue size for balanced memory/performance
BatchProcessorAdaptiveThreshold: 0.8, // 0.8 threshold for adaptive batching (80% queue full)
BatchProcessorThreadPinningThreshold: 8, // 8 frames minimum for thread pinning optimization
// Output Streaming Constants
OutputStreamingFrameIntervalMS: 20, // 20ms frame interval (50 FPS)
// IPC Constants // IPC Constants
IPCInitialBufferFrames: 500, // 500 frames for initial buffer IPCInitialBufferFrames: 500, // 500 frames for initial buffer
// Event Constants - Balanced for stability // Event Constants
EventTimeoutSeconds: 2, // 2 seconds for event timeout EventTimeoutSeconds: 2, // 2 seconds for event timeout
EventTimeFormatString: "2006-01-02T15:04:05.000Z", // "2006-01-02T15:04:05.000Z" time format EventTimeFormatString: "2006-01-02T15:04:05.000Z", // "2006-01-02T15:04:05.000Z" time format
EventSubscriptionDelayMS: 100, // 100ms subscription delay EventSubscriptionDelayMS: 100, // 100ms subscription delay
@ -569,7 +585,7 @@ func DefaultAudioConfig() *AudioConfigConstants {
AudioReaderQueueSize: 32, // 32 tasks queue size for reader pool AudioReaderQueueSize: 32, // 32 tasks queue size for reader pool
WorkerMaxIdleTime: 60 * time.Second, // 60s maximum idle time before worker termination WorkerMaxIdleTime: 60 * time.Second, // 60s maximum idle time before worker termination
// Input Processing Constants - Balanced for stability // Input Processing Constants
InputProcessingTimeoutMS: 10, // 10ms processing timeout threshold InputProcessingTimeoutMS: 10, // 10ms processing timeout threshold
// Adaptive Buffer Constants // Adaptive Buffer Constants
@ -598,15 +614,9 @@ func DefaultAudioConfig() *AudioConfigConstants {
LatencyPercentile95: 95, // 95th percentile calculation factor LatencyPercentile95: 95, // 95th percentile calculation factor
LatencyPercentile99: 99, // 99th percentile calculation factor LatencyPercentile99: 99, // 99th percentile calculation factor
// Buffer Pool Configuration
BufferPoolDefaultSize: 64, // Default buffer pool size when MaxPoolSize is invalid
BufferPoolControlSize: 512, // Control buffer pool size
ZeroCopyPreallocSizeBytes: 1024 * 1024, // Zero-copy frame pool preallocation size in bytes (1MB)
ZeroCopyMinPreallocFrames: 1, // Minimum preallocated frames for zero-copy pool
BufferPoolHitRateBase: 100.0, // Base for hit rate percentage calculation
// Buffer Pool Efficiency Constants // Buffer Pool Efficiency Constants
HitRateCalculationBase: 100.0, // 100.0 base for hit rate percentage calculation BufferPoolMaxOperations: 1000, // 1000 operations for efficiency tracking
HitRateCalculationBase: 100.0, // 100.0 base for hit rate percentage calculation
// Validation Constants // Validation Constants
MaxLatency: 500 * time.Millisecond, // 500ms maximum allowed latency MaxLatency: 500 * time.Millisecond, // 500ms maximum allowed latency
@ -636,6 +646,8 @@ func DefaultAudioConfig() *AudioConfigConstants {
MinFrameSize: 1, // 1 byte minimum frame size (allow small frames) MinFrameSize: 1, // 1 byte minimum frame size (allow small frames)
FrameSizeTolerance: 512, // 512 bytes frame size tolerance FrameSizeTolerance: 512, // 512 bytes frame size tolerance
// Removed device health monitoring configuration - functionality not used
// Latency Histogram Bucket Configuration // Latency Histogram Bucket Configuration
LatencyBucket10ms: 10 * time.Millisecond, // 10ms latency bucket LatencyBucket10ms: 10 * time.Millisecond, // 10ms latency bucket
LatencyBucket25ms: 25 * time.Millisecond, // 25ms latency bucket LatencyBucket25ms: 25 * time.Millisecond, // 25ms latency bucket
@ -649,13 +661,16 @@ func DefaultAudioConfig() *AudioConfigConstants {
// Batch Audio Processing Configuration // Batch Audio Processing Configuration
MinBatchSizeForThreadPinning: 5, // Minimum batch size to pin thread MinBatchSizeForThreadPinning: 5, // Minimum batch size to pin thread
// Goroutine Monitoring Configuration
GoroutineMonitorInterval: 30 * time.Second, // 30s monitoring interval
// Performance Configuration Flags - Production optimizations // Performance Configuration Flags - Production optimizations
} }
} }
// Global configuration instance // Global configuration instance
var Config = DefaultAudioConfig() var audioConfigInstance = DefaultAudioConfig()
// UpdateConfig allows runtime configuration updates // UpdateConfig allows runtime configuration updates
func UpdateConfig(newConfig *AudioConfigConstants) { func UpdateConfig(newConfig *AudioConfigConstants) {
@ -667,12 +682,12 @@ func UpdateConfig(newConfig *AudioConfigConstants) {
return return
} }
Config = newConfig audioConfigInstance = newConfig
logger := logging.GetDefaultLogger().With().Str("component", "AudioConfig").Logger() logger := logging.GetDefaultLogger().With().Str("component", "AudioConfig").Logger()
logger.Info().Msg("Audio configuration updated successfully") logger.Info().Msg("Audio configuration updated successfully")
} }
// GetConfig returns the current configuration // GetConfig returns the current configuration
func GetConfig() *AudioConfigConstants { func GetConfig() *AudioConfigConstants {
return Config return audioConfigInstance
} }

View File

@ -29,9 +29,11 @@ func (s *AudioControlService) MuteAudio(muted bool) error {
supervisor := GetAudioOutputSupervisor() supervisor := GetAudioOutputSupervisor()
if supervisor != nil { if supervisor != nil {
supervisor.Stop() supervisor.Stop()
s.logger.Info().Msg("audio output supervisor stopped")
} }
StopAudioRelay() StopAudioRelay()
SetAudioMuted(true) SetAudioMuted(true)
s.logger.Info().Msg("audio output muted (subprocess and relay stopped)")
} else { } else {
// Unmute: Start audio output subprocess and relay // Unmute: Start audio output subprocess and relay
if !s.sessionProvider.IsSessionActive() { if !s.sessionProvider.IsSessionActive() {
@ -42,9 +44,10 @@ func (s *AudioControlService) MuteAudio(muted bool) error {
if supervisor != nil { if supervisor != nil {
err := supervisor.Start() err := supervisor.Start()
if err != nil { if err != nil {
s.logger.Debug().Err(err).Msg("failed to start audio output supervisor") s.logger.Error().Err(err).Msg("failed to start audio output supervisor during unmute")
return err return err
} }
s.logger.Info().Msg("audio output supervisor started")
} }
// Start audio relay // Start audio relay

View File

@ -158,6 +158,78 @@ var (
}, },
) )
// Audio subprocess process metrics
audioProcessCpuPercent = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_process_cpu_percent",
Help: "CPU usage percentage of audio output subprocess",
},
)
audioProcessMemoryPercent = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_process_memory_percent",
Help: "Memory usage percentage of audio output subprocess",
},
)
audioProcessMemoryRssBytes = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_process_memory_rss_bytes",
Help: "RSS memory usage in bytes of audio output subprocess",
},
)
audioProcessMemoryVmsBytes = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_process_memory_vms_bytes",
Help: "VMS memory usage in bytes of audio output subprocess",
},
)
audioProcessRunning = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_audio_process_running",
Help: "Whether audio output subprocess is running (1=running, 0=stopped)",
},
)
// Microphone subprocess process metrics
microphoneProcessCpuPercent = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_microphone_process_cpu_percent",
Help: "CPU usage percentage of microphone input subprocess",
},
)
microphoneProcessMemoryPercent = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_microphone_process_memory_percent",
Help: "Memory usage percentage of microphone input subprocess",
},
)
microphoneProcessMemoryRssBytes = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_microphone_process_memory_rss_bytes",
Help: "RSS memory usage in bytes of microphone input subprocess",
},
)
microphoneProcessMemoryVmsBytes = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_microphone_process_memory_vms_bytes",
Help: "VMS memory usage in bytes of microphone input subprocess",
},
)
microphoneProcessRunning = promauto.NewGauge(
prometheus.GaugeOpts{
Name: "jetkvm_microphone_process_running",
Help: "Whether microphone input subprocess is running (1=running, 0=stopped)",
},
)
// Device health metrics // Device health metrics
// Removed device health metrics - functionality not used // Removed device health metrics - functionality not used
@ -374,6 +446,42 @@ func UpdateMicrophoneMetrics(metrics UnifiedAudioMetrics) {
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix()) atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
} }
// UpdateAudioProcessMetrics updates Prometheus metrics with audio subprocess data
func UpdateAudioProcessMetrics(metrics ProcessMetrics, isRunning bool) {
metricsUpdateMutex.Lock()
defer metricsUpdateMutex.Unlock()
audioProcessCpuPercent.Set(metrics.CPUPercent)
audioProcessMemoryPercent.Set(metrics.MemoryPercent)
audioProcessMemoryRssBytes.Set(float64(metrics.MemoryRSS))
audioProcessMemoryVmsBytes.Set(float64(metrics.MemoryVMS))
if isRunning {
audioProcessRunning.Set(1)
} else {
audioProcessRunning.Set(0)
}
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// UpdateMicrophoneProcessMetrics updates Prometheus metrics with microphone subprocess data
func UpdateMicrophoneProcessMetrics(metrics ProcessMetrics, isRunning bool) {
metricsUpdateMutex.Lock()
defer metricsUpdateMutex.Unlock()
microphoneProcessCpuPercent.Set(metrics.CPUPercent)
microphoneProcessMemoryPercent.Set(metrics.MemoryPercent)
microphoneProcessMemoryRssBytes.Set(float64(metrics.MemoryRSS))
microphoneProcessMemoryVmsBytes.Set(float64(metrics.MemoryVMS))
if isRunning {
microphoneProcessRunning.Set(1)
} else {
microphoneProcessRunning.Set(0)
}
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
}
// UpdateAdaptiveBufferMetrics updates Prometheus metrics with adaptive buffer information // UpdateAdaptiveBufferMetrics updates Prometheus metrics with adaptive buffer information
func UpdateAdaptiveBufferMetrics(inputBufferSize, outputBufferSize int, cpuPercent, memoryPercent float64, adjustmentMade bool) { func UpdateAdaptiveBufferMetrics(inputBufferSize, outputBufferSize int, cpuPercent, memoryPercent float64, adjustmentMade bool) {
metricsUpdateMutex.Lock() metricsUpdateMutex.Lock()
@ -406,7 +514,8 @@ func UpdateSocketBufferMetrics(component, bufferType string, size, utilization f
atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix()) atomic.StoreInt64(&lastMetricsUpdate, time.Now().Unix())
} }
// UpdateDeviceHealthMetrics - Placeholder for future device health metrics // UpdateDeviceHealthMetrics - Device health monitoring functionality has been removed
// This function is no longer used as device health monitoring is not implemented
// UpdateMemoryMetrics updates memory metrics // UpdateMemoryMetrics updates memory metrics
func UpdateMemoryMetrics() { func UpdateMemoryMetrics() {

View File

@ -55,11 +55,12 @@ func ValidateZeroCopyFrame(frame *ZeroCopyAudioFrame) error {
maxFrameSize := cachedMaxFrameSize maxFrameSize := cachedMaxFrameSize
if maxFrameSize == 0 { if maxFrameSize == 0 {
// Fallback: get from cache // Fallback: get from cache
cache := Config cache := GetCachedConfig()
maxFrameSize = cache.MaxAudioFrameSize maxFrameSize = int(cache.maxAudioFrameSize.Load())
if maxFrameSize == 0 { if maxFrameSize == 0 {
// Last resort: use default // Last resort: update cache
maxFrameSize = cache.MaxAudioFrameSize cache.Update()
maxFrameSize = int(cache.maxAudioFrameSize.Load())
} }
// Cache globally for next calls // Cache globally for next calls
cachedMaxFrameSize = maxFrameSize cachedMaxFrameSize = maxFrameSize
@ -72,15 +73,28 @@ func ValidateZeroCopyFrame(frame *ZeroCopyAudioFrame) error {
} }
// ValidateBufferSize validates buffer size parameters with enhanced boundary checks // ValidateBufferSize validates buffer size parameters with enhanced boundary checks
// Optimized for minimal overhead in hotpath // Optimized to use AudioConfigCache for frequently accessed values
func ValidateBufferSize(size int) error { func ValidateBufferSize(size int) error {
if size <= 0 { if size <= 0 {
return fmt.Errorf("%w: buffer size %d must be positive", ErrInvalidBufferSize, size) return fmt.Errorf("%w: buffer size %d must be positive", ErrInvalidBufferSize, size)
} }
// Single boundary check using pre-cached value
if size > Config.SocketMaxBuffer { // Fast path: Check against cached max frame size
cache := GetCachedConfig()
maxFrameSize := int(cache.maxAudioFrameSize.Load())
// Most common case: validating a buffer that's sized for audio frames
if maxFrameSize > 0 && size <= maxFrameSize {
return nil
}
// Slower path: full validation against SocketMaxBuffer
config := GetConfig()
// Use SocketMaxBuffer as the upper limit for general buffer validation
// This allows for socket buffers while still preventing extremely large allocations
if size > config.SocketMaxBuffer {
return fmt.Errorf("%w: buffer size %d exceeds maximum %d", return fmt.Errorf("%w: buffer size %d exceeds maximum %d",
ErrInvalidBufferSize, size, Config.SocketMaxBuffer) ErrInvalidBufferSize, size, config.SocketMaxBuffer)
} }
return nil return nil
} }
@ -93,8 +107,8 @@ func ValidateLatency(latency time.Duration) error {
} }
// Fast path: check against cached max latency // Fast path: check against cached max latency
cache := Config cache := GetCachedConfig()
maxLatency := time.Duration(cache.MaxLatency) maxLatency := time.Duration(cache.maxLatency.Load())
// If we have a valid cached value, use it // If we have a valid cached value, use it
if maxLatency > 0 { if maxLatency > 0 {
@ -110,14 +124,16 @@ func ValidateLatency(latency time.Duration) error {
return nil return nil
} }
// Slower path: full validation with GetConfig()
config := GetConfig()
minLatency := time.Millisecond // Minimum reasonable latency minLatency := time.Millisecond // Minimum reasonable latency
if latency > 0 && latency < minLatency { if latency > 0 && latency < minLatency {
return fmt.Errorf("%w: latency %v below minimum %v", return fmt.Errorf("%w: latency %v below minimum %v",
ErrInvalidLatency, latency, minLatency) ErrInvalidLatency, latency, minLatency)
} }
if latency > Config.MaxLatency { if latency > config.MaxLatency {
return fmt.Errorf("%w: latency %v exceeds maximum %v", return fmt.Errorf("%w: latency %v exceeds maximum %v",
ErrInvalidLatency, latency, Config.MaxLatency) ErrInvalidLatency, latency, config.MaxLatency)
} }
return nil return nil
} }
@ -126,9 +142,9 @@ func ValidateLatency(latency time.Duration) error {
// Optimized to use AudioConfigCache for frequently accessed values // Optimized to use AudioConfigCache for frequently accessed values
func ValidateMetricsInterval(interval time.Duration) error { func ValidateMetricsInterval(interval time.Duration) error {
// Fast path: check against cached values // Fast path: check against cached values
cache := Config cache := GetCachedConfig()
minInterval := time.Duration(cache.MinMetricsUpdateInterval) minInterval := time.Duration(cache.minMetricsUpdateInterval.Load())
maxInterval := time.Duration(cache.MaxMetricsUpdateInterval) maxInterval := time.Duration(cache.maxMetricsUpdateInterval.Load())
// If we have valid cached values, use them // If we have valid cached values, use them
if minInterval > 0 && maxInterval > 0 { if minInterval > 0 && maxInterval > 0 {
@ -143,8 +159,10 @@ func ValidateMetricsInterval(interval time.Duration) error {
return nil return nil
} }
minInterval = Config.MinMetricsUpdateInterval // Slower path: full validation with GetConfig()
maxInterval = Config.MaxMetricsUpdateInterval config := GetConfig()
minInterval = config.MinMetricsUpdateInterval
maxInterval = config.MaxMetricsUpdateInterval
if interval < minInterval { if interval < minInterval {
return ErrInvalidMetricsInterval return ErrInvalidMetricsInterval
} }
@ -166,7 +184,7 @@ func ValidateAdaptiveBufferConfig(minSize, maxSize, defaultSize int) error {
return ErrInvalidBufferSize return ErrInvalidBufferSize
} }
// Validate against global limits // Validate against global limits
maxBuffer := Config.SocketMaxBuffer maxBuffer := GetConfig().SocketMaxBuffer
if maxSize > maxBuffer { if maxSize > maxBuffer {
return ErrInvalidBufferSize return ErrInvalidBufferSize
} }
@ -175,9 +193,11 @@ func ValidateAdaptiveBufferConfig(minSize, maxSize, defaultSize int) error {
// ValidateInputIPCConfig validates input IPC configuration // ValidateInputIPCConfig validates input IPC configuration
func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error { func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error {
minSampleRate := Config.MinSampleRate // Use config values
maxSampleRate := Config.MaxSampleRate config := GetConfig()
maxChannels := Config.MaxChannels minSampleRate := config.MinSampleRate
maxSampleRate := config.MaxSampleRate
maxChannels := config.MaxChannels
if sampleRate < minSampleRate || sampleRate > maxSampleRate { if sampleRate < minSampleRate || sampleRate > maxSampleRate {
return ErrInvalidSampleRate return ErrInvalidSampleRate
} }
@ -192,9 +212,11 @@ func ValidateInputIPCConfig(sampleRate, channels, frameSize int) error {
// ValidateOutputIPCConfig validates output IPC configuration // ValidateOutputIPCConfig validates output IPC configuration
func ValidateOutputIPCConfig(sampleRate, channels, frameSize int) error { func ValidateOutputIPCConfig(sampleRate, channels, frameSize int) error {
minSampleRate := Config.MinSampleRate // Use config values
maxSampleRate := Config.MaxSampleRate config := GetConfig()
maxChannels := Config.MaxChannels minSampleRate := config.MinSampleRate
maxSampleRate := config.MaxSampleRate
maxChannels := config.MaxChannels
if sampleRate < minSampleRate || sampleRate > maxSampleRate { if sampleRate < minSampleRate || sampleRate > maxSampleRate {
return ErrInvalidSampleRate return ErrInvalidSampleRate
} }
@ -207,51 +229,130 @@ func ValidateOutputIPCConfig(sampleRate, channels, frameSize int) error {
return nil return nil
} }
// ValidateLatencyConfig validates latency monitor configuration
func ValidateLatencyConfig(config LatencyConfig) error {
if err := ValidateLatency(config.TargetLatency); err != nil {
return err
}
if err := ValidateLatency(config.MaxLatency); err != nil {
return err
}
if config.TargetLatency >= config.MaxLatency {
return ErrInvalidLatency
}
if err := ValidateMetricsInterval(config.OptimizationInterval); err != nil {
return err
}
if config.HistorySize <= 0 {
return ErrInvalidBufferSize
}
if config.JitterThreshold < 0 {
return ErrInvalidLatency
}
if config.AdaptiveThreshold < 0 || config.AdaptiveThreshold > 1.0 {
return ErrInvalidConfiguration
}
return nil
}
// ValidateSampleRate validates audio sample rate values // ValidateSampleRate validates audio sample rate values
// Optimized for minimal overhead in hotpath // Optimized to use AudioConfigCache for frequently accessed values
func ValidateSampleRate(sampleRate int) error { func ValidateSampleRate(sampleRate int) error {
if sampleRate <= 0 { if sampleRate <= 0 {
return fmt.Errorf("%w: sample rate %d must be positive", ErrInvalidSampleRate, sampleRate) return fmt.Errorf("%w: sample rate %d must be positive", ErrInvalidSampleRate, sampleRate)
} }
// Direct validation against valid rates
for _, rate := range Config.ValidSampleRates { // Fast path: Check against cached sample rate first
cache := GetCachedConfig()
cachedRate := int(cache.sampleRate.Load())
// Most common case: validating against the current sample rate
if sampleRate == cachedRate {
return nil
}
// Slower path: check against all valid rates
config := GetConfig()
validRates := config.ValidSampleRates
for _, rate := range validRates {
if sampleRate == rate { if sampleRate == rate {
return nil return nil
} }
} }
return fmt.Errorf("%w: sample rate %d not in valid rates %v", return fmt.Errorf("%w: sample rate %d not in supported rates %v",
ErrInvalidSampleRate, sampleRate, Config.ValidSampleRates) ErrInvalidSampleRate, sampleRate, validRates)
} }
// ValidateChannelCount validates audio channel count // ValidateChannelCount validates audio channel count
// Optimized for minimal overhead in hotpath // Optimized to use AudioConfigCache for frequently accessed values
func ValidateChannelCount(channels int) error { func ValidateChannelCount(channels int) error {
if channels <= 0 { if channels <= 0 {
return fmt.Errorf("%w: channel count %d must be positive", ErrInvalidChannels, channels) return fmt.Errorf("%w: channel count %d must be positive", ErrInvalidChannels, channels)
} }
// Direct boundary check
if channels > Config.MaxChannels { // Fast path: Check against cached channels first
cache := GetCachedConfig()
cachedChannels := int(cache.channels.Load())
// Most common case: validating against the current channel count
if channels == cachedChannels {
return nil
}
// Fast path: Check against cached max channels
cachedMaxChannels := int(cache.maxChannels.Load())
if cachedMaxChannels > 0 && channels <= cachedMaxChannels {
return nil
}
// Slow path: Update cache and validate
cache.Update()
updatedMaxChannels := int(cache.maxChannels.Load())
if channels > updatedMaxChannels {
return fmt.Errorf("%w: channel count %d exceeds maximum %d", return fmt.Errorf("%w: channel count %d exceeds maximum %d",
ErrInvalidChannels, channels, Config.MaxChannels) ErrInvalidChannels, channels, updatedMaxChannels)
} }
return nil return nil
} }
// ValidateBitrate validates audio bitrate values (expects kbps) // ValidateBitrate validates audio bitrate values (expects kbps)
// Optimized for minimal overhead in hotpath // Optimized to use AudioConfigCache for frequently accessed values
func ValidateBitrate(bitrate int) error { func ValidateBitrate(bitrate int) error {
if bitrate <= 0 { if bitrate <= 0 {
return fmt.Errorf("%w: bitrate %d must be positive", ErrInvalidBitrate, bitrate) return fmt.Errorf("%w: bitrate %d must be positive", ErrInvalidBitrate, bitrate)
} }
// Direct boundary check with single conversion
bitrateInBps := bitrate * 1000 // Fast path: Check against cached bitrate values
if bitrateInBps < Config.MinOpusBitrate { cache := GetCachedConfig()
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps", minBitrate := int(cache.minOpusBitrate.Load())
ErrInvalidBitrate, bitrate, bitrateInBps, Config.MinOpusBitrate) maxBitrate := int(cache.maxOpusBitrate.Load())
// If we have valid cached values, use them
if minBitrate > 0 && maxBitrate > 0 {
// Convert kbps to bps for comparison with config limits
bitrateInBps := bitrate * 1000
if bitrateInBps < minBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, minBitrate)
}
if bitrateInBps > maxBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) exceeds maximum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, maxBitrate)
}
return nil
} }
if bitrateInBps > Config.MaxOpusBitrate {
// Slower path: full validation with GetConfig()
config := GetConfig()
// Convert kbps to bps for comparison with config limits
bitrateInBps := bitrate * 1000
if bitrateInBps < config.MinOpusBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, config.MinOpusBitrate)
}
if bitrateInBps > config.MaxOpusBitrate {
return fmt.Errorf("%w: bitrate %d kbps (%d bps) exceeds maximum %d bps", return fmt.Errorf("%w: bitrate %d kbps (%d bps) exceeds maximum %d bps",
ErrInvalidBitrate, bitrate, bitrateInBps, Config.MaxOpusBitrate) ErrInvalidBitrate, bitrate, bitrateInBps, config.MaxOpusBitrate)
} }
return nil return nil
} }
@ -264,11 +365,11 @@ func ValidateFrameDuration(duration time.Duration) error {
} }
// Fast path: Check against cached frame size first // Fast path: Check against cached frame size first
cache := Config cache := GetCachedConfig()
// Convert frameSize (samples) to duration for comparison // Convert frameSize (samples) to duration for comparison
cachedFrameSize := cache.FrameSize cachedFrameSize := int(cache.frameSize.Load())
cachedSampleRate := cache.SampleRate cachedSampleRate := int(cache.sampleRate.Load())
// Only do this calculation if we have valid cached values // Only do this calculation if we have valid cached values
if cachedFrameSize > 0 && cachedSampleRate > 0 { if cachedFrameSize > 0 && cachedSampleRate > 0 {
@ -281,8 +382,8 @@ func ValidateFrameDuration(duration time.Duration) error {
} }
// Fast path: Check against cached min/max frame duration // Fast path: Check against cached min/max frame duration
cachedMinDuration := time.Duration(cache.MinFrameDuration) cachedMinDuration := time.Duration(cache.minFrameDuration.Load())
cachedMaxDuration := time.Duration(cache.MaxFrameDuration) cachedMaxDuration := time.Duration(cache.maxFrameDuration.Load())
if cachedMinDuration > 0 && cachedMaxDuration > 0 { if cachedMinDuration > 0 && cachedMaxDuration > 0 {
if duration < cachedMinDuration { if duration < cachedMinDuration {
@ -296,9 +397,10 @@ func ValidateFrameDuration(duration time.Duration) error {
return nil return nil
} }
// Slow path: Use current config values // Slow path: Update cache and validate
updatedMinDuration := time.Duration(cache.MinFrameDuration) cache.Update()
updatedMaxDuration := time.Duration(cache.MaxFrameDuration) updatedMinDuration := time.Duration(cache.minFrameDuration.Load())
updatedMaxDuration := time.Duration(cache.maxFrameDuration.Load())
if duration < updatedMinDuration { if duration < updatedMinDuration {
return fmt.Errorf("%w: frame duration %v below minimum %v", return fmt.Errorf("%w: frame duration %v below minimum %v",
@ -315,11 +417,11 @@ func ValidateFrameDuration(duration time.Duration) error {
// Uses optimized validation functions that leverage AudioConfigCache // Uses optimized validation functions that leverage AudioConfigCache
func ValidateAudioConfigComplete(config AudioConfig) error { func ValidateAudioConfigComplete(config AudioConfig) error {
// Fast path: Check if all values match the current cached configuration // Fast path: Check if all values match the current cached configuration
cache := Config cache := GetCachedConfig()
cachedSampleRate := cache.SampleRate cachedSampleRate := int(cache.sampleRate.Load())
cachedChannels := cache.Channels cachedChannels := int(cache.channels.Load())
cachedBitrate := cache.OpusBitrate / 1000 // Convert from bps to kbps cachedBitrate := int(cache.opusBitrate.Load()) / 1000 // Convert from bps to kbps
cachedFrameSize := cache.FrameSize cachedFrameSize := int(cache.frameSize.Load())
// Only do this calculation if we have valid cached values // Only do this calculation if we have valid cached values
if cachedSampleRate > 0 && cachedChannels > 0 && cachedBitrate > 0 && cachedFrameSize > 0 { if cachedSampleRate > 0 && cachedChannels > 0 && cachedBitrate > 0 && cachedFrameSize > 0 {
@ -363,11 +465,11 @@ func ValidateAudioConfigConstants(config *AudioConfigConstants) error {
} }
// Validate configuration values if config is provided // Validate configuration values if config is provided
if config != nil { if config != nil {
if Config.MaxFrameSize <= 0 { if config.MaxFrameSize <= 0 {
return fmt.Errorf("invalid MaxFrameSize: %d", Config.MaxFrameSize) return fmt.Errorf("invalid MaxFrameSize: %d", config.MaxFrameSize)
} }
if Config.SampleRate <= 0 { if config.SampleRate <= 0 {
return fmt.Errorf("invalid SampleRate: %d", Config.SampleRate) return fmt.Errorf("invalid SampleRate: %d", config.SampleRate)
} }
} }
return nil return nil
@ -379,10 +481,11 @@ var cachedMaxFrameSize int
// InitValidationCache initializes cached validation values with actual config // InitValidationCache initializes cached validation values with actual config
func InitValidationCache() { func InitValidationCache() {
// Initialize the global cache variable for backward compatibility // Initialize the global cache variable for backward compatibility
cachedMaxFrameSize = Config.MaxAudioFrameSize config := GetConfig()
cachedMaxFrameSize = config.MaxAudioFrameSize
// Initialize the global audio config cache // Update the global audio config cache
cachedMaxFrameSize = Config.MaxAudioFrameSize GetCachedConfig().Update()
} }
// ValidateAudioFrame validates audio frame data with cached max size for performance // ValidateAudioFrame validates audio frame data with cached max size for performance
@ -399,11 +502,12 @@ func ValidateAudioFrame(data []byte) error {
maxSize := cachedMaxFrameSize maxSize := cachedMaxFrameSize
if maxSize == 0 { if maxSize == 0 {
// Fallback: get from cache only if global cache not initialized // Fallback: get from cache only if global cache not initialized
cache := Config cache := GetCachedConfig()
maxSize = cache.MaxAudioFrameSize maxSize = int(cache.maxAudioFrameSize.Load())
if maxSize == 0 { if maxSize == 0 {
// Last resort: get fresh value // Last resort: update cache and get fresh value
maxSize = cache.MaxAudioFrameSize cache.Update()
maxSize = int(cache.maxAudioFrameSize.Load())
} }
// Cache the value globally for next calls // Cache the value globally for next calls
cachedMaxFrameSize = maxSize cachedMaxFrameSize = maxSize

View File

@ -65,42 +65,6 @@ func (p *GoroutinePool) Submit(task Task) bool {
} }
} }
// SubmitWithBackpressure adds a task to the pool with backpressure handling
// Returns true if task was accepted, false if dropped due to backpressure
func (p *GoroutinePool) SubmitWithBackpressure(task Task) bool {
select {
case <-p.shutdown:
return false // Pool is shutting down
case p.taskQueue <- task:
// Task accepted, ensure we have a worker to process it
p.ensureWorkerAvailable()
return true
default:
// Queue is full - apply backpressure
// Check if we're in a high-load situation
queueLen := len(p.taskQueue)
queueCap := cap(p.taskQueue)
workerCount := atomic.LoadInt64(&p.workerCount)
// If queue is >90% full and we're at max workers, drop the task
if queueLen > int(float64(queueCap)*0.9) && workerCount >= int64(p.maxWorkers) {
p.logger.Warn().Int("queue_len", queueLen).Int("queue_cap", queueCap).Msg("Dropping task due to backpressure")
return false
}
// Try one more time with a short timeout
select {
case p.taskQueue <- task:
p.ensureWorkerAvailable()
return true
case <-time.After(1 * time.Millisecond):
// Still can't submit after timeout - drop task
p.logger.Debug().Msg("Task dropped after backpressure timeout")
return false
}
}
}
// ensureWorkerAvailable makes sure at least one worker is available to process tasks // ensureWorkerAvailable makes sure at least one worker is available to process tasks
func (p *GoroutinePool) ensureWorkerAvailable() { func (p *GoroutinePool) ensureWorkerAvailable() {
// Check if we already have enough workers // Check if we already have enough workers
@ -196,7 +160,7 @@ func (p *GoroutinePool) supervisor() {
tasks := atomic.LoadInt64(&p.taskCount) tasks := atomic.LoadInt64(&p.taskCount)
queueLen := len(p.taskQueue) queueLen := len(p.taskQueue)
p.logger.Debug(). p.logger.Info().
Int64("workers", workers). Int64("workers", workers).
Int64("tasks_processed", tasks). Int64("tasks_processed", tasks).
Int("queue_length", queueLen). Int("queue_length", queueLen).
@ -215,7 +179,7 @@ func (p *GoroutinePool) Shutdown(wait bool) {
if wait { if wait {
// Wait for all tasks to be processed // Wait for all tasks to be processed
if len(p.taskQueue) > 0 { if len(p.taskQueue) > 0 {
p.logger.Debug().Int("remaining_tasks", len(p.taskQueue)).Msg("Waiting for tasks to complete") p.logger.Info().Int("remaining_tasks", len(p.taskQueue)).Msg("Waiting for tasks to complete")
} }
// Close the task queue to signal no more tasks // Close the task queue to signal no more tasks
@ -255,7 +219,7 @@ func GetAudioProcessorPool() *GoroutinePool {
} }
globalAudioProcessorInitOnce.Do(func() { globalAudioProcessorInitOnce.Do(func() {
config := Config config := GetConfig()
newPool := NewGoroutinePool( newPool := NewGoroutinePool(
"audio-processor", "audio-processor",
config.MaxAudioProcessorWorkers, config.MaxAudioProcessorWorkers,
@ -277,7 +241,7 @@ func GetAudioReaderPool() *GoroutinePool {
} }
globalAudioReaderInitOnce.Do(func() { globalAudioReaderInitOnce.Do(func() {
config := Config config := GetConfig()
newPool := NewGoroutinePool( newPool := NewGoroutinePool(
"audio-reader", "audio-reader",
config.MaxAudioReaderWorkers, config.MaxAudioReaderWorkers,
@ -301,16 +265,6 @@ func SubmitAudioReaderTask(task Task) bool {
return GetAudioReaderPool().Submit(task) return GetAudioReaderPool().Submit(task)
} }
// SubmitAudioProcessorTaskWithBackpressure submits a task with backpressure handling
func SubmitAudioProcessorTaskWithBackpressure(task Task) bool {
return GetAudioProcessorPool().SubmitWithBackpressure(task)
}
// SubmitAudioReaderTaskWithBackpressure submits a task with backpressure handling
func SubmitAudioReaderTaskWithBackpressure(task Task) bool {
return GetAudioReaderPool().SubmitWithBackpressure(task)
}
// ShutdownAudioPools shuts down all audio goroutine pools // ShutdownAudioPools shuts down all audio goroutine pools
func ShutdownAudioPools(wait bool) { func ShutdownAudioPools(wait bool) {
logger := logging.GetDefaultLogger().With().Str("component", "audio-pools").Logger() logger := logging.GetDefaultLogger().With().Str("component", "audio-pools").Logger()

View File

@ -108,13 +108,14 @@ func (aim *AudioInputManager) WriteOpusFrame(frame []byte) error {
processingTime := time.Since(startTime) processingTime := time.Since(startTime)
// Log high latency warnings // Log high latency warnings
if processingTime > time.Duration(Config.InputProcessingTimeoutMS)*time.Millisecond { if processingTime > time.Duration(GetConfig().InputProcessingTimeoutMS)*time.Millisecond {
latencyMs := float64(processingTime.Milliseconds()) latencyMs := float64(processingTime.Milliseconds())
aim.logger.Warn(). aim.logger.Warn().
Float64("latency_ms", latencyMs). Float64("latency_ms", latencyMs).
Msg("High audio processing latency detected") Msg("High audio processing latency detected")
// Record latency for goroutine cleanup optimization // Record latency for goroutine cleanup optimization
RecordAudioLatency(latencyMs)
} }
if err != nil { if err != nil {
@ -148,13 +149,14 @@ func (aim *AudioInputManager) WriteOpusFrameZeroCopy(frame *ZeroCopyAudioFrame)
processingTime := time.Since(startTime) processingTime := time.Since(startTime)
// Log high latency warnings // Log high latency warnings
if processingTime > time.Duration(Config.InputProcessingTimeoutMS)*time.Millisecond { if processingTime > time.Duration(GetConfig().InputProcessingTimeoutMS)*time.Millisecond {
latencyMs := float64(processingTime.Milliseconds()) latencyMs := float64(processingTime.Milliseconds())
aim.logger.Warn(). aim.logger.Warn().
Float64("latency_ms", latencyMs). Float64("latency_ms", latencyMs).
Msg("High audio processing latency detected") Msg("High audio processing latency detected")
// Record latency for goroutine cleanup optimization // Record latency for goroutine cleanup optimization
RecordAudioLatency(latencyMs)
} }
if err != nil { if err != nil {

View File

@ -19,28 +19,6 @@ import (
"github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/logging"
) )
// Global audio input server instance
var globalAudioInputServer *AudioInputServer
// GetGlobalAudioInputServer returns the global audio input server instance
func GetGlobalAudioInputServer() *AudioInputServer {
return globalAudioInputServer
}
// ResetGlobalAudioInputServerStats resets the global audio input server stats
func ResetGlobalAudioInputServerStats() {
if globalAudioInputServer != nil {
globalAudioInputServer.ResetServerStats()
}
}
// RecoverGlobalAudioInputServer attempts to recover from dropped frames
func RecoverGlobalAudioInputServer() {
if globalAudioInputServer != nil {
globalAudioInputServer.RecoverFromDroppedFrames()
}
}
// getEnvInt reads an integer from environment variable with a default value // getEnvInt reads an integer from environment variable with a default value
// RunAudioInputServer runs the audio input server subprocess // RunAudioInputServer runs the audio input server subprocess
@ -55,6 +33,10 @@ func RunAudioInputServer() error {
// Initialize validation cache for optimal performance // Initialize validation cache for optimal performance
InitValidationCache() InitValidationCache()
// Start adaptive buffer management for optimal performance
StartAdaptiveBuffering()
defer StopAdaptiveBuffering()
// Initialize CGO audio playback (optional for input server) // Initialize CGO audio playback (optional for input server)
// This is used for audio loopback/monitoring features // This is used for audio loopback/monitoring features
err := CGOAudioPlaybackInit() err := CGOAudioPlaybackInit()
@ -74,9 +56,6 @@ func RunAudioInputServer() error {
} }
defer server.Close() defer server.Close()
// Store globally for access by other functions
globalAudioInputServer = server
err = server.Start() err = server.Start()
if err != nil { if err != nil {
logger.Error().Err(err).Msg("failed to start audio input server") logger.Error().Err(err).Msg("failed to start audio input server")
@ -103,7 +82,7 @@ func RunAudioInputServer() error {
server.Stop() server.Stop()
// Give some time for cleanup // Give some time for cleanup
time.Sleep(Config.DefaultSleepDuration) time.Sleep(GetConfig().DefaultSleepDuration)
return nil return nil
} }

View File

@ -73,7 +73,7 @@ func (ais *AudioInputSupervisor) supervisionLoop() {
// Configure supervision parameters (no restart for input supervisor) // Configure supervision parameters (no restart for input supervisor)
config := SupervisionConfig{ config := SupervisionConfig{
ProcessType: "audio input server", ProcessType: "audio input server",
Timeout: Config.InputSupervisorTimeout, Timeout: GetConfig().InputSupervisorTimeout,
EnableRestart: false, // Input supervisor doesn't restart EnableRestart: false, // Input supervisor doesn't restart
MaxRestartAttempts: 0, MaxRestartAttempts: 0,
RestartWindow: 0, RestartWindow: 0,
@ -135,9 +135,10 @@ func (ais *AudioInputSupervisor) startProcess() error {
ais.logger.Info().Int("pid", ais.processPID).Strs("args", args).Strs("opus_env", ais.opusEnv).Msg("audio input server process started") ais.logger.Info().Int("pid", ais.processPID).Strs("args", args).Strs("opus_env", ais.opusEnv).Msg("audio input server process started")
// Add process to monitoring // Add process to monitoring
ais.processMonitor.AddProcess(ais.processPID, "audio-input-server")
// Connect client to the server synchronously to avoid race condition // Connect client to the server
ais.connectClient() go ais.connectClient()
return nil return nil
} }
@ -163,7 +164,7 @@ func (ais *AudioInputSupervisor) Stop() {
select { select {
case <-ais.processDone: case <-ais.processDone:
ais.logger.Info().Str("component", "audio-input-supervisor").Msg("component stopped gracefully") ais.logger.Info().Str("component", "audio-input-supervisor").Msg("component stopped gracefully")
case <-time.After(Config.InputSupervisorTimeout): case <-time.After(GetConfig().InputSupervisorTimeout):
ais.logger.Warn().Str("component", "audio-input-supervisor").Msg("component did not stop gracefully, forcing termination") ais.logger.Warn().Str("component", "audio-input-supervisor").Msg("component did not stop gracefully, forcing termination")
ais.forceKillProcess("audio input server") ais.forceKillProcess("audio input server")
} }
@ -189,7 +190,7 @@ func (ais *AudioInputSupervisor) GetClient() *AudioInputClient {
// connectClient attempts to connect the client to the server // connectClient attempts to connect the client to the server
func (ais *AudioInputSupervisor) connectClient() { func (ais *AudioInputSupervisor) connectClient() {
// Wait briefly for the server to start and create socket // Wait briefly for the server to start and create socket
time.Sleep(Config.DefaultSleepDuration) time.Sleep(GetConfig().DefaultSleepDuration)
// Additional small delay to ensure socket is ready after restart // Additional small delay to ensure socket is ready after restart
time.Sleep(20 * time.Millisecond) time.Sleep(20 * time.Millisecond)

View File

@ -49,7 +49,7 @@ func NewGenericMessagePool(size int) *GenericMessagePool {
pool.preallocated = make([]*OptimizedMessage, pool.preallocSize) pool.preallocated = make([]*OptimizedMessage, pool.preallocSize)
for i := 0; i < pool.preallocSize; i++ { for i := 0; i < pool.preallocSize; i++ {
pool.preallocated[i] = &OptimizedMessage{ pool.preallocated[i] = &OptimizedMessage{
data: make([]byte, 0, Config.MaxFrameSize), data: make([]byte, 0, GetConfig().MaxFrameSize),
} }
} }
@ -57,7 +57,7 @@ func NewGenericMessagePool(size int) *GenericMessagePool {
for i := 0; i < size-pool.preallocSize; i++ { for i := 0; i < size-pool.preallocSize; i++ {
select { select {
case pool.pool <- &OptimizedMessage{ case pool.pool <- &OptimizedMessage{
data: make([]byte, 0, Config.MaxFrameSize), data: make([]byte, 0, GetConfig().MaxFrameSize),
}: }:
default: default:
break break
@ -89,7 +89,7 @@ func (mp *GenericMessagePool) Get() *OptimizedMessage {
// Pool empty, create new message // Pool empty, create new message
atomic.AddInt64(&mp.missCount, 1) atomic.AddInt64(&mp.missCount, 1)
return &OptimizedMessage{ return &OptimizedMessage{
data: make([]byte, 0, Config.MaxFrameSize), data: make([]byte, 0, GetConfig().MaxFrameSize),
} }
} }
} }
@ -132,42 +132,6 @@ func (mp *GenericMessagePool) GetStats() (hitCount, missCount int64, hitRate flo
return hits, misses, hitRate return hits, misses, hitRate
} }
// Helper functions
// EncodeMessageHeader encodes a message header into a byte slice
func EncodeMessageHeader(magic uint32, msgType uint8, length uint32, timestamp int64) []byte {
header := make([]byte, 17)
binary.LittleEndian.PutUint32(header[0:4], magic)
header[4] = msgType
binary.LittleEndian.PutUint32(header[5:9], length)
binary.LittleEndian.PutUint64(header[9:17], uint64(timestamp))
return header
}
// EncodeAudioConfig encodes basic audio configuration to binary format
func EncodeAudioConfig(sampleRate, channels, frameSize int) []byte {
data := make([]byte, 12) // 3 * int32
binary.LittleEndian.PutUint32(data[0:4], uint32(sampleRate))
binary.LittleEndian.PutUint32(data[4:8], uint32(channels))
binary.LittleEndian.PutUint32(data[8:12], uint32(frameSize))
return data
}
// EncodeOpusConfig encodes complete Opus configuration to binary format
func EncodeOpusConfig(sampleRate, channels, frameSize, bitrate, complexity, vbr, signalType, bandwidth, dtx int) []byte {
data := make([]byte, 36) // 9 * int32
binary.LittleEndian.PutUint32(data[0:4], uint32(sampleRate))
binary.LittleEndian.PutUint32(data[4:8], uint32(channels))
binary.LittleEndian.PutUint32(data[8:12], uint32(frameSize))
binary.LittleEndian.PutUint32(data[12:16], uint32(bitrate))
binary.LittleEndian.PutUint32(data[16:20], uint32(complexity))
binary.LittleEndian.PutUint32(data[20:24], uint32(vbr))
binary.LittleEndian.PutUint32(data[24:28], uint32(signalType))
binary.LittleEndian.PutUint32(data[28:32], uint32(bandwidth))
binary.LittleEndian.PutUint32(data[32:36], uint32(dtx))
return data
}
// Common write message function // Common write message function
func WriteIPCMessage(conn net.Conn, msg IPCMessage, pool *GenericMessagePool, droppedFramesCounter *int64) error { func WriteIPCMessage(conn net.Conn, msg IPCMessage, pool *GenericMessagePool, droppedFramesCounter *int64) error {
if conn == nil { if conn == nil {
@ -179,11 +143,13 @@ func WriteIPCMessage(conn net.Conn, msg IPCMessage, pool *GenericMessagePool, dr
defer pool.Put(optMsg) defer pool.Put(optMsg)
// Prepare header in pre-allocated buffer // Prepare header in pre-allocated buffer
header := EncodeMessageHeader(msg.GetMagic(), msg.GetType(), msg.GetLength(), msg.GetTimestamp()) binary.LittleEndian.PutUint32(optMsg.header[0:4], msg.GetMagic())
copy(optMsg.header[:], header) optMsg.header[4] = msg.GetType()
binary.LittleEndian.PutUint32(optMsg.header[5:9], msg.GetLength())
binary.LittleEndian.PutUint64(optMsg.header[9:17], uint64(msg.GetTimestamp()))
// Set write deadline for timeout handling (more efficient than goroutines) // Set write deadline for timeout handling (more efficient than goroutines)
if deadline := time.Now().Add(Config.WriteTimeout); deadline.After(time.Now()) { if deadline := time.Now().Add(GetConfig().WriteTimeout); deadline.After(time.Now()) {
if err := conn.SetWriteDeadline(deadline); err != nil { if err := conn.SetWriteDeadline(deadline); err != nil {
// If we can't set deadline, proceed without it // If we can't set deadline, proceed without it
// This maintains compatibility with connections that don't support deadlines // This maintains compatibility with connections that don't support deadlines

View File

@ -23,8 +23,8 @@ const (
// Constants are now defined in unified_ipc.go // Constants are now defined in unified_ipc.go
var ( var (
maxFrameSize = Config.MaxFrameSize // Maximum Opus frame size maxFrameSize = GetConfig().MaxFrameSize // Maximum Opus frame size
messagePoolSize = Config.MessagePoolSize // Pre-allocated message pool size messagePoolSize = GetConfig().MessagePoolSize // Pre-allocated message pool size
) )
// Legacy aliases for backward compatibility // Legacy aliases for backward compatibility
@ -77,7 +77,7 @@ func initializeMessagePool() {
messagePoolInitOnce.Do(func() { messagePoolInitOnce.Do(func() {
preallocSize := messagePoolSize / 4 // 25% pre-allocated for immediate use preallocSize := messagePoolSize / 4 // 25% pre-allocated for immediate use
globalMessagePool.preallocSize = preallocSize globalMessagePool.preallocSize = preallocSize
globalMessagePool.maxPoolSize = messagePoolSize * Config.PoolGrowthMultiplier // Allow growth up to 2x globalMessagePool.maxPoolSize = messagePoolSize * GetConfig().PoolGrowthMultiplier // Allow growth up to 2x
globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize) globalMessagePool.preallocated = make([]*OptimizedIPCMessage, 0, preallocSize)
// Pre-allocate messages for immediate use // Pre-allocate messages for immediate use
@ -191,10 +191,6 @@ type AudioInputServer struct {
stopChan chan struct{} // Stop signal for all goroutines stopChan chan struct{} // Stop signal for all goroutines
wg sync.WaitGroup // Wait group for goroutine coordination wg sync.WaitGroup // Wait group for goroutine coordination
// Channel resizing support
channelMutex sync.RWMutex // Protects channel recreation
lastBufferSize int64 // Last known buffer size for change detection
// Socket buffer configuration // Socket buffer configuration
socketBufferConfig SocketBufferConfig socketBufferConfig SocketBufferConfig
} }
@ -231,15 +227,9 @@ func NewAudioInputServer() (*AudioInputServer, error) {
return nil, fmt.Errorf("failed to create unix socket after 3 attempts: %w", err) return nil, fmt.Errorf("failed to create unix socket after 3 attempts: %w", err)
} }
// Get initial buffer size from config // Get initial buffer size from adaptive buffer manager
initialBufferSize := int64(Config.AdaptiveDefaultBufferSize) adaptiveManager := GetAdaptiveBufferManager()
initialBufferSize := int64(adaptiveManager.GetInputBufferSize())
// Ensure minimum buffer size to prevent immediate overflow
// Use at least 50 frames to handle burst traffic
minBufferSize := int64(50)
if initialBufferSize < minBufferSize {
initialBufferSize = minBufferSize
}
// Initialize socket buffer configuration // Initialize socket buffer configuration
socketBufferConfig := DefaultSocketBufferConfig() socketBufferConfig := DefaultSocketBufferConfig()
@ -250,7 +240,6 @@ func NewAudioInputServer() (*AudioInputServer, error) {
processChan: make(chan *InputIPCMessage, initialBufferSize), processChan: make(chan *InputIPCMessage, initialBufferSize),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
bufferSize: initialBufferSize, bufferSize: initialBufferSize,
lastBufferSize: initialBufferSize,
socketBufferConfig: socketBufferConfig, socketBufferConfig: socketBufferConfig,
}, nil }, nil
} }
@ -377,7 +366,7 @@ func (ais *AudioInputServer) handleConnection(conn net.Conn) {
if ais.conn == nil { if ais.conn == nil {
return return
} }
time.Sleep(Config.DefaultSleepDuration) time.Sleep(GetConfig().DefaultSleepDuration)
} }
} }
} }
@ -498,11 +487,11 @@ func (ais *AudioInputServer) processOpusFrame(data []byte) error {
} }
// Get cached config once - avoid repeated calls and locking // Get cached config once - avoid repeated calls and locking
cache := Config cache := GetCachedConfig()
// Skip cache expiry check in hotpath - background updates handle this // Skip cache expiry check in hotpath - background updates handle this
// Get a PCM buffer from the pool for optimized decode-write // Get a PCM buffer from the pool for optimized decode-write
pcmBuffer := GetBufferFromPool(cache.MaxPCMBufferSize) pcmBuffer := GetBufferFromPool(cache.GetMaxPCMBufferSize())
defer ReturnBufferToPool(pcmBuffer) defer ReturnBufferToPool(pcmBuffer)
// Direct CGO call - avoid wrapper function overhead // Direct CGO call - avoid wrapper function overhead
@ -645,9 +634,9 @@ func (aic *AudioInputClient) Connect() error {
return nil return nil
} }
// Exponential backoff starting from config // Exponential backoff starting from config
backoffStart := Config.BackoffStart backoffStart := GetConfig().BackoffStart
delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
maxDelay := Config.MaxRetryDelay maxDelay := GetConfig().MaxRetryDelay
if delay > maxDelay { if delay > maxDelay {
delay = maxDelay delay = maxDelay
} }
@ -688,28 +677,32 @@ func (aic *AudioInputClient) Disconnect() {
// SendFrame sends an Opus frame to the audio input server // SendFrame sends an Opus frame to the audio input server
func (aic *AudioInputClient) SendFrame(frame []byte) error { func (aic *AudioInputClient) SendFrame(frame []byte) error {
// Fast path validation
if len(frame) == 0 {
return nil
}
aic.mtx.Lock() aic.mtx.Lock()
defer aic.mtx.Unlock()
if !aic.running || aic.conn == nil { if !aic.running || aic.conn == nil {
aic.mtx.Unlock() return fmt.Errorf("not connected to audio input server")
return fmt.Errorf("not connected") }
frameLen := len(frame)
if frameLen == 0 {
return nil // Empty frame, ignore
}
// Inline frame validation to reduce function call overhead
if frameLen > maxFrameSize {
return ErrFrameDataTooLarge
} }
// Direct message creation without timestamp overhead
msg := &InputIPCMessage{ msg := &InputIPCMessage{
Magic: inputMagicNumber, Magic: inputMagicNumber,
Type: InputMessageTypeOpusFrame, Type: InputMessageTypeOpusFrame,
Length: uint32(len(frame)), Length: uint32(frameLen),
Data: frame, Timestamp: time.Now().UnixNano(),
Data: frame,
} }
err := aic.writeMessage(msg) return aic.writeMessage(msg)
aic.mtx.Unlock()
return err
} }
// SendFrameZeroCopy sends a zero-copy Opus frame to the audio input server // SendFrameZeroCopy sends a zero-copy Opus frame to the audio input server
@ -763,8 +756,11 @@ func (aic *AudioInputClient) SendConfig(config InputIPCConfig) error {
return fmt.Errorf("input configuration validation failed: %w", err) return fmt.Errorf("input configuration validation failed: %w", err)
} }
// Serialize config using common function // Serialize config (simple binary format)
data := EncodeAudioConfig(config.SampleRate, config.Channels, config.FrameSize) data := make([]byte, 12) // 3 * int32
binary.LittleEndian.PutUint32(data[0:4], uint32(config.SampleRate))
binary.LittleEndian.PutUint32(data[4:8], uint32(config.Channels))
binary.LittleEndian.PutUint32(data[8:12], uint32(config.FrameSize))
msg := &InputIPCMessage{ msg := &InputIPCMessage{
Magic: inputMagicNumber, Magic: inputMagicNumber,
@ -792,8 +788,17 @@ func (aic *AudioInputClient) SendOpusConfig(config InputIPCOpusConfig) error {
config.SampleRate, config.Channels, config.FrameSize, config.Bitrate) config.SampleRate, config.Channels, config.FrameSize, config.Bitrate)
} }
// Serialize Opus configuration using common function // Serialize Opus configuration (9 * int32 = 36 bytes)
data := EncodeOpusConfig(config.SampleRate, config.Channels, config.FrameSize, config.Bitrate, config.Complexity, config.VBR, config.SignalType, config.Bandwidth, config.DTX) data := make([]byte, 36)
binary.LittleEndian.PutUint32(data[0:4], uint32(config.SampleRate))
binary.LittleEndian.PutUint32(data[4:8], uint32(config.Channels))
binary.LittleEndian.PutUint32(data[8:12], uint32(config.FrameSize))
binary.LittleEndian.PutUint32(data[12:16], uint32(config.Bitrate))
binary.LittleEndian.PutUint32(data[16:20], uint32(config.Complexity))
binary.LittleEndian.PutUint32(data[20:24], uint32(config.VBR))
binary.LittleEndian.PutUint32(data[24:28], uint32(config.SignalType))
binary.LittleEndian.PutUint32(data[28:32], uint32(config.Bandwidth))
binary.LittleEndian.PutUint32(data[32:36], uint32(config.DTX))
msg := &InputIPCMessage{ msg := &InputIPCMessage{
Magic: inputMagicNumber, Magic: inputMagicNumber,
@ -861,28 +866,6 @@ func (aic *AudioInputClient) ResetStats() {
ResetFrameStats(&aic.totalFrames, &aic.droppedFrames) ResetFrameStats(&aic.totalFrames, &aic.droppedFrames)
} }
// ResetServerStats resets server frame statistics
func (ais *AudioInputServer) ResetServerStats() {
atomic.StoreInt64(&ais.totalFrames, 0)
atomic.StoreInt64(&ais.droppedFrames, 0)
}
// RecoverFromDroppedFrames attempts to recover when too many frames are dropped
func (ais *AudioInputServer) RecoverFromDroppedFrames() {
total := atomic.LoadInt64(&ais.totalFrames)
dropped := atomic.LoadInt64(&ais.droppedFrames)
// If more than 50% of frames are dropped, attempt recovery
if total > 100 && dropped > total/2 {
logger := logging.GetDefaultLogger().With().Str("component", AudioInputServerComponent).Logger()
logger.Warn().Int64("total", total).Int64("dropped", dropped).Msg("high drop rate detected, attempting recovery")
// Reset stats and update buffer size from adaptive manager
ais.ResetServerStats()
ais.UpdateBufferSize()
}
}
// startReaderGoroutine starts the message reader using the goroutine pool // startReaderGoroutine starts the message reader using the goroutine pool
func (ais *AudioInputServer) startReaderGoroutine() { func (ais *AudioInputServer) startReaderGoroutine() {
ais.wg.Add(1) ais.wg.Add(1)
@ -894,10 +877,10 @@ func (ais *AudioInputServer) startReaderGoroutine() {
// Enhanced error tracking and recovery // Enhanced error tracking and recovery
var consecutiveErrors int var consecutiveErrors int
var lastErrorTime time.Time var lastErrorTime time.Time
maxConsecutiveErrors := Config.MaxConsecutiveErrors maxConsecutiveErrors := GetConfig().MaxConsecutiveErrors
errorResetWindow := Config.RestartWindow // Use existing restart window errorResetWindow := GetConfig().RestartWindow // Use existing restart window
baseBackoffDelay := Config.RetryDelay baseBackoffDelay := GetConfig().RetryDelay
maxBackoffDelay := Config.MaxRetryDelay maxBackoffDelay := GetConfig().MaxRetryDelay
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger() logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
@ -967,13 +950,9 @@ func (ais *AudioInputServer) startReaderGoroutine() {
} }
} }
// Send to message channel with non-blocking write (use read lock for channel access) // Send to message channel with non-blocking write
ais.channelMutex.RLock()
messageChan := ais.messageChan
ais.channelMutex.RUnlock()
select { select {
case messageChan <- msg: case ais.messageChan <- msg:
atomic.AddInt64(&ais.totalFrames, 1) atomic.AddInt64(&ais.totalFrames, 1)
default: default:
// Channel full, drop message // Channel full, drop message
@ -987,16 +966,16 @@ func (ais *AudioInputServer) startReaderGoroutine() {
} }
} }
// Submit the reader task to the audio reader pool with backpressure // Submit the reader task to the audio reader pool
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger() logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
if !SubmitAudioReaderTaskWithBackpressure(readerTask) { if !SubmitAudioReaderTask(readerTask) {
// Task was dropped due to backpressure - this is expected under high load // If the pool is full or shutting down, fall back to direct goroutine creation
// Log at debug level to avoid spam, but track the drop // Only log if warn level enabled - avoid sampling logic in critical path
logger.Debug().Msg("Audio reader task dropped due to backpressure") if logger.GetLevel() <= zerolog.WarnLevel {
logger.Warn().Msg("Audio reader pool full or shutting down, falling back to direct goroutine creation")
}
// Don't fall back to unlimited goroutine creation go readerTask()
// Instead, let the system recover naturally
ais.wg.Done() // Decrement the wait group since we're not starting the task
} }
} }
@ -1008,7 +987,7 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
processorTask := func() { processorTask := func() {
// Only lock OS thread and set priority for high-load scenarios // Only lock OS thread and set priority for high-load scenarios
// This reduces interference with input processing threads // This reduces interference with input processing threads
config := Config config := GetConfig()
useThreadOptimizations := config.MaxAudioProcessorWorkers > 8 useThreadOptimizations := config.MaxAudioProcessorWorkers > 8
if useThreadOptimizations { if useThreadOptimizations {
@ -1032,7 +1011,7 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
select { select {
case <-ais.stopChan: case <-ais.stopChan:
return return
case msg := <-ais.getMessageChan(): case msg := <-ais.messageChan:
// Process message with error handling // Process message with error handling
start := time.Now() start := time.Now()
err := ais.processMessageWithRecovery(msg, logger) err := ais.processMessageWithRecovery(msg, logger)
@ -1053,10 +1032,9 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
// If too many processing errors, drop frames more aggressively // If too many processing errors, drop frames more aggressively
if processingErrors >= maxProcessingErrors { if processingErrors >= maxProcessingErrors {
// Clear processing queue to recover // Clear processing queue to recover
processChan := ais.getProcessChan() for len(ais.processChan) > 0 {
for len(processChan) > 0 {
select { select {
case <-processChan: case <-ais.processChan:
atomic.AddInt64(&ais.droppedFrames, 1) atomic.AddInt64(&ais.droppedFrames, 1)
default: default:
break break
@ -1079,16 +1057,13 @@ func (ais *AudioInputServer) startProcessorGoroutine() {
} }
} }
// Submit the processor task to the audio processor pool with backpressure // Submit the processor task to the audio processor pool
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger() logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
if !SubmitAudioProcessorTaskWithBackpressure(processorTask) { if !SubmitAudioProcessorTask(processorTask) {
// Task was dropped due to backpressure - this is expected under high load // If the pool is full or shutting down, fall back to direct goroutine creation
// Log at debug level to avoid spam, but track the drop logger.Warn().Msg("Audio processor pool full or shutting down, falling back to direct goroutine creation")
logger.Debug().Msg("Audio processor task dropped due to backpressure")
// Don't fall back to unlimited goroutine creation go processorTask()
// Instead, let the system recover naturally
ais.wg.Done() // Decrement the wait group since we're not starting the task
} }
} }
@ -1097,14 +1072,13 @@ func (ais *AudioInputServer) processMessageWithRecovery(msg *InputIPCMessage, lo
// Intelligent frame dropping: prioritize recent frames // Intelligent frame dropping: prioritize recent frames
if msg.Type == InputMessageTypeOpusFrame { if msg.Type == InputMessageTypeOpusFrame {
// Check if processing queue is getting full // Check if processing queue is getting full
processChan := ais.getProcessChan() queueLen := len(ais.processChan)
queueLen := len(processChan)
bufferSize := int(atomic.LoadInt64(&ais.bufferSize)) bufferSize := int(atomic.LoadInt64(&ais.bufferSize))
if queueLen > bufferSize*3/4 { if queueLen > bufferSize*3/4 {
// Drop oldest frames, keep newest // Drop oldest frames, keep newest
select { select {
case <-processChan: // Remove oldest case <-ais.processChan: // Remove oldest
atomic.AddInt64(&ais.droppedFrames, 1) atomic.AddInt64(&ais.droppedFrames, 1)
logger.Debug().Msg("Dropped oldest frame to make room") logger.Debug().Msg("Dropped oldest frame to make room")
default: default:
@ -1112,15 +1086,11 @@ func (ais *AudioInputServer) processMessageWithRecovery(msg *InputIPCMessage, lo
} }
} }
// Send to processing queue with timeout (use read lock for channel access) // Send to processing queue with timeout
ais.channelMutex.RLock()
processChan := ais.processChan
ais.channelMutex.RUnlock()
select { select {
case processChan <- msg: case ais.processChan <- msg:
return nil return nil
case <-time.After(Config.WriteTimeout): case <-time.After(GetConfig().WriteTimeout):
// Processing queue full and timeout reached, drop frame // Processing queue full and timeout reached, drop frame
atomic.AddInt64(&ais.droppedFrames, 1) atomic.AddInt64(&ais.droppedFrames, 1)
return fmt.Errorf("processing queue timeout") return fmt.Errorf("processing queue timeout")
@ -1139,7 +1109,7 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
monitorTask := func() { monitorTask := func() {
// Monitor goroutine doesn't need thread locking for most scenarios // Monitor goroutine doesn't need thread locking for most scenarios
// Only use thread optimizations for high-throughput scenarios // Only use thread optimizations for high-throughput scenarios
config := Config config := GetConfig()
useThreadOptimizations := config.MaxAudioProcessorWorkers > 8 useThreadOptimizations := config.MaxAudioProcessorWorkers > 8
if useThreadOptimizations { if useThreadOptimizations {
@ -1150,11 +1120,11 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
} }
defer ais.wg.Done() defer ais.wg.Done()
ticker := time.NewTicker(Config.DefaultTickerInterval) ticker := time.NewTicker(GetConfig().DefaultTickerInterval)
defer ticker.Stop() defer ticker.Stop()
// Buffer size update ticker (less frequent) // Buffer size update ticker (less frequent)
bufferUpdateTicker := time.NewTicker(Config.BufferUpdateInterval) bufferUpdateTicker := time.NewTicker(GetConfig().BufferUpdateInterval)
defer bufferUpdateTicker.Stop() defer bufferUpdateTicker.Stop()
for { for {
@ -1165,7 +1135,7 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
// Process frames from processing queue // Process frames from processing queue
for { for {
select { select {
case msg := <-ais.getProcessChan(): case msg := <-ais.processChan:
start := time.Now() start := time.Now()
err := ais.processMessage(msg) err := ais.processMessage(msg)
processingTime := time.Since(start) processingTime := time.Since(start)
@ -1204,7 +1174,8 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
// Check if we need to update buffer size // Check if we need to update buffer size
select { select {
case <-bufferUpdateTicker.C: case <-bufferUpdateTicker.C:
// Buffer size is now fixed from config // Update buffer size from adaptive buffer manager
ais.UpdateBufferSize()
default: default:
// No buffer update needed // No buffer update needed
} }
@ -1212,16 +1183,13 @@ func (ais *AudioInputServer) startMonitorGoroutine() {
} }
} }
// Submit the monitor task to the audio processor pool with backpressure // Submit the monitor task to the audio processor pool
logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger() logger := logging.GetDefaultLogger().With().Str("component", AudioInputClientComponent).Logger()
if !SubmitAudioProcessorTaskWithBackpressure(monitorTask) { if !SubmitAudioProcessorTask(monitorTask) {
// Task was dropped due to backpressure - this is expected under high load // If the pool is full or shutting down, fall back to direct goroutine creation
// Log at debug level to avoid spam, but track the drop logger.Warn().Msg("Audio processor pool full or shutting down, falling back to direct goroutine creation")
logger.Debug().Msg("Audio monitor task dropped due to backpressure")
// Don't fall back to unlimited goroutine creation go monitorTask()
// Instead, let the system recover naturally
ais.wg.Done() // Decrement the wait group since we're not starting the task
} }
} }
@ -1233,16 +1201,17 @@ func (ais *AudioInputServer) GetServerStats() (total, dropped int64, avgProcessi
atomic.LoadInt64(&ais.bufferSize) atomic.LoadInt64(&ais.bufferSize)
} }
// UpdateBufferSize updates the buffer size (now using fixed config values) // UpdateBufferSize updates the buffer size from adaptive buffer manager
func (ais *AudioInputServer) UpdateBufferSize() { func (ais *AudioInputServer) UpdateBufferSize() {
// Buffer size is now fixed from config adaptiveManager := GetAdaptiveBufferManager()
newSize := int64(Config.AdaptiveDefaultBufferSize) newSize := int64(adaptiveManager.GetInputBufferSize())
atomic.StoreInt64(&ais.bufferSize, newSize) atomic.StoreInt64(&ais.bufferSize, newSize)
} }
// ReportLatency reports processing latency (now a no-op with fixed buffers) // ReportLatency reports processing latency to adaptive buffer manager
func (ais *AudioInputServer) ReportLatency(latency time.Duration) { func (ais *AudioInputServer) ReportLatency(latency time.Duration) {
// Latency reporting is now a no-op with fixed buffer sizes adaptiveManager := GetAdaptiveBufferManager()
adaptiveManager.UpdateLatency(latency)
} }
// GetMessagePoolStats returns detailed statistics about the message pool // GetMessagePoolStats returns detailed statistics about the message pool
@ -1257,7 +1226,7 @@ func (mp *MessagePool) GetMessagePoolStats() MessagePoolStats {
var hitRate float64 var hitRate float64
if totalRequests > 0 { if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * Config.PercentageMultiplier hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
} }
// Calculate channel pool size // Calculate channel pool size
@ -1290,20 +1259,6 @@ func GetGlobalMessagePoolStats() MessagePoolStats {
return globalMessagePool.GetMessagePoolStats() return globalMessagePool.GetMessagePoolStats()
} }
// getMessageChan safely returns the current message channel
func (ais *AudioInputServer) getMessageChan() chan *InputIPCMessage {
ais.channelMutex.RLock()
defer ais.channelMutex.RUnlock()
return ais.messageChan
}
// getProcessChan safely returns the current process channel
func (ais *AudioInputServer) getProcessChan() chan *InputIPCMessage {
ais.channelMutex.RLock()
defer ais.channelMutex.RUnlock()
return ais.processChan
}
// Helper functions // Helper functions
// getInputSocketPath is now defined in unified_ipc.go // getInputSocketPath is now defined in unified_ipc.go

View File

@ -4,393 +4,67 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "io"
"net"
"sync"
"sync/atomic" "sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
) )
// Legacy aliases for backward compatibility // Legacy aliases for backward compatibility
type OutputIPCConfig = UnifiedIPCConfig type OutputIPCConfig = UnifiedIPCConfig
type OutputIPCOpusConfig = UnifiedIPCOpusConfig
type OutputMessageType = UnifiedMessageType type OutputMessageType = UnifiedMessageType
type OutputIPCMessage = UnifiedIPCMessage type OutputIPCMessage = UnifiedIPCMessage
// Legacy constants for backward compatibility // Legacy constants for backward compatibility
const ( const (
OutputMessageTypeOpusFrame = MessageTypeOpusFrame OutputMessageTypeOpusFrame = MessageTypeOpusFrame
OutputMessageTypeConfig = MessageTypeConfig OutputMessageTypeConfig = MessageTypeConfig
OutputMessageTypeOpusConfig = MessageTypeOpusConfig OutputMessageTypeStop = MessageTypeStop
OutputMessageTypeStop = MessageTypeStop OutputMessageTypeHeartbeat = MessageTypeHeartbeat
OutputMessageTypeHeartbeat = MessageTypeHeartbeat OutputMessageTypeAck = MessageTypeAck
OutputMessageTypeAck = MessageTypeAck
) )
// Methods are now inherited from UnifiedIPCMessage // Methods are now inherited from UnifiedIPCMessage
// Global shared message pool for output IPC client header reading // Global shared message pool for output IPC client header reading
var globalOutputClientMessagePool = NewGenericMessagePool(Config.OutputMessagePoolSize) var globalOutputClientMessagePool = NewGenericMessagePool(GetConfig().OutputMessagePoolSize)
// AudioOutputServer provides audio output IPC functionality // AudioOutputServer is now an alias for UnifiedAudioServer
type AudioOutputServer struct { type AudioOutputServer = UnifiedAudioServer
// Atomic counters
bufferSize int64 // Current buffer size (atomic)
droppedFrames int64 // Dropped frames counter (atomic)
totalFrames int64 // Total frames counter (atomic)
listener net.Listener
conn net.Conn
mtx sync.Mutex
running bool
logger zerolog.Logger
// Message channels
messageChan chan *OutputIPCMessage // Buffered channel for incoming messages
processChan chan *OutputIPCMessage // Buffered channel for processing queue
wg sync.WaitGroup // Wait group for goroutine coordination
// Configuration
socketPath string
magicNumber uint32
}
func NewAudioOutputServer() (*AudioOutputServer, error) { func NewAudioOutputServer() (*AudioOutputServer, error) {
socketPath := getOutputSocketPath() return NewUnifiedAudioServer(false) // false = output server
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-server").Logger()
server := &AudioOutputServer{
socketPath: socketPath,
magicNumber: Config.OutputMagicNumber,
logger: logger,
messageChan: make(chan *OutputIPCMessage, Config.ChannelBufferSize),
processChan: make(chan *OutputIPCMessage, Config.ChannelBufferSize),
}
return server, nil
} }
// Start method is now inherited from UnifiedAudioServer
// acceptConnections method is now inherited from UnifiedAudioServer
// startProcessorGoroutine method is now inherited from UnifiedAudioServer
// Stop method is now inherited from UnifiedAudioServer
// Close method is now inherited from UnifiedAudioServer
// SendFrame method is now inherited from UnifiedAudioServer
// GetServerStats returns server performance statistics // GetServerStats returns server performance statistics
// Start starts the audio output server
func (s *AudioOutputServer) Start() error {
s.mtx.Lock()
defer s.mtx.Unlock()
if s.running {
return fmt.Errorf("audio output server is already running")
}
// Create Unix socket
listener, err := net.Listen("unix", s.socketPath)
if err != nil {
return fmt.Errorf("failed to create unix socket: %w", err)
}
s.listener = listener
s.running = true
// Start goroutines
s.wg.Add(1)
go s.acceptConnections()
s.logger.Info().Str("socket_path", s.socketPath).Msg("Audio output server started")
return nil
}
// Stop stops the audio output server
func (s *AudioOutputServer) Stop() {
s.mtx.Lock()
defer s.mtx.Unlock()
if !s.running {
return
}
s.running = false
if s.listener != nil {
s.listener.Close()
}
if s.conn != nil {
s.conn.Close()
}
// Close channels
close(s.messageChan)
close(s.processChan)
s.wg.Wait()
s.logger.Info().Msg("Audio output server stopped")
}
// acceptConnections handles incoming connections
func (s *AudioOutputServer) acceptConnections() {
defer s.wg.Done()
for s.running {
conn, err := s.listener.Accept()
if err != nil {
if s.running {
s.logger.Error().Err(err).Msg("Failed to accept connection")
}
return
}
s.mtx.Lock()
s.conn = conn
s.mtx.Unlock()
s.logger.Info().Msg("Client connected to audio output server")
// Start message processing for this connection
s.wg.Add(1)
go s.handleConnection(conn)
}
}
// handleConnection processes messages from a client connection
func (s *AudioOutputServer) handleConnection(conn net.Conn) {
defer s.wg.Done()
defer conn.Close()
for s.running {
msg, err := s.readMessage(conn)
if err != nil {
if s.running {
s.logger.Error().Err(err).Msg("Failed to read message from client")
}
return
}
if err := s.processMessage(msg); err != nil {
s.logger.Error().Err(err).Msg("Failed to process message")
}
}
}
// readMessage reads a message from the connection
func (s *AudioOutputServer) readMessage(conn net.Conn) (*OutputIPCMessage, error) {
header := make([]byte, 17)
if _, err := io.ReadFull(conn, header); err != nil {
return nil, fmt.Errorf("failed to read header: %w", err)
}
magic := binary.LittleEndian.Uint32(header[0:4])
if magic != s.magicNumber {
return nil, fmt.Errorf("invalid magic number: expected %d, got %d", s.magicNumber, magic)
}
msgType := OutputMessageType(header[4])
length := binary.LittleEndian.Uint32(header[5:9])
timestamp := int64(binary.LittleEndian.Uint64(header[9:17]))
var data []byte
if length > 0 {
data = make([]byte, length)
if _, err := io.ReadFull(conn, data); err != nil {
return nil, fmt.Errorf("failed to read data: %w", err)
}
}
return &OutputIPCMessage{
Magic: magic,
Type: msgType,
Length: length,
Timestamp: timestamp,
Data: data,
}, nil
}
// processMessage processes a received message
func (s *AudioOutputServer) processMessage(msg *OutputIPCMessage) error {
switch msg.Type {
case OutputMessageTypeOpusConfig:
return s.processOpusConfig(msg.Data)
case OutputMessageTypeStop:
s.logger.Info().Msg("Received stop message")
return nil
case OutputMessageTypeHeartbeat:
s.logger.Debug().Msg("Received heartbeat")
return nil
default:
s.logger.Warn().Int("type", int(msg.Type)).Msg("Unknown message type")
return nil
}
}
// processOpusConfig processes Opus configuration updates
func (s *AudioOutputServer) processOpusConfig(data []byte) error {
// Validate configuration data size (9 * int32 = 36 bytes)
if len(data) != 36 {
return fmt.Errorf("invalid Opus configuration data size: expected 36 bytes, got %d", len(data))
}
// Decode Opus configuration
config := OutputIPCOpusConfig{
SampleRate: int(binary.LittleEndian.Uint32(data[0:4])),
Channels: int(binary.LittleEndian.Uint32(data[4:8])),
FrameSize: int(binary.LittleEndian.Uint32(data[8:12])),
Bitrate: int(binary.LittleEndian.Uint32(data[12:16])),
Complexity: int(binary.LittleEndian.Uint32(data[16:20])),
VBR: int(binary.LittleEndian.Uint32(data[20:24])),
SignalType: int(binary.LittleEndian.Uint32(data[24:28])),
Bandwidth: int(binary.LittleEndian.Uint32(data[28:32])),
DTX: int(binary.LittleEndian.Uint32(data[32:36])),
}
s.logger.Info().Interface("config", config).Msg("Received Opus configuration update")
// Ensure we're running in the audio server subprocess
if !isAudioServerProcess() {
s.logger.Warn().Msg("Opus configuration update ignored - not running in audio server subprocess")
return nil
}
// Check if audio output streaming is currently active
if atomic.LoadInt32(&outputStreamingRunning) == 0 {
s.logger.Info().Msg("Audio output streaming not active, configuration will be applied when streaming starts")
return nil
}
// Ensure capture is initialized before updating encoder parameters
// The C function requires both encoder and capture_initialized to be true
if err := cgoAudioInit(); err != nil {
s.logger.Debug().Err(err).Msg("Audio capture already initialized or initialization failed")
// Continue anyway - capture may already be initialized
}
// Apply configuration using CGO function (only if audio system is running)
vbrConstraint := Config.CGOOpusVBRConstraint
if err := updateOpusEncoderParams(config.Bitrate, config.Complexity, config.VBR, vbrConstraint, config.SignalType, config.Bandwidth, config.DTX); err != nil {
s.logger.Error().Err(err).Msg("Failed to update Opus encoder parameters - encoder may not be initialized")
return err
}
s.logger.Info().Msg("Opus encoder parameters updated successfully")
return nil
}
// SendFrame sends an audio frame to the client
func (s *AudioOutputServer) SendFrame(frame []byte) error {
s.mtx.Lock()
conn := s.conn
s.mtx.Unlock()
if conn == nil {
return fmt.Errorf("no client connected")
}
msg := &OutputIPCMessage{
Magic: s.magicNumber,
Type: OutputMessageTypeOpusFrame,
Length: uint32(len(frame)),
Timestamp: time.Now().UnixNano(),
Data: frame,
}
return s.writeMessage(conn, msg)
}
// writeMessage writes a message to the connection
func (s *AudioOutputServer) writeMessage(conn net.Conn, msg *OutputIPCMessage) error {
header := EncodeMessageHeader(msg.Magic, uint8(msg.Type), msg.Length, msg.Timestamp)
if _, err := conn.Write(header); err != nil {
return fmt.Errorf("failed to write header: %w", err)
}
if msg.Length > 0 && msg.Data != nil {
if _, err := conn.Write(msg.Data); err != nil {
return fmt.Errorf("failed to write data: %w", err)
}
}
atomic.AddInt64(&s.totalFrames, 1)
return nil
}
func (s *AudioOutputServer) GetServerStats() (total, dropped int64, bufferSize int64) { func (s *AudioOutputServer) GetServerStats() (total, dropped int64, bufferSize int64) {
return atomic.LoadInt64(&s.totalFrames), atomic.LoadInt64(&s.droppedFrames), atomic.LoadInt64(&s.bufferSize) stats := GetFrameStats(&s.totalFrames, &s.droppedFrames)
return stats.Total, stats.Dropped, atomic.LoadInt64(&s.bufferSize)
} }
// AudioOutputClient provides audio output IPC client functionality // AudioOutputClient is now an alias for UnifiedAudioClient
type AudioOutputClient struct { type AudioOutputClient = UnifiedAudioClient
// Atomic counters
droppedFrames int64 // Atomic counter for dropped frames
totalFrames int64 // Atomic counter for total frames
conn net.Conn
mtx sync.Mutex
running bool
logger zerolog.Logger
socketPath string
magicNumber uint32
bufferPool *AudioBufferPool // Buffer pool for memory optimization
// Health monitoring
autoReconnect bool // Enable automatic reconnection
}
func NewAudioOutputClient() *AudioOutputClient { func NewAudioOutputClient() *AudioOutputClient {
socketPath := getOutputSocketPath() return NewUnifiedAudioClient(false) // false = output client
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-client").Logger()
return &AudioOutputClient{
socketPath: socketPath,
magicNumber: Config.OutputMagicNumber,
logger: logger,
bufferPool: NewAudioBufferPool(Config.MaxFrameSize),
autoReconnect: true,
}
} }
// Connect connects to the audio output server // Connect method is now inherited from UnifiedAudioClient
func (c *AudioOutputClient) Connect() error {
c.mtx.Lock()
defer c.mtx.Unlock()
if c.running { // Disconnect method is now inherited from UnifiedAudioClient
return fmt.Errorf("audio output client is already connected")
}
conn, err := net.Dial("unix", c.socketPath) // IsConnected method is now inherited from UnifiedAudioClient
if err != nil {
return fmt.Errorf("failed to connect to audio output server: %w", err)
}
c.conn = conn // Close method is now inherited from UnifiedAudioClient
c.running = true
c.logger.Info().Str("socket_path", c.socketPath).Msg("Connected to audio output server")
return nil
}
// Disconnect disconnects from the audio output server
func (c *AudioOutputClient) Disconnect() {
c.mtx.Lock()
defer c.mtx.Unlock()
if !c.running {
return
}
c.running = false
if c.conn != nil {
c.conn.Close()
c.conn = nil
}
c.logger.Info().Msg("Disconnected from audio output server")
}
// IsConnected returns whether the client is connected
func (c *AudioOutputClient) IsConnected() bool {
c.mtx.Lock()
defer c.mtx.Unlock()
return c.running && c.conn != nil
}
func (c *AudioOutputClient) ReceiveFrame() ([]byte, error) { func (c *AudioOutputClient) ReceiveFrame() ([]byte, error) {
c.mtx.Lock() c.mtx.Lock()
@ -421,7 +95,7 @@ func (c *AudioOutputClient) ReceiveFrame() ([]byte, error) {
} }
size := binary.LittleEndian.Uint32(optMsg.header[5:9]) size := binary.LittleEndian.Uint32(optMsg.header[5:9])
maxFrameSize := Config.OutputMaxFrameSize maxFrameSize := GetConfig().OutputMaxFrameSize
if int(size) > maxFrameSize { if int(size) > maxFrameSize {
return nil, fmt.Errorf("received frame size validation failed: got %d bytes, maximum allowed %d bytes", size, maxFrameSize) return nil, fmt.Errorf("received frame size validation failed: got %d bytes, maximum allowed %d bytes", size, maxFrameSize)
} }
@ -442,53 +116,6 @@ func (c *AudioOutputClient) ReceiveFrame() ([]byte, error) {
return frame, nil return frame, nil
} }
// SendOpusConfig sends Opus configuration to the audio output server
func (c *AudioOutputClient) SendOpusConfig(config OutputIPCOpusConfig) error {
c.mtx.Lock()
defer c.mtx.Unlock()
if !c.running || c.conn == nil {
return fmt.Errorf("not connected to audio output server")
}
// Validate configuration parameters
if config.SampleRate <= 0 || config.Channels <= 0 || config.FrameSize <= 0 || config.Bitrate <= 0 {
return fmt.Errorf("invalid Opus configuration: SampleRate=%d, Channels=%d, FrameSize=%d, Bitrate=%d",
config.SampleRate, config.Channels, config.FrameSize, config.Bitrate)
}
// Serialize Opus configuration using common function
data := EncodeOpusConfig(config.SampleRate, config.Channels, config.FrameSize, config.Bitrate, config.Complexity, config.VBR, config.SignalType, config.Bandwidth, config.DTX)
msg := &OutputIPCMessage{
Magic: c.magicNumber,
Type: OutputMessageTypeOpusConfig,
Length: uint32(len(data)),
Timestamp: time.Now().UnixNano(),
Data: data,
}
return c.writeMessage(msg)
}
// writeMessage writes a message to the connection
func (c *AudioOutputClient) writeMessage(msg *OutputIPCMessage) error {
header := EncodeMessageHeader(msg.Magic, uint8(msg.Type), msg.Length, msg.Timestamp)
if _, err := c.conn.Write(header); err != nil {
return fmt.Errorf("failed to write header: %w", err)
}
if msg.Length > 0 && msg.Data != nil {
if _, err := c.conn.Write(msg.Data); err != nil {
return fmt.Errorf("failed to write data: %w", err)
}
}
atomic.AddInt64(&c.totalFrames, 1)
return nil
}
// GetClientStats returns client performance statistics // GetClientStats returns client performance statistics
func (c *AudioOutputClient) GetClientStats() (total, dropped int64) { func (c *AudioOutputClient) GetClientStats() (total, dropped int64) {
stats := GetFrameStats(&c.totalFrames, &c.droppedFrames) stats := GetFrameStats(&c.totalFrames, &c.droppedFrames)
@ -496,4 +123,5 @@ func (c *AudioOutputClient) GetClientStats() (total, dropped int64) {
} }
// Helper functions // Helper functions
// getOutputSocketPath is defined in ipc_unified.go
// getOutputSocketPath is now defined in unified_ipc.go

View File

@ -4,11 +4,9 @@ import (
"encoding/binary" "encoding/binary"
"fmt" "fmt"
"io" "io"
"math"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -19,21 +17,13 @@ import (
// Unified IPC constants // Unified IPC constants
var ( var (
outputMagicNumber uint32 = Config.OutputMagicNumber // "JKOU" (JetKVM Output) outputMagicNumber uint32 = GetConfig().OutputMagicNumber // "JKOU" (JetKVM Output)
inputMagicNumber uint32 = Config.InputMagicNumber // "JKMI" (JetKVM Microphone Input) inputMagicNumber uint32 = GetConfig().InputMagicNumber // "JKMI" (JetKVM Microphone Input)
outputSocketName = "audio_output.sock" outputSocketName = "audio_output.sock"
inputSocketName = "audio_input.sock" inputSocketName = "audio_input.sock"
headerSize = 17 // Fixed header size: 4+1+4+8 bytes headerSize = 17 // Fixed header size: 4+1+4+8 bytes
) )
// Header buffer pool to reduce allocation overhead
var headerBufferPool = sync.Pool{
New: func() interface{} {
buf := make([]byte, headerSize)
return &buf
},
}
// UnifiedMessageType represents the type of IPC message for both input and output // UnifiedMessageType represents the type of IPC message for both input and output
type UnifiedMessageType uint8 type UnifiedMessageType uint8
@ -99,6 +89,7 @@ type UnifiedIPCOpusConfig struct {
// UnifiedAudioServer provides common functionality for both input and output servers // UnifiedAudioServer provides common functionality for both input and output servers
type UnifiedAudioServer struct { type UnifiedAudioServer struct {
// Atomic counters for performance monitoring // Atomic counters for performance monitoring
bufferSize int64 // Current buffer size (atomic)
droppedFrames int64 // Dropped frames counter (atomic) droppedFrames int64 // Dropped frames counter (atomic)
totalFrames int64 // Total frames counter (atomic) totalFrames int64 // Total frames counter (atomic)
@ -117,6 +108,10 @@ type UnifiedAudioServer struct {
socketPath string socketPath string
magicNumber uint32 magicNumber uint32
socketBufferConfig SocketBufferConfig socketBufferConfig SocketBufferConfig
// Performance monitoring
latencyMonitor *LatencyMonitor
adaptiveOptimizer *AdaptiveOptimizer
} }
// NewUnifiedAudioServer creates a new unified audio server // NewUnifiedAudioServer creates a new unified audio server
@ -141,9 +136,11 @@ func NewUnifiedAudioServer(isInput bool) (*UnifiedAudioServer, error) {
logger: logger, logger: logger,
socketPath: socketPath, socketPath: socketPath,
magicNumber: magicNumber, magicNumber: magicNumber,
messageChan: make(chan *UnifiedIPCMessage, Config.ChannelBufferSize), messageChan: make(chan *UnifiedIPCMessage, GetConfig().ChannelBufferSize),
processChan: make(chan *UnifiedIPCMessage, Config.ChannelBufferSize), processChan: make(chan *UnifiedIPCMessage, GetConfig().ChannelBufferSize),
socketBufferConfig: DefaultSocketBufferConfig(), socketBufferConfig: DefaultSocketBufferConfig(),
latencyMonitor: nil,
adaptiveOptimizer: nil,
} }
return server, nil return server, nil
@ -158,38 +155,15 @@ func (s *UnifiedAudioServer) Start() error {
return fmt.Errorf("server already running") return fmt.Errorf("server already running")
} }
// Remove existing socket file with retry logic // Remove existing socket file
for i := 0; i < 3; i++ { if err := os.Remove(s.socketPath); err != nil && !os.IsNotExist(err) {
if err := os.Remove(s.socketPath); err != nil && !os.IsNotExist(err) { return fmt.Errorf("failed to remove existing socket: %w", err)
s.logger.Warn().Err(err).Int("attempt", i+1).Msg("failed to remove existing socket file, retrying")
time.Sleep(100 * time.Millisecond)
continue
}
break
}
// Create listener with retry on address already in use
var listener net.Listener
var err error
for i := 0; i < 3; i++ {
listener, err = net.Listen("unix", s.socketPath)
if err == nil {
break
}
// If address is still in use, try to remove socket file again
if strings.Contains(err.Error(), "address already in use") {
s.logger.Warn().Err(err).Int("attempt", i+1).Msg("socket address in use, attempting cleanup and retry")
os.Remove(s.socketPath)
time.Sleep(200 * time.Millisecond)
continue
}
return fmt.Errorf("failed to create unix socket: %w", err)
} }
// Create listener
listener, err := net.Listen("unix", s.socketPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create unix socket after retries: %w", err) return fmt.Errorf("failed to create listener: %w", err)
} }
s.listener = listener s.listener = listener
@ -309,11 +283,8 @@ func (s *UnifiedAudioServer) startProcessorGoroutine() {
// readMessage reads a message from the connection // readMessage reads a message from the connection
func (s *UnifiedAudioServer) readMessage(conn net.Conn) (*UnifiedIPCMessage, error) { func (s *UnifiedAudioServer) readMessage(conn net.Conn) (*UnifiedIPCMessage, error) {
// Get header buffer from pool // Read header
headerPtr := headerBufferPool.Get().(*[]byte) header := make([]byte, headerSize)
header := *headerPtr
defer headerBufferPool.Put(headerPtr)
if _, err := io.ReadFull(conn, header); err != nil { if _, err := io.ReadFull(conn, header); err != nil {
return nil, fmt.Errorf("failed to read header: %w", err) return nil, fmt.Errorf("failed to read header: %w", err)
} }
@ -329,7 +300,7 @@ func (s *UnifiedAudioServer) readMessage(conn net.Conn) (*UnifiedIPCMessage, err
timestamp := int64(binary.LittleEndian.Uint64(header[9:17])) timestamp := int64(binary.LittleEndian.Uint64(header[9:17]))
// Validate length // Validate length
if length > uint32(Config.MaxFrameSize) { if length > uint32(GetConfig().MaxFrameSize) {
return nil, fmt.Errorf("message too large: %d bytes", length) return nil, fmt.Errorf("message too large: %d bytes", length)
} }
@ -357,10 +328,7 @@ func (s *UnifiedAudioServer) SendFrame(frame []byte) error {
defer s.mtx.Unlock() defer s.mtx.Unlock()
if !s.running || s.conn == nil { if !s.running || s.conn == nil {
// Silently drop frames when no client is connected return fmt.Errorf("no client connected")
// This prevents "no client connected" warnings during startup and quality changes
atomic.AddInt64(&s.droppedFrames, 1)
return nil // Return nil to avoid flooding logs with connection warnings
} }
start := time.Now() start := time.Now()
@ -382,6 +350,10 @@ func (s *UnifiedAudioServer) SendFrame(frame []byte) error {
} }
// Record latency for monitoring // Record latency for monitoring
if s.latencyMonitor != nil {
writeLatency := time.Since(start)
s.latencyMonitor.RecordLatency(writeLatency, "ipc_write")
}
atomic.AddInt64(&s.totalFrames, 1) atomic.AddInt64(&s.totalFrames, 1)
return nil return nil
@ -389,20 +361,21 @@ func (s *UnifiedAudioServer) SendFrame(frame []byte) error {
// writeMessage writes a message to the connection // writeMessage writes a message to the connection
func (s *UnifiedAudioServer) writeMessage(conn net.Conn, msg *UnifiedIPCMessage) error { func (s *UnifiedAudioServer) writeMessage(conn net.Conn, msg *UnifiedIPCMessage) error {
header := EncodeMessageHeader(msg.Magic, uint8(msg.Type), msg.Length, msg.Timestamp) // Write header
header := make([]byte, headerSize)
binary.LittleEndian.PutUint32(header[0:4], msg.Magic)
header[4] = uint8(msg.Type)
binary.LittleEndian.PutUint32(header[5:9], msg.Length)
binary.LittleEndian.PutUint64(header[9:17], uint64(msg.Timestamp))
// Optimize: Use single write for header+data to reduce system calls if _, err := conn.Write(header); err != nil {
return fmt.Errorf("failed to write header: %w", err)
}
// Write data if present
if msg.Length > 0 && msg.Data != nil { if msg.Length > 0 && msg.Data != nil {
// Pre-allocate combined buffer to avoid copying if _, err := conn.Write(msg.Data); err != nil {
combined := make([]byte, len(header)+len(msg.Data)) return fmt.Errorf("failed to write data: %w", err)
copy(combined, header)
copy(combined[len(header):], msg.Data)
if _, err := conn.Write(combined); err != nil {
return fmt.Errorf("failed to write message: %w", err)
}
} else {
if _, err := conn.Write(header); err != nil {
return fmt.Errorf("failed to write header: %w", err)
} }
} }
@ -411,7 +384,7 @@ func (s *UnifiedAudioServer) writeMessage(conn net.Conn, msg *UnifiedIPCMessage)
// UnifiedAudioClient provides common functionality for both input and output clients // UnifiedAudioClient provides common functionality for both input and output clients
type UnifiedAudioClient struct { type UnifiedAudioClient struct {
// Atomic counters for frame statistics // Atomic fields first for ARM32 alignment
droppedFrames int64 // Atomic counter for dropped frames droppedFrames int64 // Atomic counter for dropped frames
totalFrames int64 // Atomic counter for total frames totalFrames int64 // Atomic counter for total frames
@ -422,13 +395,6 @@ type UnifiedAudioClient struct {
socketPath string socketPath string
magicNumber uint32 magicNumber uint32
bufferPool *AudioBufferPool // Buffer pool for memory optimization bufferPool *AudioBufferPool // Buffer pool for memory optimization
// Connection health monitoring
lastHealthCheck time.Time
connectionErrors int64 // Atomic counter for connection errors
autoReconnect bool // Enable automatic reconnection
healthCheckTicker *time.Ticker
stopHealthCheck chan struct{}
} }
// NewUnifiedAudioClient creates a new unified audio client // NewUnifiedAudioClient creates a new unified audio client
@ -450,12 +416,10 @@ func NewUnifiedAudioClient(isInput bool) *UnifiedAudioClient {
logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger() logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger()
return &UnifiedAudioClient{ return &UnifiedAudioClient{
logger: logger, logger: logger,
socketPath: socketPath, socketPath: socketPath,
magicNumber: magicNumber, magicNumber: magicNumber,
bufferPool: NewAudioBufferPool(Config.MaxFrameSize), bufferPool: NewAudioBufferPool(GetConfig().MaxFrameSize),
autoReconnect: true, // Enable automatic reconnection by default
stopHealthCheck: make(chan struct{}),
} }
} }
@ -475,46 +439,32 @@ func (c *UnifiedAudioClient) Connect() error {
} }
// Try connecting multiple times as the server might not be ready // Try connecting multiple times as the server might not be ready
// Use configurable retry parameters for better control // Reduced retry count and delay for faster startup
maxAttempts := Config.MaxConnectionAttempts for i := 0; i < 10; i++ {
initialDelay := Config.ConnectionRetryDelay conn, err := net.Dial("unix", c.socketPath)
maxDelay := Config.MaxConnectionRetryDelay
backoffFactor := Config.ConnectionBackoffFactor
for i := 0; i < maxAttempts; i++ {
// Set connection timeout for each attempt
conn, err := net.DialTimeout("unix", c.socketPath, Config.ConnectionTimeoutDelay)
if err == nil { if err == nil {
c.conn = conn c.conn = conn
c.running = true c.running = true
// Reset frame counters on successful connection // Reset frame counters on successful connection
atomic.StoreInt64(&c.totalFrames, 0) atomic.StoreInt64(&c.totalFrames, 0)
atomic.StoreInt64(&c.droppedFrames, 0) atomic.StoreInt64(&c.droppedFrames, 0)
atomic.StoreInt64(&c.connectionErrors, 0) c.logger.Info().Str("socket_path", c.socketPath).Msg("Connected to server")
c.lastHealthCheck = time.Now()
// Start health check monitoring if auto-reconnect is enabled
if c.autoReconnect {
c.startHealthCheck()
}
c.logger.Info().Str("socket_path", c.socketPath).Int("attempt", i+1).Msg("Connected to server")
return nil return nil
} }
// Exponential backoff starting from config
// Log connection attempt failure backoffStart := GetConfig().BackoffStart
c.logger.Debug().Err(err).Str("socket_path", c.socketPath).Int("attempt", i+1).Int("max_attempts", maxAttempts).Msg("Connection attempt failed") delay := time.Duration(backoffStart.Nanoseconds()*(1<<uint(i/3))) * time.Nanosecond
maxDelay := GetConfig().MaxRetryDelay
// Don't sleep after the last attempt if delay > maxDelay {
if i < maxAttempts-1 { delay = maxDelay
// Calculate adaptive delay based on connection failure patterns
delay := c.calculateAdaptiveDelay(i, initialDelay, maxDelay, backoffFactor)
time.Sleep(delay)
} }
time.Sleep(delay)
} }
// Ensure clean state on connection failure // Ensure clean state on connection failure
c.conn = nil c.conn = nil
c.running = false c.running = false
return fmt.Errorf("failed to connect to audio server after %d attempts", Config.MaxConnectionAttempts) return fmt.Errorf("failed to connect to audio server after 10 attempts")
} }
// Disconnect disconnects the client from the server // Disconnect disconnects the client from the server
@ -528,9 +478,6 @@ func (c *UnifiedAudioClient) Disconnect() {
c.running = false c.running = false
// Stop health check monitoring
c.stopHealthCheckMonitoring()
if c.conn != nil { if c.conn != nil {
c.conn.Close() c.conn.Close()
c.conn = nil c.conn = nil
@ -550,129 +497,14 @@ func (c *UnifiedAudioClient) IsConnected() bool {
func (c *UnifiedAudioClient) GetFrameStats() (total, dropped int64) { func (c *UnifiedAudioClient) GetFrameStats() (total, dropped int64) {
total = atomic.LoadInt64(&c.totalFrames) total = atomic.LoadInt64(&c.totalFrames)
dropped = atomic.LoadInt64(&c.droppedFrames) dropped = atomic.LoadInt64(&c.droppedFrames)
return return total, dropped
}
// startHealthCheck starts the connection health monitoring
func (c *UnifiedAudioClient) startHealthCheck() {
if c.healthCheckTicker != nil {
c.healthCheckTicker.Stop()
}
c.healthCheckTicker = time.NewTicker(Config.HealthCheckInterval)
go func() {
for {
select {
case <-c.healthCheckTicker.C:
c.performHealthCheck()
case <-c.stopHealthCheck:
return
}
}
}()
}
// stopHealthCheckMonitoring stops the health check monitoring
func (c *UnifiedAudioClient) stopHealthCheckMonitoring() {
if c.healthCheckTicker != nil {
c.healthCheckTicker.Stop()
c.healthCheckTicker = nil
}
select {
case c.stopHealthCheck <- struct{}{}:
default:
}
}
// performHealthCheck checks the connection health and attempts reconnection if needed
func (c *UnifiedAudioClient) performHealthCheck() {
c.mtx.Lock()
defer c.mtx.Unlock()
if !c.running || c.conn == nil {
return
}
// Simple health check: try to get connection info
if tcpConn, ok := c.conn.(*net.UnixConn); ok {
if _, err := tcpConn.File(); err != nil {
// Connection is broken
atomic.AddInt64(&c.connectionErrors, 1)
c.logger.Warn().Err(err).Msg("Connection health check failed, attempting reconnection")
// Close the broken connection
c.conn.Close()
c.conn = nil
c.running = false
// Attempt reconnection
go func() {
time.Sleep(Config.ReconnectionInterval)
if err := c.Connect(); err != nil {
c.logger.Error().Err(err).Msg("Failed to reconnect during health check")
}
}()
}
}
c.lastHealthCheck = time.Now()
}
// SetAutoReconnect enables or disables automatic reconnection
func (c *UnifiedAudioClient) SetAutoReconnect(enabled bool) {
c.mtx.Lock()
defer c.mtx.Unlock()
c.autoReconnect = enabled
if !enabled {
c.stopHealthCheckMonitoring()
} else if c.running {
c.startHealthCheck()
}
}
// GetConnectionErrors returns the number of connection errors
func (c *UnifiedAudioClient) GetConnectionErrors() int64 {
return atomic.LoadInt64(&c.connectionErrors)
}
// calculateAdaptiveDelay calculates retry delay based on system load and failure patterns
func (c *UnifiedAudioClient) calculateAdaptiveDelay(attempt int, initialDelay, maxDelay time.Duration, backoffFactor float64) time.Duration {
// Base exponential backoff
baseDelay := time.Duration(float64(initialDelay.Nanoseconds()) * math.Pow(backoffFactor, float64(attempt)))
// Get connection error history for adaptive adjustment
errorCount := atomic.LoadInt64(&c.connectionErrors)
// Adjust delay based on recent connection errors
// More errors = longer delays to avoid overwhelming the server
adaptiveFactor := 1.0
if errorCount > 5 {
adaptiveFactor = 1.5 // 50% longer delays after many errors
} else if errorCount > 10 {
adaptiveFactor = 2.0 // Double delays after excessive errors
}
// Apply adaptive factor
adaptiveDelay := time.Duration(float64(baseDelay.Nanoseconds()) * adaptiveFactor)
// Ensure we don't exceed maximum delay
if adaptiveDelay > maxDelay {
adaptiveDelay = maxDelay
}
// Add small random jitter to avoid thundering herd
jitter := time.Duration(float64(adaptiveDelay.Nanoseconds()) * 0.1 * (0.5 + float64(attempt%3)/6.0))
adaptiveDelay += jitter
return adaptiveDelay
} }
// Helper functions for socket paths // Helper functions for socket paths
func getInputSocketPath() string { func getInputSocketPath() string {
return filepath.Join("/var/run", inputSocketName) return filepath.Join(os.TempDir(), inputSocketName)
} }
func getOutputSocketPath() string { func getOutputSocketPath() string {
return filepath.Join("/var/run", outputSocketName) return filepath.Join(os.TempDir(), outputSocketName)
} }

View File

@ -28,6 +28,7 @@ type BaseSupervisor struct {
processPID int processPID int
// Process monitoring // Process monitoring
processMonitor *ProcessMonitor
// Exit tracking // Exit tracking
lastExitCode int lastExitCode int
@ -44,10 +45,10 @@ type BaseSupervisor struct {
func NewBaseSupervisor(componentName string) *BaseSupervisor { func NewBaseSupervisor(componentName string) *BaseSupervisor {
logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger() logger := logging.GetDefaultLogger().With().Str("component", componentName).Logger()
return &BaseSupervisor{ return &BaseSupervisor{
logger: &logger, logger: &logger,
processMonitor: GetProcessMonitor(),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
processDone: make(chan struct{}), processDone: make(chan struct{}),
} }
} }
@ -210,6 +211,7 @@ func (bs *BaseSupervisor) waitForProcessExit(processType string) {
bs.mutex.Unlock() bs.mutex.Unlock()
// Remove process from monitoring // Remove process from monitoring
bs.processMonitor.RemoveProcess(pid)
if exitCode != 0 { if exitCode != 0 {
bs.logger.Error().Int("pid", pid).Int("exit_code", exitCode).Msgf("%s process exited with error", processType) bs.logger.Error().Int("pid", pid).Int("exit_code", exitCode).Msgf("%s process exited with error", processType)

View File

@ -32,7 +32,7 @@ type AudioInputIPCManager struct {
// NewAudioInputIPCManager creates a new IPC-based audio input manager // NewAudioInputIPCManager creates a new IPC-based audio input manager
func NewAudioInputIPCManager() *AudioInputIPCManager { func NewAudioInputIPCManager() *AudioInputIPCManager {
return &AudioInputIPCManager{ return &AudioInputIPCManager{
supervisor: GetAudioInputSupervisor(), // Use global shared supervisor supervisor: NewAudioInputSupervisor(),
logger: logging.GetDefaultLogger().With().Str("component", AudioInputIPCComponent).Logger(), logger: logging.GetDefaultLogger().With().Str("component", AudioInputIPCComponent).Logger(),
} }
} }
@ -63,9 +63,9 @@ func (aim *AudioInputIPCManager) Start() error {
} }
config := InputIPCConfig{ config := InputIPCConfig{
SampleRate: Config.InputIPCSampleRate, SampleRate: GetConfig().InputIPCSampleRate,
Channels: Config.InputIPCChannels, Channels: GetConfig().InputIPCChannels,
FrameSize: Config.InputIPCFrameSize, FrameSize: GetConfig().InputIPCFrameSize,
} }
// Validate configuration before using it // Validate configuration before using it
@ -80,7 +80,7 @@ func (aim *AudioInputIPCManager) Start() error {
} }
// Wait for subprocess readiness // Wait for subprocess readiness
time.Sleep(Config.LongSleepDuration) time.Sleep(GetConfig().LongSleepDuration)
err = aim.supervisor.SendConfig(config) err = aim.supervisor.SendConfig(config)
if err != nil { if err != nil {

View File

@ -57,9 +57,9 @@ func (aom *AudioOutputIPCManager) Start() error {
// Send initial configuration // Send initial configuration
config := OutputIPCConfig{ config := OutputIPCConfig{
SampleRate: Config.SampleRate, SampleRate: GetConfig().SampleRate,
Channels: Config.Channels, Channels: GetConfig().Channels,
FrameSize: int(Config.AudioQualityMediumFrameSize.Milliseconds()), FrameSize: int(GetConfig().AudioQualityMediumFrameSize.Milliseconds()),
} }
if err := aom.SendConfig(config); err != nil { if err := aom.SendConfig(config); err != nil {

View File

@ -105,7 +105,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
} }
if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) { if atomic.CompareAndSwapInt32(&micContentionInitialized, 0, 1) {
manager := NewMicrophoneContentionManager(Config.MicContentionTimeout) manager := NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager)) atomic.StorePointer(&globalMicContentionManager, unsafe.Pointer(manager))
return manager return manager
} }
@ -115,7 +115,7 @@ func GetMicrophoneContentionManager() *MicrophoneContentionManager {
return (*MicrophoneContentionManager)(ptr) return (*MicrophoneContentionManager)(ptr)
} }
return NewMicrophoneContentionManager(Config.MicContentionTimeout) return NewMicrophoneContentionManager(GetConfig().MicContentionTimeout)
} }
func TryMicrophoneOperation() OperationResult { func TryMicrophoneOperation() OperationResult {

View File

@ -0,0 +1,198 @@
package audio
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog"
)
// AdaptiveOptimizer automatically adjusts audio parameters based on latency metrics
type AdaptiveOptimizer struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
optimizationCount int64 // Number of optimizations performed (atomic)
lastOptimization int64 // Timestamp of last optimization (atomic)
optimizationLevel int64 // Current optimization level (0-10) (atomic)
latencyMonitor *LatencyMonitor
bufferManager *AdaptiveBufferManager
logger zerolog.Logger
// Control channels
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
// Configuration
config OptimizerConfig
}
// OptimizerConfig holds configuration for the adaptive optimizer
type OptimizerConfig struct {
MaxOptimizationLevel int // Maximum optimization level (0-10)
CooldownPeriod time.Duration // Minimum time between optimizations
Aggressiveness float64 // How aggressively to optimize (0.0-1.0)
RollbackThreshold time.Duration // Latency threshold to rollback optimizations
StabilityPeriod time.Duration // Time to wait for stability after optimization
}
// DefaultOptimizerConfig returns a sensible default configuration
func DefaultOptimizerConfig() OptimizerConfig {
return OptimizerConfig{
MaxOptimizationLevel: 8,
CooldownPeriod: GetConfig().CooldownPeriod,
Aggressiveness: GetConfig().OptimizerAggressiveness,
RollbackThreshold: GetConfig().RollbackThreshold,
StabilityPeriod: GetConfig().AdaptiveOptimizerStability,
}
}
// NewAdaptiveOptimizer creates a new adaptive optimizer
func NewAdaptiveOptimizer(latencyMonitor *LatencyMonitor, bufferManager *AdaptiveBufferManager, config OptimizerConfig, logger zerolog.Logger) *AdaptiveOptimizer {
ctx, cancel := context.WithCancel(context.Background())
optimizer := &AdaptiveOptimizer{
latencyMonitor: latencyMonitor,
bufferManager: bufferManager,
config: config,
logger: logger.With().Str("component", "adaptive-optimizer").Logger(),
ctx: ctx,
cancel: cancel,
}
// Register as latency monitor callback
latencyMonitor.AddOptimizationCallback(optimizer.handleLatencyOptimization)
return optimizer
}
// Start begins the adaptive optimization process
func (ao *AdaptiveOptimizer) Start() {
ao.wg.Add(1)
go ao.optimizationLoop()
ao.logger.Debug().Msg("adaptive optimizer started")
}
// Stop stops the adaptive optimizer
func (ao *AdaptiveOptimizer) Stop() {
ao.cancel()
ao.wg.Wait()
ao.logger.Debug().Msg("adaptive optimizer stopped")
}
// initializeStrategies sets up the available optimization strategies
// handleLatencyOptimization is called when latency optimization is needed
func (ao *AdaptiveOptimizer) handleLatencyOptimization(metrics LatencyMetrics) error {
currentLevel := atomic.LoadInt64(&ao.optimizationLevel)
lastOpt := atomic.LoadInt64(&ao.lastOptimization)
// Check cooldown period
if time.Since(time.Unix(0, lastOpt)) < ao.config.CooldownPeriod {
return nil
}
// Determine if we need to increase or decrease optimization level
targetLevel := ao.calculateTargetOptimizationLevel(metrics)
if targetLevel > currentLevel {
return ao.increaseOptimization(int(targetLevel))
} else if targetLevel < currentLevel {
return ao.decreaseOptimization(int(targetLevel))
}
return nil
}
// calculateTargetOptimizationLevel determines the appropriate optimization level
func (ao *AdaptiveOptimizer) calculateTargetOptimizationLevel(metrics LatencyMetrics) int64 {
// Base calculation on current latency vs target
latencyRatio := float64(metrics.Current) / float64(GetConfig().AdaptiveOptimizerLatencyTarget) // 50ms target
// Adjust based on trend
switch metrics.Trend {
case LatencyTrendIncreasing:
latencyRatio *= 1.2 // Be more aggressive
case LatencyTrendDecreasing:
latencyRatio *= 0.8 // Be less aggressive
case LatencyTrendVolatile:
latencyRatio *= 1.1 // Slightly more aggressive
}
// Apply aggressiveness factor
latencyRatio *= ao.config.Aggressiveness
// Convert to optimization level
targetLevel := int64(latencyRatio * GetConfig().LatencyScalingFactor) // Scale to 0-10 range
if targetLevel > int64(ao.config.MaxOptimizationLevel) {
targetLevel = int64(ao.config.MaxOptimizationLevel)
}
if targetLevel < 0 {
targetLevel = 0
}
return targetLevel
}
// increaseOptimization applies optimization strategies up to the target level
func (ao *AdaptiveOptimizer) increaseOptimization(targetLevel int) error {
atomic.StoreInt64(&ao.optimizationLevel, int64(targetLevel))
atomic.StoreInt64(&ao.lastOptimization, time.Now().UnixNano())
atomic.AddInt64(&ao.optimizationCount, 1)
return nil
}
// decreaseOptimization rolls back optimization strategies to the target level
func (ao *AdaptiveOptimizer) decreaseOptimization(targetLevel int) error {
atomic.StoreInt64(&ao.optimizationLevel, int64(targetLevel))
atomic.StoreInt64(&ao.lastOptimization, time.Now().UnixNano())
return nil
}
// optimizationLoop runs the main optimization monitoring loop
func (ao *AdaptiveOptimizer) optimizationLoop() {
defer ao.wg.Done()
ticker := time.NewTicker(ao.config.StabilityPeriod)
defer ticker.Stop()
for {
select {
case <-ao.ctx.Done():
return
case <-ticker.C:
ao.checkStability()
}
}
}
// checkStability monitors system stability and rolls back if needed
func (ao *AdaptiveOptimizer) checkStability() {
metrics := ao.latencyMonitor.GetMetrics()
// Check if we need to rollback due to excessive latency
if metrics.Current > ao.config.RollbackThreshold {
currentLevel := int(atomic.LoadInt64(&ao.optimizationLevel))
if currentLevel > 0 {
ao.logger.Warn().Dur("current_latency", metrics.Current).Dur("threshold", ao.config.RollbackThreshold).Msg("rolling back optimizations due to excessive latency")
if err := ao.decreaseOptimization(currentLevel - 1); err != nil {
ao.logger.Error().Err(err).Msg("failed to decrease optimization level")
}
}
}
}
// GetOptimizationStats returns current optimization statistics
func (ao *AdaptiveOptimizer) GetOptimizationStats() map[string]interface{} {
return map[string]interface{}{
"optimization_level": atomic.LoadInt64(&ao.optimizationLevel),
"optimization_count": atomic.LoadInt64(&ao.optimizationCount),
"last_optimization": time.Unix(0, atomic.LoadInt64(&ao.lastOptimization)),
}
}
// Strategy implementation methods (stubs for now)

View File

@ -0,0 +1,144 @@
package audio
import (
"runtime"
"sync/atomic"
"time"
"github.com/jetkvm/kvm/internal/logging"
)
// GoroutineMonitor tracks goroutine count and provides cleanup mechanisms
type GoroutineMonitor struct {
baselineCount int
peakCount int
lastCount int
monitorInterval time.Duration
lastCheck time.Time
enabled int32
}
// Global goroutine monitor instance
var globalGoroutineMonitor *GoroutineMonitor
// NewGoroutineMonitor creates a new goroutine monitor
func NewGoroutineMonitor(monitorInterval time.Duration) *GoroutineMonitor {
if monitorInterval <= 0 {
monitorInterval = 30 * time.Second
}
// Get current goroutine count as baseline
baselineCount := runtime.NumGoroutine()
return &GoroutineMonitor{
baselineCount: baselineCount,
peakCount: baselineCount,
lastCount: baselineCount,
monitorInterval: monitorInterval,
lastCheck: time.Now(),
}
}
// Start begins goroutine monitoring
func (gm *GoroutineMonitor) Start() {
if !atomic.CompareAndSwapInt32(&gm.enabled, 0, 1) {
return // Already running
}
go gm.monitorLoop()
}
// Stop stops goroutine monitoring
func (gm *GoroutineMonitor) Stop() {
atomic.StoreInt32(&gm.enabled, 0)
}
// monitorLoop periodically checks goroutine count
func (gm *GoroutineMonitor) monitorLoop() {
logger := logging.GetDefaultLogger().With().Str("component", "goroutine-monitor").Logger()
logger.Info().Int("baseline", gm.baselineCount).Msg("goroutine monitor started")
for atomic.LoadInt32(&gm.enabled) == 1 {
time.Sleep(gm.monitorInterval)
gm.checkGoroutineCount()
}
logger.Info().Msg("goroutine monitor stopped")
}
// checkGoroutineCount checks current goroutine count and logs if it exceeds thresholds
func (gm *GoroutineMonitor) checkGoroutineCount() {
currentCount := runtime.NumGoroutine()
gm.lastCount = currentCount
// Update peak count if needed
if currentCount > gm.peakCount {
gm.peakCount = currentCount
}
// Calculate growth since baseline
growth := currentCount - gm.baselineCount
growthPercent := float64(growth) / float64(gm.baselineCount) * 100
// Log warning if growth exceeds thresholds
logger := logging.GetDefaultLogger().With().Str("component", "goroutine-monitor").Logger()
// Different log levels based on growth severity
if growthPercent > 30 {
// Severe growth - trigger cleanup
logger.Warn().Int("current", currentCount).Int("baseline", gm.baselineCount).
Int("growth", growth).Float64("growth_percent", growthPercent).
Msg("excessive goroutine growth detected - triggering cleanup")
// Force garbage collection to clean up unused resources
runtime.GC()
// Force cleanup of goroutine buffer cache
cleanupGoroutineCache()
} else if growthPercent > 20 {
// Moderate growth - just log warning
logger.Warn().Int("current", currentCount).Int("baseline", gm.baselineCount).
Int("growth", growth).Float64("growth_percent", growthPercent).
Msg("significant goroutine growth detected")
} else if growthPercent > 10 {
// Minor growth - log info
logger.Info().Int("current", currentCount).Int("baseline", gm.baselineCount).
Int("growth", growth).Float64("growth_percent", growthPercent).
Msg("goroutine growth detected")
}
// Update last check time
gm.lastCheck = time.Now()
}
// GetGoroutineStats returns current goroutine statistics
func (gm *GoroutineMonitor) GetGoroutineStats() map[string]interface{} {
return map[string]interface{}{
"current_count": gm.lastCount,
"baseline_count": gm.baselineCount,
"peak_count": gm.peakCount,
"growth": gm.lastCount - gm.baselineCount,
"growth_percent": float64(gm.lastCount-gm.baselineCount) / float64(gm.baselineCount) * 100,
"last_check": gm.lastCheck,
}
}
// GetGoroutineMonitor returns the global goroutine monitor instance
func GetGoroutineMonitor() *GoroutineMonitor {
if globalGoroutineMonitor == nil {
globalGoroutineMonitor = NewGoroutineMonitor(GetConfig().GoroutineMonitorInterval)
}
return globalGoroutineMonitor
}
// StartGoroutineMonitoring starts the global goroutine monitor
func StartGoroutineMonitoring() {
// Goroutine monitoring disabled
}
// StopGoroutineMonitoring stops the global goroutine monitor
func StopGoroutineMonitoring() {
if globalGoroutineMonitor != nil {
globalGoroutineMonitor.Stop()
}
}

View File

@ -0,0 +1,333 @@
package audio
import (
"context"
"sync"
"sync/atomic"
"time"
"github.com/rs/zerolog"
)
// LatencyMonitor tracks and optimizes audio latency in real-time
type LatencyMonitor struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
currentLatency int64 // Current latency in nanoseconds (atomic)
averageLatency int64 // Rolling average latency in nanoseconds (atomic)
minLatency int64 // Minimum observed latency in nanoseconds (atomic)
maxLatency int64 // Maximum observed latency in nanoseconds (atomic)
latencySamples int64 // Number of latency samples collected (atomic)
jitterAccumulator int64 // Accumulated jitter for variance calculation (atomic)
lastOptimization int64 // Timestamp of last optimization in nanoseconds (atomic)
config LatencyConfig
logger zerolog.Logger
// Control channels
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
// Optimization callbacks
optimizationCallbacks []OptimizationCallback
mutex sync.RWMutex
// Performance tracking
latencyHistory []LatencyMeasurement
historyMutex sync.RWMutex
}
// LatencyConfig holds configuration for latency monitoring
type LatencyConfig struct {
TargetLatency time.Duration // Target latency to maintain
MaxLatency time.Duration // Maximum acceptable latency
OptimizationInterval time.Duration // How often to run optimization
HistorySize int // Number of latency measurements to keep
JitterThreshold time.Duration // Jitter threshold for optimization
AdaptiveThreshold float64 // Threshold for adaptive adjustments (0.0-1.0)
}
// LatencyMeasurement represents a single latency measurement
type LatencyMeasurement struct {
Timestamp time.Time
Latency time.Duration
Jitter time.Duration
Source string // Source of the measurement (e.g., "input", "output", "processing")
}
// OptimizationCallback is called when latency optimization is triggered
type OptimizationCallback func(metrics LatencyMetrics) error
// LatencyMetrics provides comprehensive latency statistics
type LatencyMetrics struct {
Current time.Duration
Average time.Duration
Min time.Duration
Max time.Duration
Jitter time.Duration
SampleCount int64
Trend LatencyTrend
}
// LatencyTrend indicates the direction of latency changes
type LatencyTrend int
const (
LatencyTrendStable LatencyTrend = iota
LatencyTrendIncreasing
LatencyTrendDecreasing
LatencyTrendVolatile
)
// DefaultLatencyConfig returns a sensible default configuration
func DefaultLatencyConfig() LatencyConfig {
config := GetConfig()
return LatencyConfig{
TargetLatency: config.LatencyMonitorTarget,
MaxLatency: config.MaxLatencyThreshold,
OptimizationInterval: config.LatencyOptimizationInterval,
HistorySize: config.LatencyHistorySize,
JitterThreshold: config.JitterThreshold,
AdaptiveThreshold: config.LatencyAdaptiveThreshold,
}
}
// NewLatencyMonitor creates a new latency monitoring system
func NewLatencyMonitor(config LatencyConfig, logger zerolog.Logger) *LatencyMonitor {
// Validate latency configuration
if err := ValidateLatencyConfig(config); err != nil {
// Log validation error and use default configuration
logger.Error().Err(err).Msg("Invalid latency configuration provided, using defaults")
config = DefaultLatencyConfig()
}
ctx, cancel := context.WithCancel(context.Background())
return &LatencyMonitor{
config: config,
logger: logger.With().Str("component", "latency-monitor").Logger(),
ctx: ctx,
cancel: cancel,
latencyHistory: make([]LatencyMeasurement, 0, config.HistorySize),
minLatency: int64(time.Hour), // Initialize to high value
}
}
// Start begins latency monitoring and optimization
func (lm *LatencyMonitor) Start() {
lm.wg.Add(1)
go lm.monitoringLoop()
}
// Stop stops the latency monitor
func (lm *LatencyMonitor) Stop() {
lm.cancel()
lm.wg.Wait()
}
// RecordLatency records a new latency measurement
func (lm *LatencyMonitor) RecordLatency(latency time.Duration, source string) {
now := time.Now()
latencyNanos := latency.Nanoseconds()
// Update atomic counters
atomic.StoreInt64(&lm.currentLatency, latencyNanos)
atomic.AddInt64(&lm.latencySamples, 1)
// Update min/max
for {
oldMin := atomic.LoadInt64(&lm.minLatency)
if latencyNanos >= oldMin || atomic.CompareAndSwapInt64(&lm.minLatency, oldMin, latencyNanos) {
break
}
}
for {
oldMax := atomic.LoadInt64(&lm.maxLatency)
if latencyNanos <= oldMax || atomic.CompareAndSwapInt64(&lm.maxLatency, oldMax, latencyNanos) {
break
}
}
// Update rolling average using exponential moving average
oldAvg := atomic.LoadInt64(&lm.averageLatency)
newAvg := oldAvg + (latencyNanos-oldAvg)/10 // Alpha = 0.1
atomic.StoreInt64(&lm.averageLatency, newAvg)
// Calculate jitter (difference from average)
jitter := latencyNanos - newAvg
if jitter < 0 {
jitter = -jitter
}
atomic.AddInt64(&lm.jitterAccumulator, jitter)
// Store in history
lm.historyMutex.Lock()
measurement := LatencyMeasurement{
Timestamp: now,
Latency: latency,
Jitter: time.Duration(jitter),
Source: source,
}
if len(lm.latencyHistory) >= lm.config.HistorySize {
// Remove oldest measurement
copy(lm.latencyHistory, lm.latencyHistory[1:])
lm.latencyHistory[len(lm.latencyHistory)-1] = measurement
} else {
lm.latencyHistory = append(lm.latencyHistory, measurement)
}
lm.historyMutex.Unlock()
}
// GetMetrics returns current latency metrics
func (lm *LatencyMonitor) GetMetrics() LatencyMetrics {
current := atomic.LoadInt64(&lm.currentLatency)
average := atomic.LoadInt64(&lm.averageLatency)
min := atomic.LoadInt64(&lm.minLatency)
max := atomic.LoadInt64(&lm.maxLatency)
samples := atomic.LoadInt64(&lm.latencySamples)
jitterSum := atomic.LoadInt64(&lm.jitterAccumulator)
var jitter time.Duration
if samples > 0 {
jitter = time.Duration(jitterSum / samples)
}
return LatencyMetrics{
Current: time.Duration(current),
Average: time.Duration(average),
Min: time.Duration(min),
Max: time.Duration(max),
Jitter: jitter,
SampleCount: samples,
Trend: lm.calculateTrend(),
}
}
// AddOptimizationCallback adds a callback for latency optimization
func (lm *LatencyMonitor) AddOptimizationCallback(callback OptimizationCallback) {
lm.mutex.Lock()
lm.optimizationCallbacks = append(lm.optimizationCallbacks, callback)
lm.mutex.Unlock()
}
// monitoringLoop runs the main monitoring and optimization loop
func (lm *LatencyMonitor) monitoringLoop() {
defer lm.wg.Done()
ticker := time.NewTicker(lm.config.OptimizationInterval)
defer ticker.Stop()
for {
select {
case <-lm.ctx.Done():
return
case <-ticker.C:
lm.runOptimization()
}
}
}
// runOptimization checks if optimization is needed and triggers callbacks with threshold validation.
//
// Validation Rules:
// - Current latency must not exceed MaxLatency (default: 200ms)
// - Average latency checked against adaptive threshold: TargetLatency * (1 + AdaptiveThreshold)
// - Jitter must not exceed JitterThreshold (default: 20ms)
// - All latency values must be non-negative durations
//
// Optimization Triggers:
// - Current latency > MaxLatency: Immediate optimization needed
// - Average latency > adaptive threshold: Gradual optimization needed
// - Jitter > JitterThreshold: Stability optimization needed
//
// Threshold Calculations:
// - Adaptive threshold = TargetLatency * (1.0 + AdaptiveThreshold)
// - Default: 50ms * (1.0 + 0.8) = 90ms adaptive threshold
// - Provides buffer above target before triggering optimization
//
// The function ensures real-time audio performance by monitoring multiple
// latency metrics and triggering optimization callbacks when thresholds are exceeded.
func (lm *LatencyMonitor) runOptimization() {
metrics := lm.GetMetrics()
// Check if optimization is needed
needsOptimization := false
// Check if current latency exceeds threshold
if metrics.Current > lm.config.MaxLatency {
needsOptimization = true
lm.logger.Warn().Dur("current_latency", metrics.Current).Dur("max_latency", lm.config.MaxLatency).Msg("latency exceeds maximum threshold")
}
// Check if average latency is above adaptive threshold
adaptiveThreshold := time.Duration(float64(lm.config.TargetLatency.Nanoseconds()) * (1.0 + lm.config.AdaptiveThreshold))
if metrics.Average > adaptiveThreshold {
needsOptimization = true
}
// Check if jitter is too high
if metrics.Jitter > lm.config.JitterThreshold {
needsOptimization = true
}
if needsOptimization {
atomic.StoreInt64(&lm.lastOptimization, time.Now().UnixNano())
// Run optimization callbacks
lm.mutex.RLock()
callbacks := make([]OptimizationCallback, len(lm.optimizationCallbacks))
copy(callbacks, lm.optimizationCallbacks)
lm.mutex.RUnlock()
for _, callback := range callbacks {
if err := callback(metrics); err != nil {
lm.logger.Error().Err(err).Msg("optimization callback failed")
}
}
}
}
// calculateTrend analyzes recent latency measurements to determine trend
func (lm *LatencyMonitor) calculateTrend() LatencyTrend {
lm.historyMutex.RLock()
defer lm.historyMutex.RUnlock()
if len(lm.latencyHistory) < 10 {
return LatencyTrendStable
}
// Analyze last 10 measurements
recentMeasurements := lm.latencyHistory[len(lm.latencyHistory)-10:]
var increasing, decreasing int
for i := 1; i < len(recentMeasurements); i++ {
if recentMeasurements[i].Latency > recentMeasurements[i-1].Latency {
increasing++
} else if recentMeasurements[i].Latency < recentMeasurements[i-1].Latency {
decreasing++
}
}
// Determine trend based on direction changes
if increasing > 6 {
return LatencyTrendIncreasing
} else if decreasing > 6 {
return LatencyTrendDecreasing
} else if increasing+decreasing > 7 {
return LatencyTrendVolatile
}
return LatencyTrendStable
}
// GetLatencyHistory returns a copy of recent latency measurements
func (lm *LatencyMonitor) GetLatencyHistory() []LatencyMeasurement {
lm.historyMutex.RLock()
defer lm.historyMutex.RUnlock()
history := make([]LatencyMeasurement, len(lm.latencyHistory))
copy(history, lm.latencyHistory)
return history
}

View File

@ -0,0 +1,406 @@
package audio
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
"sync"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
// Variables for process monitoring (using configuration)
var (
// System constants
maxCPUPercent = GetConfig().MaxCPUPercent
minCPUPercent = GetConfig().MinCPUPercent
defaultClockTicks = GetConfig().DefaultClockTicks
defaultMemoryGB = GetConfig().DefaultMemoryGB
// Monitoring thresholds
maxWarmupSamples = GetConfig().MaxWarmupSamples
warmupCPUSamples = GetConfig().WarmupCPUSamples
// Channel buffer size
metricsChannelBuffer = GetConfig().MetricsChannelBuffer
// Clock tick detection ranges
minValidClockTicks = float64(GetConfig().MinValidClockTicks)
maxValidClockTicks = float64(GetConfig().MaxValidClockTicks)
)
// Variables for process monitoring
var (
pageSize = GetConfig().PageSize
)
// ProcessMetrics represents CPU and memory usage metrics for a process
type ProcessMetrics struct {
PID int `json:"pid"`
CPUPercent float64 `json:"cpu_percent"`
MemoryRSS int64 `json:"memory_rss_bytes"`
MemoryVMS int64 `json:"memory_vms_bytes"`
MemoryPercent float64 `json:"memory_percent"`
Timestamp time.Time `json:"timestamp"`
ProcessName string `json:"process_name"`
}
type ProcessMonitor struct {
logger zerolog.Logger
mutex sync.RWMutex
monitoredPIDs map[int]*processState
running bool
stopChan chan struct{}
metricsChan chan ProcessMetrics
updateInterval time.Duration
totalMemory int64
memoryOnce sync.Once
clockTicks float64
clockTicksOnce sync.Once
}
// processState tracks the state needed for CPU calculation
type processState struct {
name string
lastCPUTime int64
lastSysTime int64
lastUserTime int64
lastSample time.Time
warmupSamples int
}
// NewProcessMonitor creates a new process monitor
func NewProcessMonitor() *ProcessMonitor {
return &ProcessMonitor{
logger: logging.GetDefaultLogger().With().Str("component", "process-monitor").Logger(),
monitoredPIDs: make(map[int]*processState),
stopChan: make(chan struct{}),
metricsChan: make(chan ProcessMetrics, metricsChannelBuffer),
updateInterval: GetMetricsUpdateInterval(),
}
}
// Start begins monitoring processes
func (pm *ProcessMonitor) Start() {
pm.mutex.Lock()
defer pm.mutex.Unlock()
if pm.running {
return
}
pm.running = true
go pm.monitorLoop()
pm.logger.Debug().Msg("process monitor started")
}
// Stop stops monitoring processes
func (pm *ProcessMonitor) Stop() {
pm.mutex.Lock()
defer pm.mutex.Unlock()
if !pm.running {
return
}
pm.running = false
close(pm.stopChan)
pm.logger.Debug().Msg("process monitor stopped")
}
// AddProcess adds a process to monitor
func (pm *ProcessMonitor) AddProcess(pid int, name string) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
pm.monitoredPIDs[pid] = &processState{
name: name,
lastSample: time.Now(),
}
pm.logger.Info().Int("pid", pid).Str("name", name).Msg("Added process to monitor")
}
// RemoveProcess removes a process from monitoring
func (pm *ProcessMonitor) RemoveProcess(pid int) {
pm.mutex.Lock()
defer pm.mutex.Unlock()
delete(pm.monitoredPIDs, pid)
pm.logger.Info().Int("pid", pid).Msg("Removed process from monitor")
}
// GetMetricsChan returns the channel for receiving metrics
func (pm *ProcessMonitor) GetMetricsChan() <-chan ProcessMetrics {
return pm.metricsChan
}
// GetCurrentMetrics returns current metrics for all monitored processes
func (pm *ProcessMonitor) GetCurrentMetrics() []ProcessMetrics {
pm.mutex.RLock()
defer pm.mutex.RUnlock()
var metrics []ProcessMetrics
for pid, state := range pm.monitoredPIDs {
if metric, err := pm.collectMetrics(pid, state); err == nil {
metrics = append(metrics, metric)
}
}
return metrics
}
// monitorLoop is the main monitoring loop
func (pm *ProcessMonitor) monitorLoop() {
ticker := time.NewTicker(pm.updateInterval)
defer ticker.Stop()
for {
select {
case <-pm.stopChan:
return
case <-ticker.C:
pm.collectAllMetrics()
}
}
}
func (pm *ProcessMonitor) collectAllMetrics() {
pm.mutex.RLock()
pidsToCheck := make([]int, 0, len(pm.monitoredPIDs))
states := make([]*processState, 0, len(pm.monitoredPIDs))
for pid, state := range pm.monitoredPIDs {
pidsToCheck = append(pidsToCheck, pid)
states = append(states, state)
}
pm.mutex.RUnlock()
deadPIDs := make([]int, 0)
for i, pid := range pidsToCheck {
if metric, err := pm.collectMetrics(pid, states[i]); err == nil {
select {
case pm.metricsChan <- metric:
default:
}
} else {
deadPIDs = append(deadPIDs, pid)
}
}
for _, pid := range deadPIDs {
pm.RemoveProcess(pid)
}
}
func (pm *ProcessMonitor) collectMetrics(pid int, state *processState) (ProcessMetrics, error) {
now := time.Now()
metric := ProcessMetrics{
PID: pid,
Timestamp: now,
ProcessName: state.name,
}
statPath := fmt.Sprintf("/proc/%d/stat", pid)
statData, err := os.ReadFile(statPath)
if err != nil {
return metric, fmt.Errorf("failed to read process statistics from /proc/%d/stat: %w", pid, err)
}
fields := strings.Fields(string(statData))
if len(fields) < 24 {
return metric, fmt.Errorf("invalid process stat format: expected at least 24 fields, got %d from /proc/%d/stat", len(fields), pid)
}
utime, _ := strconv.ParseInt(fields[13], 10, 64)
stime, _ := strconv.ParseInt(fields[14], 10, 64)
totalCPUTime := utime + stime
vsize, _ := strconv.ParseInt(fields[22], 10, 64)
rss, _ := strconv.ParseInt(fields[23], 10, 64)
metric.MemoryRSS = rss * int64(pageSize)
metric.MemoryVMS = vsize
// Calculate CPU percentage
metric.CPUPercent = pm.calculateCPUPercent(totalCPUTime, state, now)
// Increment warmup counter
if state.warmupSamples < maxWarmupSamples {
state.warmupSamples++
}
// Calculate memory percentage (RSS / total system memory)
if totalMem := pm.getTotalMemory(); totalMem > 0 {
metric.MemoryPercent = float64(metric.MemoryRSS) / float64(totalMem) * GetConfig().PercentageMultiplier
}
// Update state for next calculation
state.lastCPUTime = totalCPUTime
state.lastUserTime = utime
state.lastSysTime = stime
state.lastSample = now
return metric, nil
}
// calculateCPUPercent calculates CPU percentage for a process with validation and bounds checking.
//
// Validation Rules:
// - Returns 0.0 for first sample (no baseline for comparison)
// - Requires positive time delta between samples
// - Applies CPU percentage bounds: [MinCPUPercent, MaxCPUPercent]
// - Uses system clock ticks for accurate CPU time conversion
// - Validates clock ticks within range [MinValidClockTicks, MaxValidClockTicks]
//
// Bounds Applied:
// - CPU percentage clamped to [0.01%, 100.0%] (default values)
// - Clock ticks validated within [50, 1000] range (default values)
// - Time delta must be > 0 to prevent division by zero
//
// Warmup Behavior:
// - During warmup period (< WarmupCPUSamples), returns MinCPUPercent for idle processes
// - This indicates process is alive but not consuming significant CPU
//
// The function ensures accurate CPU percentage calculation while preventing
// invalid measurements that could affect system monitoring and adaptive algorithms.
func (pm *ProcessMonitor) calculateCPUPercent(totalCPUTime int64, state *processState, now time.Time) float64 {
if state.lastSample.IsZero() {
// First sample - initialize baseline
state.warmupSamples = 0
return 0.0
}
timeDelta := now.Sub(state.lastSample).Seconds()
cpuDelta := float64(totalCPUTime - state.lastCPUTime)
if timeDelta <= 0 {
return 0.0
}
if cpuDelta > 0 {
// Convert from clock ticks to seconds using actual system clock ticks
clockTicks := pm.getClockTicks()
cpuSeconds := cpuDelta / clockTicks
cpuPercent := (cpuSeconds / timeDelta) * GetConfig().PercentageMultiplier
// Apply bounds
if cpuPercent > maxCPUPercent {
cpuPercent = maxCPUPercent
}
if cpuPercent < minCPUPercent {
cpuPercent = minCPUPercent
}
return cpuPercent
}
// No CPU delta - process was idle
if state.warmupSamples < warmupCPUSamples {
// During warmup, provide a small non-zero value to indicate process is alive
return minCPUPercent
}
return 0.0
}
func (pm *ProcessMonitor) getClockTicks() float64 {
pm.clockTicksOnce.Do(func() {
// Try to detect actual clock ticks from kernel boot parameters or /proc/stat
if data, err := os.ReadFile("/proc/cmdline"); err == nil {
// Look for HZ parameter in kernel command line
cmdline := string(data)
if strings.Contains(cmdline, "HZ=") {
fields := strings.Fields(cmdline)
for _, field := range fields {
if strings.HasPrefix(field, "HZ=") {
if hz, err := strconv.ParseFloat(field[3:], 64); err == nil && hz > 0 {
pm.clockTicks = hz
return
}
}
}
}
}
// Try reading from /proc/timer_list for more accurate detection
if data, err := os.ReadFile("/proc/timer_list"); err == nil {
timer := string(data)
// Look for tick device frequency
lines := strings.Split(timer, "\n")
for _, line := range lines {
if strings.Contains(line, "tick_period:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
if period, err := strconv.ParseInt(fields[1], 10, 64); err == nil && period > 0 {
// Convert nanoseconds to Hz
hz := GetConfig().CGONanosecondsPerSecond / float64(period)
if hz >= minValidClockTicks && hz <= maxValidClockTicks {
pm.clockTicks = hz
return
}
}
}
}
}
}
// Fallback: Most embedded ARM systems (like jetKVM) use 250 Hz or 1000 Hz
// rather than the traditional 100 Hz
pm.clockTicks = defaultClockTicks
pm.logger.Warn().Float64("clock_ticks", pm.clockTicks).Msg("Using fallback clock ticks value")
// Log successful detection for non-fallback values
if pm.clockTicks != defaultClockTicks {
pm.logger.Info().Float64("clock_ticks", pm.clockTicks).Msg("Detected system clock ticks")
}
})
return pm.clockTicks
}
func (pm *ProcessMonitor) getTotalMemory() int64 {
pm.memoryOnce.Do(func() {
file, err := os.Open("/proc/meminfo")
if err != nil {
pm.totalMemory = int64(defaultMemoryGB) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes)
return
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "MemTotal:") {
fields := strings.Fields(line)
if len(fields) >= 2 {
if kb, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
pm.totalMemory = kb * int64(GetConfig().ProcessMonitorKBToBytes)
return
}
}
break
}
}
pm.totalMemory = int64(defaultMemoryGB) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) * int64(GetConfig().ProcessMonitorKBToBytes) // Fallback
})
return pm.totalMemory
}
// GetTotalMemory returns total system memory in bytes (public method)
func (pm *ProcessMonitor) GetTotalMemory() int64 {
return pm.getTotalMemory()
}
// Global process monitor instance
var globalProcessMonitor *ProcessMonitor
var processMonitorOnce sync.Once
// GetProcessMonitor returns the global process monitor instance
func GetProcessMonitor() *ProcessMonitor {
processMonitorOnce.Do(func() {
globalProcessMonitor = NewProcessMonitor()
globalProcessMonitor.Start()
})
return globalProcessMonitor
}

View File

@ -70,7 +70,7 @@ func RunAudioOutputServer() error {
StopNonBlockingAudioStreaming() StopNonBlockingAudioStreaming()
// Give some time for cleanup // Give some time for cleanup
time.Sleep(Config.DefaultSleepDuration) time.Sleep(GetConfig().DefaultSleepDuration)
return nil return nil
} }

View File

@ -48,6 +48,9 @@ func getOutputStreamingLogger() *zerolog.Logger {
// StartAudioOutputStreaming starts audio output streaming (capturing system audio) // StartAudioOutputStreaming starts audio output streaming (capturing system audio)
func StartAudioOutputStreaming(send func([]byte)) error { func StartAudioOutputStreaming(send func([]byte)) error {
// Initialize audio monitoring (latency tracking and cache cleanup)
InitializeAudioMonitoring()
if !atomic.CompareAndSwapInt32(&outputStreamingRunning, 0, 1) { if !atomic.CompareAndSwapInt32(&outputStreamingRunning, 0, 1) {
return ErrAudioAlreadyRunning return ErrAudioAlreadyRunning
} }
@ -81,9 +84,9 @@ func StartAudioOutputStreaming(send func([]byte)) error {
buffer := make([]byte, GetMaxAudioFrameSize()) buffer := make([]byte, GetMaxAudioFrameSize())
consecutiveErrors := 0 consecutiveErrors := 0
maxConsecutiveErrors := Config.MaxConsecutiveErrors maxConsecutiveErrors := GetConfig().MaxConsecutiveErrors
errorBackoffDelay := Config.RetryDelay errorBackoffDelay := GetConfig().RetryDelay
maxErrorBackoff := Config.MaxRetryDelay maxErrorBackoff := GetConfig().MaxRetryDelay
for { for {
select { select {
@ -120,18 +123,18 @@ func StartAudioOutputStreaming(send func([]byte)) error {
Err(initErr). Err(initErr).
Msg("Failed to reinitialize audio system") Msg("Failed to reinitialize audio system")
// Exponential backoff for reinitialization failures // Exponential backoff for reinitialization failures
errorBackoffDelay = time.Duration(float64(errorBackoffDelay) * Config.BackoffMultiplier) errorBackoffDelay = time.Duration(float64(errorBackoffDelay) * GetConfig().BackoffMultiplier)
if errorBackoffDelay > maxErrorBackoff { if errorBackoffDelay > maxErrorBackoff {
errorBackoffDelay = maxErrorBackoff errorBackoffDelay = maxErrorBackoff
} }
} else { } else {
getOutputStreamingLogger().Info().Msg("Audio system reinitialized successfully") getOutputStreamingLogger().Info().Msg("Audio system reinitialized successfully")
consecutiveErrors = 0 consecutiveErrors = 0
errorBackoffDelay = Config.RetryDelay // Reset backoff errorBackoffDelay = GetConfig().RetryDelay // Reset backoff
} }
} else { } else {
// Brief delay for transient errors // Brief delay for transient errors
time.Sleep(Config.ShortSleepDuration) time.Sleep(GetConfig().ShortSleepDuration)
} }
continue continue
} }
@ -139,7 +142,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
// Success - reset error counters // Success - reset error counters
if consecutiveErrors > 0 { if consecutiveErrors > 0 {
consecutiveErrors = 0 consecutiveErrors = 0
errorBackoffDelay = Config.RetryDelay errorBackoffDelay = GetConfig().RetryDelay
} }
if n > 0 { if n > 0 {
@ -161,7 +164,7 @@ func StartAudioOutputStreaming(send func([]byte)) error {
RecordFrameReceived(n) RecordFrameReceived(n)
} }
// Small delay to prevent busy waiting // Small delay to prevent busy waiting
time.Sleep(Config.ShortSleepDuration) time.Sleep(GetConfig().ShortSleepDuration)
} }
} }
}() }()
@ -182,6 +185,6 @@ func StopAudioOutputStreaming() {
// Wait for streaming to stop // Wait for streaming to stop
for atomic.LoadInt32(&outputStreamingRunning) == 1 { for atomic.LoadInt32(&outputStreamingRunning) == 1 {
time.Sleep(Config.ShortSleepDuration) time.Sleep(GetConfig().ShortSleepDuration)
} }
} }

View File

@ -19,19 +19,19 @@ const (
// Restart configuration is now retrieved from centralized config // Restart configuration is now retrieved from centralized config
func getMaxRestartAttempts() int { func getMaxRestartAttempts() int {
return Config.MaxRestartAttempts return GetConfig().MaxRestartAttempts
} }
func getRestartWindow() time.Duration { func getRestartWindow() time.Duration {
return Config.RestartWindow return GetConfig().RestartWindow
} }
func getRestartDelay() time.Duration { func getRestartDelay() time.Duration {
return Config.RestartDelay return GetConfig().RestartDelay
} }
func getMaxRestartDelay() time.Duration { func getMaxRestartDelay() time.Duration {
return Config.MaxRestartDelay return GetConfig().MaxRestartDelay
} }
// AudioOutputSupervisor manages the audio output server subprocess lifecycle // AudioOutputSupervisor manages the audio output server subprocess lifecycle
@ -125,12 +125,6 @@ func (s *AudioOutputSupervisor) Start() error {
// Start the supervision loop // Start the supervision loop
go s.supervisionLoop() go s.supervisionLoop()
// Establish IPC connection to subprocess after a brief delay
go func() {
time.Sleep(500 * time.Millisecond) // Wait for subprocess to start
s.connectClient()
}()
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component started successfully") s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component started successfully")
return nil return nil
} }
@ -151,20 +145,11 @@ func (s *AudioOutputSupervisor) Stop() {
select { select {
case <-s.processDone: case <-s.processDone:
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped gracefully") s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped gracefully")
case <-time.After(Config.OutputSupervisorTimeout): case <-time.After(GetConfig().OutputSupervisorTimeout):
s.logger.Warn().Str("component", AudioOutputSupervisorComponent).Msg("component did not stop gracefully, forcing termination") s.logger.Warn().Str("component", AudioOutputSupervisorComponent).Msg("component did not stop gracefully, forcing termination")
s.forceKillProcess("audio output server") s.forceKillProcess("audio output server")
} }
// Ensure socket file cleanup even if subprocess didn't clean up properly
// This prevents "address already in use" errors on restart
outputSocketPath := getOutputSocketPath()
if err := os.Remove(outputSocketPath); err != nil && !os.IsNotExist(err) {
s.logger.Warn().Err(err).Str("socket_path", outputSocketPath).Msg("failed to remove output socket file during supervisor stop")
} else if err == nil {
s.logger.Debug().Str("socket_path", outputSocketPath).Msg("cleaned up output socket file")
}
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped") s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped")
} }
@ -173,7 +158,7 @@ func (s *AudioOutputSupervisor) supervisionLoop() {
// Configure supervision parameters // Configure supervision parameters
config := SupervisionConfig{ config := SupervisionConfig{
ProcessType: "audio output server", ProcessType: "audio output server",
Timeout: Config.OutputSupervisorTimeout, Timeout: GetConfig().OutputSupervisorTimeout,
EnableRestart: true, EnableRestart: true,
MaxRestartAttempts: getMaxRestartAttempts(), MaxRestartAttempts: getMaxRestartAttempts(),
RestartWindow: getRestartWindow(), RestartWindow: getRestartWindow(),
@ -228,6 +213,7 @@ func (s *AudioOutputSupervisor) startProcess() error {
s.logger.Info().Int("pid", s.processPID).Strs("args", args).Strs("opus_env", s.opusEnv).Msg("audio server process started") s.logger.Info().Int("pid", s.processPID).Strs("args", args).Strs("opus_env", s.opusEnv).Msg("audio server process started")
// Add process to monitoring // Add process to monitoring
s.processMonitor.AddProcess(s.processPID, "audio-output-server")
if s.onProcessStart != nil { if s.onProcessStart != nil {
s.onProcessStart(s.processPID) s.onProcessStart(s.processPID)
@ -289,43 +275,3 @@ func (s *AudioOutputSupervisor) calculateRestartDelay() time.Duration {
return delay return delay
} }
// client holds the IPC client for communicating with the subprocess
var outputClient *AudioOutputClient
// IsConnected returns whether the supervisor has an active connection to the subprocess
func (s *AudioOutputSupervisor) IsConnected() bool {
return outputClient != nil && outputClient.IsConnected()
}
// GetClient returns the IPC client for the subprocess
func (s *AudioOutputSupervisor) GetClient() *AudioOutputClient {
return outputClient
}
// connectClient establishes connection to the audio output subprocess
func (s *AudioOutputSupervisor) connectClient() {
if outputClient == nil {
outputClient = NewAudioOutputClient()
}
// Try to connect to the subprocess
if err := outputClient.Connect(); err != nil {
s.logger.Warn().Err(err).Msg("Failed to connect to audio output subprocess")
} else {
s.logger.Info().Msg("Connected to audio output subprocess")
}
}
// SendOpusConfig sends Opus configuration to the audio output subprocess
func (s *AudioOutputSupervisor) SendOpusConfig(config OutputIPCOpusConfig) error {
if outputClient == nil {
return fmt.Errorf("client not initialized")
}
if !outputClient.IsConnected() {
return fmt.Errorf("client not connected")
}
return outputClient.SendOpusConfig(config)
}

View File

@ -39,7 +39,7 @@ var (
// MaxAudioFrameSize is now retrieved from centralized config // MaxAudioFrameSize is now retrieved from centralized config
func GetMaxAudioFrameSize() int { func GetMaxAudioFrameSize() int {
return Config.MaxAudioFrameSize return GetConfig().MaxAudioFrameSize
} }
// AudioQuality represents different audio quality presets // AudioQuality represents different audio quality presets
@ -74,17 +74,17 @@ type AudioMetrics struct {
var ( var (
currentConfig = AudioConfig{ currentConfig = AudioConfig{
Quality: AudioQualityMedium, Quality: AudioQualityMedium,
Bitrate: Config.AudioQualityMediumOutputBitrate, Bitrate: GetConfig().AudioQualityMediumOutputBitrate,
SampleRate: Config.SampleRate, SampleRate: GetConfig().SampleRate,
Channels: Config.Channels, Channels: GetConfig().Channels,
FrameSize: Config.AudioQualityMediumFrameSize, FrameSize: GetConfig().AudioQualityMediumFrameSize,
} }
currentMicrophoneConfig = AudioConfig{ currentMicrophoneConfig = AudioConfig{
Quality: AudioQualityMedium, Quality: AudioQualityMedium,
Bitrate: Config.AudioQualityMediumInputBitrate, Bitrate: GetConfig().AudioQualityMediumInputBitrate,
SampleRate: Config.SampleRate, SampleRate: GetConfig().SampleRate,
Channels: 1, Channels: 1,
FrameSize: Config.AudioQualityMediumFrameSize, FrameSize: GetConfig().AudioQualityMediumFrameSize,
} }
metrics AudioMetrics metrics AudioMetrics
) )
@ -96,24 +96,24 @@ var qualityPresets = map[AudioQuality]struct {
frameSize time.Duration frameSize time.Duration
}{ }{
AudioQualityLow: { AudioQualityLow: {
outputBitrate: Config.AudioQualityLowOutputBitrate, inputBitrate: Config.AudioQualityLowInputBitrate, outputBitrate: GetConfig().AudioQualityLowOutputBitrate, inputBitrate: GetConfig().AudioQualityLowInputBitrate,
sampleRate: Config.AudioQualityLowSampleRate, channels: Config.AudioQualityLowChannels, sampleRate: GetConfig().AudioQualityLowSampleRate, channels: GetConfig().AudioQualityLowChannels,
frameSize: Config.AudioQualityLowFrameSize, frameSize: GetConfig().AudioQualityLowFrameSize,
}, },
AudioQualityMedium: { AudioQualityMedium: {
outputBitrate: Config.AudioQualityMediumOutputBitrate, inputBitrate: Config.AudioQualityMediumInputBitrate, outputBitrate: GetConfig().AudioQualityMediumOutputBitrate, inputBitrate: GetConfig().AudioQualityMediumInputBitrate,
sampleRate: Config.AudioQualityMediumSampleRate, channels: Config.AudioQualityMediumChannels, sampleRate: GetConfig().AudioQualityMediumSampleRate, channels: GetConfig().AudioQualityMediumChannels,
frameSize: Config.AudioQualityMediumFrameSize, frameSize: GetConfig().AudioQualityMediumFrameSize,
}, },
AudioQualityHigh: { AudioQualityHigh: {
outputBitrate: Config.AudioQualityHighOutputBitrate, inputBitrate: Config.AudioQualityHighInputBitrate, outputBitrate: GetConfig().AudioQualityHighOutputBitrate, inputBitrate: GetConfig().AudioQualityHighInputBitrate,
sampleRate: Config.SampleRate, channels: Config.AudioQualityHighChannels, sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityHighChannels,
frameSize: Config.AudioQualityHighFrameSize, frameSize: GetConfig().AudioQualityHighFrameSize,
}, },
AudioQualityUltra: { AudioQualityUltra: {
outputBitrate: Config.AudioQualityUltraOutputBitrate, inputBitrate: Config.AudioQualityUltraInputBitrate, outputBitrate: GetConfig().AudioQualityUltraOutputBitrate, inputBitrate: GetConfig().AudioQualityUltraInputBitrate,
sampleRate: Config.SampleRate, channels: Config.AudioQualityUltraChannels, sampleRate: GetConfig().SampleRate, channels: GetConfig().AudioQualityUltraChannels,
frameSize: Config.AudioQualityUltraFrameSize, frameSize: GetConfig().AudioQualityUltraFrameSize,
}, },
} }
@ -142,7 +142,7 @@ func GetMicrophoneQualityPresets() map[AudioQuality]AudioConfig {
Bitrate: preset.inputBitrate, Bitrate: preset.inputBitrate,
SampleRate: func() int { SampleRate: func() int {
if quality == AudioQualityLow { if quality == AudioQualityLow {
return Config.AudioQualityMicLowSampleRate return GetConfig().AudioQualityMicLowSampleRate
} }
return preset.sampleRate return preset.sampleRate
}(), }(),
@ -172,84 +172,58 @@ func SetAudioQuality(quality AudioQuality) {
var complexity, vbr, signalType, bandwidth, dtx int var complexity, vbr, signalType, bandwidth, dtx int
switch quality { switch quality {
case AudioQualityLow: case AudioQualityLow:
complexity = Config.AudioQualityLowOpusComplexity complexity = GetConfig().AudioQualityLowOpusComplexity
vbr = Config.AudioQualityLowOpusVBR vbr = GetConfig().AudioQualityLowOpusVBR
signalType = Config.AudioQualityLowOpusSignalType signalType = GetConfig().AudioQualityLowOpusSignalType
bandwidth = Config.AudioQualityLowOpusBandwidth bandwidth = GetConfig().AudioQualityLowOpusBandwidth
dtx = Config.AudioQualityLowOpusDTX dtx = GetConfig().AudioQualityLowOpusDTX
case AudioQualityMedium: case AudioQualityMedium:
complexity = Config.AudioQualityMediumOpusComplexity complexity = GetConfig().AudioQualityMediumOpusComplexity
vbr = Config.AudioQualityMediumOpusVBR vbr = GetConfig().AudioQualityMediumOpusVBR
signalType = Config.AudioQualityMediumOpusSignalType signalType = GetConfig().AudioQualityMediumOpusSignalType
bandwidth = Config.AudioQualityMediumOpusBandwidth bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
dtx = Config.AudioQualityMediumOpusDTX dtx = GetConfig().AudioQualityMediumOpusDTX
case AudioQualityHigh: case AudioQualityHigh:
complexity = Config.AudioQualityHighOpusComplexity complexity = GetConfig().AudioQualityHighOpusComplexity
vbr = Config.AudioQualityHighOpusVBR vbr = GetConfig().AudioQualityHighOpusVBR
signalType = Config.AudioQualityHighOpusSignalType signalType = GetConfig().AudioQualityHighOpusSignalType
bandwidth = Config.AudioQualityHighOpusBandwidth bandwidth = GetConfig().AudioQualityHighOpusBandwidth
dtx = Config.AudioQualityHighOpusDTX dtx = GetConfig().AudioQualityHighOpusDTX
case AudioQualityUltra: case AudioQualityUltra:
complexity = Config.AudioQualityUltraOpusComplexity complexity = GetConfig().AudioQualityUltraOpusComplexity
vbr = Config.AudioQualityUltraOpusVBR vbr = GetConfig().AudioQualityUltraOpusVBR
signalType = Config.AudioQualityUltraOpusSignalType signalType = GetConfig().AudioQualityUltraOpusSignalType
bandwidth = Config.AudioQualityUltraOpusBandwidth bandwidth = GetConfig().AudioQualityUltraOpusBandwidth
dtx = Config.AudioQualityUltraOpusDTX dtx = GetConfig().AudioQualityUltraOpusDTX
default: default:
// Use medium quality as fallback // Use medium quality as fallback
complexity = Config.AudioQualityMediumOpusComplexity complexity = GetConfig().AudioQualityMediumOpusComplexity
vbr = Config.AudioQualityMediumOpusVBR vbr = GetConfig().AudioQualityMediumOpusVBR
signalType = Config.AudioQualityMediumOpusSignalType signalType = GetConfig().AudioQualityMediumOpusSignalType
bandwidth = Config.AudioQualityMediumOpusBandwidth bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
dtx = Config.AudioQualityMediumOpusDTX dtx = GetConfig().AudioQualityMediumOpusDTX
} }
// Update audio output subprocess configuration dynamically without restart // Restart audio output subprocess with new OPUS configuration
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("updating audio output quality settings dynamically")
// Set new OPUS configuration for future restarts
if supervisor := GetAudioOutputSupervisor(); supervisor != nil { if supervisor := GetAudioOutputSupervisor(); supervisor != nil {
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("restarting audio output subprocess with new quality settings")
// Set new OPUS configuration
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx) supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
// Send dynamic configuration update to running subprocess via IPC // Stop current subprocess
if supervisor.IsConnected() { supervisor.Stop()
// Convert AudioConfig to OutputIPCOpusConfig with complete Opus parameters
opusConfig := OutputIPCOpusConfig{
SampleRate: config.SampleRate,
Channels: config.Channels,
FrameSize: int(config.FrameSize.Milliseconds() * int64(config.SampleRate) / 1000), // Convert ms to samples
Bitrate: config.Bitrate * 1000, // Convert kbps to bps
Complexity: complexity,
VBR: vbr,
SignalType: signalType,
Bandwidth: bandwidth,
DTX: dtx,
}
logger.Info().Interface("opusConfig", opusConfig).Msg("sending Opus configuration to audio output subprocess") // Start subprocess with new configuration
if err := supervisor.SendOpusConfig(opusConfig); err != nil { if err := supervisor.Start(); err != nil {
logger.Warn().Err(err).Msg("failed to send dynamic Opus config update via IPC, falling back to subprocess restart") logger.Error().Err(err).Msg("failed to restart audio output subprocess")
// Fallback to subprocess restart if IPC update fails }
supervisor.Stop() } else {
if err := supervisor.Start(); err != nil { // Fallback to dynamic update if supervisor is not available
logger.Error().Err(err).Msg("failed to restart audio output subprocess after IPC update failure") vbrConstraint := GetConfig().CGOOpusVBRConstraint
} if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
} else { logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
logger.Info().Msg("audio output quality updated dynamically via IPC")
// Reset audio output stats after config update
go func() {
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
// Reset audio input server stats to clear persistent warnings
ResetGlobalAudioInputServerStats()
// Attempt recovery if there are still issues
time.Sleep(1 * time.Second)
RecoverGlobalAudioInputServer()
}()
}
} else {
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio output subprocess not connected, configuration will apply on next start")
} }
} }
} }
@ -260,16 +234,6 @@ func GetAudioConfig() AudioConfig {
return currentConfig return currentConfig
} }
// Simplified OPUS parameter lookup table
var opusParams = map[AudioQuality]struct {
complexity, vbr, signalType, bandwidth, dtx int
}{
AudioQualityLow: {Config.AudioQualityLowOpusComplexity, Config.AudioQualityLowOpusVBR, Config.AudioQualityLowOpusSignalType, Config.AudioQualityLowOpusBandwidth, Config.AudioQualityLowOpusDTX},
AudioQualityMedium: {Config.AudioQualityMediumOpusComplexity, Config.AudioQualityMediumOpusVBR, Config.AudioQualityMediumOpusSignalType, Config.AudioQualityMediumOpusBandwidth, Config.AudioQualityMediumOpusDTX},
AudioQualityHigh: {Config.AudioQualityHighOpusComplexity, Config.AudioQualityHighOpusVBR, Config.AudioQualityHighOpusSignalType, Config.AudioQualityHighOpusBandwidth, Config.AudioQualityHighOpusDTX},
AudioQualityUltra: {Config.AudioQualityUltraOpusComplexity, Config.AudioQualityUltraOpusVBR, Config.AudioQualityUltraOpusSignalType, Config.AudioQualityUltraOpusBandwidth, Config.AudioQualityUltraOpusDTX},
}
// SetMicrophoneQuality updates the current microphone quality configuration // SetMicrophoneQuality updates the current microphone quality configuration
func SetMicrophoneQuality(quality AudioQuality) { func SetMicrophoneQuality(quality AudioQuality) {
// Validate audio quality parameter // Validate audio quality parameter
@ -284,32 +248,51 @@ func SetMicrophoneQuality(quality AudioQuality) {
if config, exists := presets[quality]; exists { if config, exists := presets[quality]; exists {
currentMicrophoneConfig = config currentMicrophoneConfig = config
// Get OPUS parameters using lookup table // Get OPUS parameters for the selected quality
params, exists := opusParams[quality] var complexity, vbr, signalType, bandwidth, dtx int
if !exists { switch quality {
// Fallback to medium quality case AudioQualityLow:
params = opusParams[AudioQualityMedium] complexity = GetConfig().AudioQualityLowOpusComplexity
vbr = GetConfig().AudioQualityLowOpusVBR
signalType = GetConfig().AudioQualityLowOpusSignalType
bandwidth = GetConfig().AudioQualityLowOpusBandwidth
dtx = GetConfig().AudioQualityLowOpusDTX
case AudioQualityMedium:
complexity = GetConfig().AudioQualityMediumOpusComplexity
vbr = GetConfig().AudioQualityMediumOpusVBR
signalType = GetConfig().AudioQualityMediumOpusSignalType
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
dtx = GetConfig().AudioQualityMediumOpusDTX
case AudioQualityHigh:
complexity = GetConfig().AudioQualityHighOpusComplexity
vbr = GetConfig().AudioQualityHighOpusVBR
signalType = GetConfig().AudioQualityHighOpusSignalType
bandwidth = GetConfig().AudioQualityHighOpusBandwidth
dtx = GetConfig().AudioQualityHighOpusDTX
case AudioQualityUltra:
complexity = GetConfig().AudioQualityUltraOpusComplexity
vbr = GetConfig().AudioQualityUltraOpusVBR
signalType = GetConfig().AudioQualityUltraOpusSignalType
bandwidth = GetConfig().AudioQualityUltraOpusBandwidth
dtx = GetConfig().AudioQualityUltraOpusDTX
default:
// Use medium quality as fallback
complexity = GetConfig().AudioQualityMediumOpusComplexity
vbr = GetConfig().AudioQualityMediumOpusVBR
signalType = GetConfig().AudioQualityMediumOpusSignalType
bandwidth = GetConfig().AudioQualityMediumOpusBandwidth
dtx = GetConfig().AudioQualityMediumOpusDTX
} }
// Update audio input subprocess configuration dynamically without restart // Update audio input subprocess configuration dynamically without restart
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
// Set new OPUS configuration for future restarts
if supervisor := GetAudioInputSupervisor(); supervisor != nil { if supervisor := GetAudioInputSupervisor(); supervisor != nil {
supervisor.SetOpusConfig(config.Bitrate*1000, params.complexity, params.vbr, params.signalType, params.bandwidth, params.dtx) logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
logger.Info().Int("quality", int(quality)).Msg("updating audio input subprocess quality settings dynamically")
// Check if microphone is active but IPC control is broken // Set new OPUS configuration for future restarts
inputManager := getAudioInputManager() supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
if inputManager.IsRunning() && !supervisor.IsConnected() {
// Reconnect the IPC control channel
supervisor.Stop()
time.Sleep(50 * time.Millisecond)
if err := supervisor.Start(); err != nil {
logger.Debug().Err(err).Msg("failed to reconnect IPC control channel")
}
}
// Send dynamic configuration update to running subprocess via IPC // Send dynamic configuration update to running subprocess
if supervisor.IsConnected() { if supervisor.IsConnected() {
// Convert AudioConfig to InputIPCOpusConfig with complete Opus parameters // Convert AudioConfig to InputIPCOpusConfig with complete Opus parameters
opusConfig := InputIPCOpusConfig{ opusConfig := InputIPCOpusConfig{
@ -317,32 +300,23 @@ func SetMicrophoneQuality(quality AudioQuality) {
Channels: config.Channels, Channels: config.Channels,
FrameSize: int(config.FrameSize.Milliseconds() * int64(config.SampleRate) / 1000), // Convert ms to samples FrameSize: int(config.FrameSize.Milliseconds() * int64(config.SampleRate) / 1000), // Convert ms to samples
Bitrate: config.Bitrate * 1000, // Convert kbps to bps Bitrate: config.Bitrate * 1000, // Convert kbps to bps
Complexity: params.complexity, Complexity: complexity,
VBR: params.vbr, VBR: vbr,
SignalType: params.signalType, SignalType: signalType,
Bandwidth: params.bandwidth, Bandwidth: bandwidth,
DTX: params.dtx, DTX: dtx,
} }
logger.Info().Interface("opusConfig", opusConfig).Msg("sending Opus configuration to audio input subprocess")
if err := supervisor.SendOpusConfig(opusConfig); err != nil { if err := supervisor.SendOpusConfig(opusConfig); err != nil {
logger.Debug().Err(err).Msg("failed to send dynamic Opus config update via IPC") logger.Warn().Err(err).Msg("failed to send dynamic Opus config update, subprocess may need restart")
// Fallback to subprocess restart if IPC update fails // Fallback to restart if dynamic update fails
supervisor.Stop() supervisor.Stop()
if err := supervisor.Start(); err != nil { if err := supervisor.Start(); err != nil {
logger.Error().Err(err).Msg("failed to restart audio input subprocess after IPC update failure") logger.Error().Err(err).Msg("failed to restart audio input subprocess after config update failure")
} }
} else { } else {
logger.Info().Msg("audio input quality updated dynamically via IPC") logger.Info().Msg("audio input quality updated dynamically with complete Opus configuration")
// Reset audio input stats after config update
go func() {
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
// Reset audio input server stats to clear persistent warnings
ResetGlobalAudioInputServerStats()
// Attempt recovery if microphone is still having issues
time.Sleep(1 * time.Second)
RecoverGlobalAudioInputServer()
}()
} }
} else { } else {
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio input subprocess not connected, configuration will apply on next start") logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio input subprocess not connected, configuration will apply on next start")

View File

@ -2,9 +2,7 @@ package audio
import ( import (
"errors" "errors"
"fmt"
"sync" "sync"
"time"
) )
// Global relay instance for the main process // Global relay instance for the main process
@ -30,34 +28,13 @@ func StartAudioRelay(audioTrack AudioTrackWriter) error {
// Get current audio config // Get current audio config
config := GetAudioConfig() config := GetAudioConfig()
// Retry starting the relay with exponential backoff // Start the relay (audioTrack can be nil initially)
// This handles cases where the subprocess hasn't created its socket yet if err := relay.Start(audioTrack, config); err != nil {
maxAttempts := 5 return err
baseDelay := 200 * time.Millisecond
maxDelay := 2 * time.Second
var lastErr error
for i := 0; i < maxAttempts; i++ {
if err := relay.Start(audioTrack, config); err != nil {
lastErr = err
if i < maxAttempts-1 {
// Calculate exponential backoff delay
delay := time.Duration(float64(baseDelay) * (1.5 * float64(i+1)))
if delay > maxDelay {
delay = maxDelay
}
time.Sleep(delay)
continue
}
return fmt.Errorf("failed to start audio relay after %d attempts: %w", maxAttempts, lastErr)
}
// Success
globalRelay = relay
return nil
} }
return fmt.Errorf("failed to start audio relay after %d attempts: %w", maxAttempts, lastErr) globalRelay = relay
return nil
} }
// StopAudioRelay stops the audio relay system // StopAudioRelay stops the audio relay system
@ -112,93 +89,37 @@ func IsAudioRelayRunning() bool {
} }
// UpdateAudioRelayTrack updates the WebRTC audio track for the relay // UpdateAudioRelayTrack updates the WebRTC audio track for the relay
// This function is refactored to prevent mutex deadlocks during quality changes
func UpdateAudioRelayTrack(audioTrack AudioTrackWriter) error { func UpdateAudioRelayTrack(audioTrack AudioTrackWriter) error {
var needsCallback bool
var callbackFunc TrackReplacementCallback
// Critical section: minimize time holding the mutex
relayMutex.Lock() relayMutex.Lock()
defer relayMutex.Unlock()
if globalRelay == nil { if globalRelay == nil {
// No relay running, start one with the provided track // No relay running, start one with the provided track
relay := NewAudioRelay() relay := NewAudioRelay()
config := GetAudioConfig() config := GetAudioConfig()
if err := relay.Start(audioTrack, config); err != nil { if err := relay.Start(audioTrack, config); err != nil {
relayMutex.Unlock()
return err return err
} }
globalRelay = relay globalRelay = relay
} else { return nil
// Update the track in the existing relay
globalRelay.UpdateTrack(audioTrack)
}
// Capture callback state while holding mutex
needsCallback = trackReplacementCallback != nil
if needsCallback {
callbackFunc = trackReplacementCallback
}
relayMutex.Unlock()
// Execute callback outside of mutex to prevent deadlock
if needsCallback && callbackFunc != nil {
// Use goroutine with timeout to prevent blocking
done := make(chan error, 1)
go func() {
done <- callbackFunc(audioTrack)
}()
// Wait for callback with timeout
select {
case err := <-done:
if err != nil {
// Log error but don't fail the relay operation
// The relay can still work even if WebRTC track replacement fails
_ = err // Suppress linter warning
}
case <-time.After(5 * time.Second):
// Timeout: log warning but continue
// This prevents indefinite blocking during quality changes
_ = fmt.Errorf("track replacement callback timed out")
}
} }
// Update the track in the existing relay
globalRelay.UpdateTrack(audioTrack)
return nil return nil
} }
// CurrentSessionCallback is a function type for getting the current session's audio track // CurrentSessionCallback is a function type for getting the current session's audio track
type CurrentSessionCallback func() AudioTrackWriter type CurrentSessionCallback func() AudioTrackWriter
// TrackReplacementCallback is a function type for replacing the WebRTC audio track
type TrackReplacementCallback func(AudioTrackWriter) error
// currentSessionCallback holds the callback function to get the current session's audio track // currentSessionCallback holds the callback function to get the current session's audio track
var currentSessionCallback CurrentSessionCallback var currentSessionCallback CurrentSessionCallback
// trackReplacementCallback holds the callback function to replace the WebRTC audio track
var trackReplacementCallback TrackReplacementCallback
// SetCurrentSessionCallback sets the callback function to get the current session's audio track // SetCurrentSessionCallback sets the callback function to get the current session's audio track
func SetCurrentSessionCallback(callback CurrentSessionCallback) { func SetCurrentSessionCallback(callback CurrentSessionCallback) {
currentSessionCallback = callback currentSessionCallback = callback
} }
// SetTrackReplacementCallback sets the callback function to replace the WebRTC audio track
func SetTrackReplacementCallback(callback TrackReplacementCallback) {
trackReplacementCallback = callback
}
// UpdateAudioRelayTrackAsync performs async track update to prevent blocking
// This is used during WebRTC session creation to avoid deadlocks
func UpdateAudioRelayTrackAsync(audioTrack AudioTrackWriter) {
go func() {
if err := UpdateAudioRelayTrack(audioTrack); err != nil {
// Log error but don't block session creation
_ = err // Suppress linter warning
}
}()
}
// connectRelayToCurrentSession connects the audio relay to the current WebRTC session's audio track // connectRelayToCurrentSession connects the audio relay to the current WebRTC session's audio track
// This is used when restarting the relay during unmute operations // This is used when restarting the relay during unmute operations
func connectRelayToCurrentSession() error { func connectRelayToCurrentSession() error {

View File

@ -18,8 +18,8 @@ type SocketBufferConfig struct {
// DefaultSocketBufferConfig returns the default socket buffer configuration // DefaultSocketBufferConfig returns the default socket buffer configuration
func DefaultSocketBufferConfig() SocketBufferConfig { func DefaultSocketBufferConfig() SocketBufferConfig {
return SocketBufferConfig{ return SocketBufferConfig{
SendBufferSize: Config.SocketOptimalBuffer, SendBufferSize: GetConfig().SocketOptimalBuffer,
RecvBufferSize: Config.SocketOptimalBuffer, RecvBufferSize: GetConfig().SocketOptimalBuffer,
Enabled: true, Enabled: true,
} }
} }
@ -27,8 +27,8 @@ func DefaultSocketBufferConfig() SocketBufferConfig {
// HighLoadSocketBufferConfig returns configuration for high-load scenarios // HighLoadSocketBufferConfig returns configuration for high-load scenarios
func HighLoadSocketBufferConfig() SocketBufferConfig { func HighLoadSocketBufferConfig() SocketBufferConfig {
return SocketBufferConfig{ return SocketBufferConfig{
SendBufferSize: Config.SocketMaxBuffer, SendBufferSize: GetConfig().SocketMaxBuffer,
RecvBufferSize: Config.SocketMaxBuffer, RecvBufferSize: GetConfig().SocketMaxBuffer,
Enabled: true, Enabled: true,
} }
} }
@ -123,8 +123,8 @@ func ValidateSocketBufferConfig(config SocketBufferConfig) error {
return nil return nil
} }
minBuffer := Config.SocketMinBuffer minBuffer := GetConfig().SocketMinBuffer
maxBuffer := Config.SocketMaxBuffer maxBuffer := GetConfig().SocketMaxBuffer
if config.SendBufferSize < minBuffer { if config.SendBufferSize < minBuffer {
return fmt.Errorf("send buffer size validation failed: got %d bytes, minimum required %d bytes (configured range: %d-%d)", return fmt.Errorf("send buffer size validation failed: got %d bytes, minimum required %d bytes (configured range: %d-%d)",

View File

@ -4,138 +4,765 @@
package audio package audio
import ( import (
"runtime"
"sort"
"sync"
"sync/atomic" "sync/atomic"
"time"
"unsafe"
) )
// AudioBufferPool provides a simple buffer pool for audio processing // AudioLatencyInfo holds simplified latency information for cleanup decisions
type AudioBufferPool struct { type AudioLatencyInfo struct {
// Atomic counters LatencyMs float64
hitCount int64 // Pool hit counter (atomic) Timestamp time.Time
missCount int64 // Pool miss counter (atomic)
// Pool configuration
bufferSize int
pool chan []byte
maxSize int
} }
// NewAudioBufferPool creates a new simple audio buffer pool // Global latency tracking
func NewAudioBufferPool(bufferSize int) *AudioBufferPool { var (
maxSize := Config.MaxPoolSize currentAudioLatency = AudioLatencyInfo{}
if maxSize <= 0 { currentAudioLatencyLock sync.RWMutex
maxSize = Config.BufferPoolDefaultSize audioMonitoringInitialized int32 // Atomic flag to track initialization
)
// InitializeAudioMonitoring starts the background goroutines for latency tracking and cache cleanup
// This is safe to call multiple times as it will only initialize once
func InitializeAudioMonitoring() {
// Use atomic CAS to ensure we only initialize once
if atomic.CompareAndSwapInt32(&audioMonitoringInitialized, 0, 1) {
// Start the latency recorder
startLatencyRecorder()
// Start the cleanup goroutine
startCleanupGoroutine()
}
}
// latencyChannel is used for non-blocking latency recording
var latencyChannel = make(chan float64, 10)
// startLatencyRecorder starts the latency recorder goroutine
// This should be called during package initialization
func startLatencyRecorder() {
go latencyRecorderLoop()
}
// latencyRecorderLoop processes latency recordings in the background
func latencyRecorderLoop() {
for latencyMs := range latencyChannel {
currentAudioLatencyLock.Lock()
currentAudioLatency = AudioLatencyInfo{
LatencyMs: latencyMs,
Timestamp: time.Now(),
}
currentAudioLatencyLock.Unlock()
}
}
// RecordAudioLatency records the current audio processing latency
// This is called from the audio input manager when latency is measured
// It is non-blocking to ensure zero overhead in the critical audio path
func RecordAudioLatency(latencyMs float64) {
// Non-blocking send - if channel is full, we drop the update
select {
case latencyChannel <- latencyMs:
// Successfully sent
default:
// Channel full, drop this update to avoid blocking the audio path
}
}
// GetAudioLatencyMetrics returns the current audio latency information
// Returns nil if no latency data is available or if it's too old
func GetAudioLatencyMetrics() *AudioLatencyInfo {
currentAudioLatencyLock.RLock()
defer currentAudioLatencyLock.RUnlock()
// Check if we have valid latency data
if currentAudioLatency.Timestamp.IsZero() {
return nil
} }
pool := &AudioBufferPool{ // Check if the data is too old (more than 5 seconds)
bufferSize: bufferSize, if time.Since(currentAudioLatency.Timestamp) > 5*time.Second {
pool: make(chan []byte, maxSize), return nil
maxSize: maxSize,
} }
// Pre-populate the pool return &AudioLatencyInfo{
for i := 0; i < maxSize/2; i++ { LatencyMs: currentAudioLatency.LatencyMs,
buf := make([]byte, bufferSize) Timestamp: currentAudioLatency.Timestamp,
}
}
// Enhanced lock-free buffer cache for per-goroutine optimization
type lockFreeBufferCache struct {
buffers [8]*[]byte // Increased from 4 to 8 buffers per goroutine cache for better hit rates
}
const (
// Enhanced cache configuration for per-goroutine optimization
cacheSize = 8 // Increased from 4 to 8 buffers per goroutine cache for better hit rates
cacheTTL = 10 * time.Second // Increased from 5s to 10s for better cache retention
// Additional cache constants for enhanced performance
maxCacheEntries = 256 // Maximum number of goroutine cache entries to prevent memory bloat
cacheCleanupInterval = 30 * time.Second // How often to clean up stale cache entries
cacheWarmupThreshold = 50 // Number of requests before enabling cache warmup
cacheHitRateTarget = 0.85 // Target cache hit rate for optimization
)
// TTL tracking for goroutine cache entries
type cacheEntry struct {
cache *lockFreeBufferCache
lastAccess int64 // Unix timestamp of last access
gid int64 // Goroutine ID for better tracking
}
// Per-goroutine buffer cache using goroutine-local storage
var goroutineBufferCache = make(map[int64]*lockFreeBufferCache)
var goroutineCacheMutex sync.RWMutex
var lastCleanupTime int64 // Unix timestamp of last cleanup
const maxCacheSize = 500 // Maximum number of goroutine caches (reduced from 1000)
const cleanupInterval int64 = 30 // Cleanup interval in seconds (30 seconds, reduced from 60)
const bufferTTL int64 = 60 // Time-to-live for cached buffers in seconds (1 minute, reduced from 2)
// getGoroutineID extracts goroutine ID from runtime stack for cache key
func getGoroutineID() int64 {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
// Parse "goroutine 123 [running]:" format
for i := 10; i < len(b); i++ {
if b[i] == ' ' {
id := int64(0)
for j := 10; j < i; j++ {
if b[j] >= '0' && b[j] <= '9' {
id = id*10 + int64(b[j]-'0')
}
}
return id
}
}
return 0
}
// Map of goroutine ID to cache entry with TTL tracking
var goroutineCacheWithTTL = make(map[int64]*cacheEntry)
// cleanupChannel is used for asynchronous cleanup requests
var cleanupChannel = make(chan struct{}, 1)
// startCleanupGoroutine starts the cleanup goroutine
// This should be called during package initialization
func startCleanupGoroutine() {
go cleanupLoop()
}
// cleanupLoop processes cleanup requests in the background
func cleanupLoop() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select { select {
case pool.pool <- buf: case <-cleanupChannel:
default: // Received explicit cleanup request
break performCleanup(true)
case <-ticker.C:
// Regular cleanup check
performCleanup(false)
}
}
}
// requestCleanup signals the cleanup goroutine to perform a cleanup
// This is non-blocking and can be called from the critical path
func requestCleanup() {
select {
case cleanupChannel <- struct{}{}:
// Successfully requested cleanup
default:
// Channel full, cleanup already pending
}
}
// performCleanup does the actual cache cleanup work
// This runs in a dedicated goroutine, not in the critical path
func performCleanup(forced bool) {
now := time.Now().Unix()
lastCleanup := atomic.LoadInt64(&lastCleanupTime)
// Check if we're in a high-latency situation
isHighLatency := false
latencyMetrics := GetAudioLatencyMetrics()
if latencyMetrics != nil && latencyMetrics.LatencyMs > 10.0 {
// Under high latency, be more aggressive with cleanup
isHighLatency = true
}
// Only cleanup if enough time has passed (less time if high latency) or if forced
interval := cleanupInterval
if isHighLatency {
interval = cleanupInterval / 2 // More frequent cleanup under high latency
}
if !forced && now-lastCleanup < interval {
return
}
// Try to acquire cleanup lock atomically
if !atomic.CompareAndSwapInt64(&lastCleanupTime, lastCleanup, now) {
return // Another goroutine is already cleaning up
}
// Perform the actual cleanup
doCleanupGoroutineCache()
}
// cleanupGoroutineCache triggers an asynchronous cleanup of the goroutine cache
// This is safe to call from the critical path as it's non-blocking
func cleanupGoroutineCache() {
// Request asynchronous cleanup
requestCleanup()
}
// The actual cleanup implementation that runs in the background goroutine
func doCleanupGoroutineCache() {
// Get current time for TTL calculations
now := time.Now().Unix()
// Check if we're in a high-latency situation
isHighLatency := false
latencyMetrics := GetAudioLatencyMetrics()
if latencyMetrics != nil && latencyMetrics.LatencyMs > 10.0 {
// Under high latency, be more aggressive with cleanup
isHighLatency = true
}
goroutineCacheMutex.Lock()
defer goroutineCacheMutex.Unlock()
// Convert old cache format to new TTL-based format if needed
if len(goroutineCacheWithTTL) == 0 && len(goroutineBufferCache) > 0 {
for gid, cache := range goroutineBufferCache {
goroutineCacheWithTTL[gid] = &cacheEntry{
cache: cache,
lastAccess: now,
gid: gid,
}
}
// Clear old cache to free memory
goroutineBufferCache = make(map[int64]*lockFreeBufferCache)
}
// Enhanced cleanup with size limits and better TTL management
entriesToRemove := make([]int64, 0)
ttl := bufferTTL
if isHighLatency {
// Under high latency, use a much shorter TTL
ttl = bufferTTL / 4
}
// Remove entries older than enhanced TTL
for gid, entry := range goroutineCacheWithTTL {
// Both now and entry.lastAccess are int64, so this comparison is safe
if now-entry.lastAccess > ttl {
entriesToRemove = append(entriesToRemove, gid)
} }
} }
return pool // If we have too many cache entries, remove the oldest ones
if len(goroutineCacheWithTTL) > maxCacheEntries {
// Sort by last access time and remove oldest entries
type cacheEntryWithGID struct {
gid int64
lastAccess int64
}
entries := make([]cacheEntryWithGID, 0, len(goroutineCacheWithTTL))
for gid, entry := range goroutineCacheWithTTL {
entries = append(entries, cacheEntryWithGID{gid: gid, lastAccess: entry.lastAccess})
}
// Sort by last access time (oldest first)
sort.Slice(entries, func(i, j int) bool {
return entries[i].lastAccess < entries[j].lastAccess
})
// Mark oldest entries for removal
excessCount := len(goroutineCacheWithTTL) - maxCacheEntries
for i := 0; i < excessCount && i < len(entries); i++ {
entriesToRemove = append(entriesToRemove, entries[i].gid)
}
}
// If cache is still too large after TTL cleanup, remove oldest entries
// Under high latency, use a more aggressive target size
targetSize := maxCacheSize
targetReduction := maxCacheSize / 2
if isHighLatency {
// Under high latency, target a much smaller cache size
targetSize = maxCacheSize / 4
targetReduction = maxCacheSize / 8
}
if len(goroutineCacheWithTTL) > targetSize {
// Find oldest entries
type ageEntry struct {
gid int64
lastAccess int64
}
oldestEntries := make([]ageEntry, 0, len(goroutineCacheWithTTL))
for gid, entry := range goroutineCacheWithTTL {
oldestEntries = append(oldestEntries, ageEntry{gid, entry.lastAccess})
}
// Sort by lastAccess (oldest first)
sort.Slice(oldestEntries, func(i, j int) bool {
return oldestEntries[i].lastAccess < oldestEntries[j].lastAccess
})
// Remove oldest entries to get down to target reduction size
toRemove := len(goroutineCacheWithTTL) - targetReduction
for i := 0; i < toRemove && i < len(oldestEntries); i++ {
entriesToRemove = append(entriesToRemove, oldestEntries[i].gid)
}
}
// Remove marked entries and return their buffers to the pool
for _, gid := range entriesToRemove {
if entry, exists := goroutineCacheWithTTL[gid]; exists {
// Return buffers to main pool before removing entry
for i, buf := range entry.cache.buffers {
if buf != nil {
// Clear the buffer slot atomically
entry.cache.buffers[i] = nil
}
}
delete(goroutineCacheWithTTL, gid)
}
}
}
type AudioBufferPool struct {
// Atomic fields MUST be first for ARM32 alignment (int64 fields need 8-byte alignment)
currentSize int64 // Current pool size (atomic)
hitCount int64 // Pool hit counter (atomic)
missCount int64 // Pool miss counter (atomic)
// Other fields
pool sync.Pool
bufferSize int
maxPoolSize int
mutex sync.RWMutex
// Memory optimization fields
preallocated []*[]byte // Pre-allocated buffers for immediate use
preallocSize int // Number of pre-allocated buffers
}
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
// Validate buffer size parameter
if err := ValidateBufferSize(bufferSize); err != nil {
// Use default value on validation error
bufferSize = GetConfig().AudioFramePoolSize
}
// Enhanced preallocation strategy based on buffer size and system capacity
var preallocSize int
if bufferSize <= GetConfig().AudioFramePoolSize {
// For smaller pools, use enhanced preallocation (40% instead of 20%)
preallocSize = GetConfig().PreallocPercentage * 2
} else {
// For larger pools, use standard enhanced preallocation (30% instead of 10%)
preallocSize = (GetConfig().PreallocPercentage * 3) / 2
}
// Ensure minimum preallocation for better performance
minPrealloc := 50 // Minimum 50 buffers for startup performance
if preallocSize < minPrealloc {
preallocSize = minPrealloc
}
// Pre-allocate with exact capacity to avoid slice growth
preallocated := make([]*[]byte, 0, preallocSize)
// Pre-allocate buffers with optimized capacity
for i := 0; i < preallocSize; i++ {
// Use exact buffer size to prevent over-allocation
buf := make([]byte, 0, bufferSize)
preallocated = append(preallocated, &buf)
}
return &AudioBufferPool{
bufferSize: bufferSize,
maxPoolSize: GetConfig().MaxPoolSize * 2, // Double the max pool size for better buffering
preallocated: preallocated,
preallocSize: preallocSize,
pool: sync.Pool{
New: func() interface{} {
// Allocate exact size to minimize memory waste
buf := make([]byte, 0, bufferSize)
return &buf
},
},
}
} }
// Get retrieves a buffer from the pool
func (p *AudioBufferPool) Get() []byte { func (p *AudioBufferPool) Get() []byte {
select { // Skip cleanup trigger in hotpath - cleanup runs in background
case buf := <-p.pool: // cleanupGoroutineCache() - moved to background goroutine
// Fast path: Try lock-free per-goroutine cache first
gid := getGoroutineID()
goroutineCacheMutex.RLock()
cacheEntry, exists := goroutineCacheWithTTL[gid]
goroutineCacheMutex.RUnlock()
if exists && cacheEntry != nil && cacheEntry.cache != nil {
// Try to get buffer from lock-free cache
cache := cacheEntry.cache
for i := 0; i < len(cache.buffers); i++ {
bufPtr := (*unsafe.Pointer)(unsafe.Pointer(&cache.buffers[i]))
buf := (*[]byte)(atomic.LoadPointer(bufPtr))
if buf != nil && atomic.CompareAndSwapPointer(bufPtr, unsafe.Pointer(buf), nil) {
// Direct hit count update to avoid sampling complexity in critical path
atomic.AddInt64(&p.hitCount, 1)
*buf = (*buf)[:0]
return *buf
}
}
// Update access time only after cache miss to reduce overhead
cacheEntry.lastAccess = time.Now().Unix()
}
// Fallback: Try pre-allocated pool with mutex
p.mutex.Lock()
if len(p.preallocated) > 0 {
lastIdx := len(p.preallocated) - 1
buf := p.preallocated[lastIdx]
p.preallocated = p.preallocated[:lastIdx]
p.mutex.Unlock()
// Direct hit count update to avoid sampling complexity in critical path
atomic.AddInt64(&p.hitCount, 1) atomic.AddInt64(&p.hitCount, 1)
return buf[:0] // Reset length but keep capacity *buf = (*buf)[:0]
default: return *buf
atomic.AddInt64(&p.missCount, 1)
return make([]byte, 0, p.bufferSize)
} }
p.mutex.Unlock()
// Try sync.Pool next
if poolBuf := p.pool.Get(); poolBuf != nil {
buf := poolBuf.(*[]byte)
// Direct hit count update to avoid sampling complexity in critical path
atomic.AddInt64(&p.hitCount, 1)
atomic.AddInt64(&p.currentSize, -1)
// Fast capacity check - most buffers should be correct size
if cap(*buf) >= p.bufferSize {
*buf = (*buf)[:0]
return *buf
}
// Buffer too small, fall through to allocation
}
// Pool miss - allocate new buffer with exact capacity
// Direct miss count update to avoid sampling complexity in critical path
atomic.AddInt64(&p.missCount, 1)
return make([]byte, 0, p.bufferSize)
} }
// Put returns a buffer to the pool
func (p *AudioBufferPool) Put(buf []byte) { func (p *AudioBufferPool) Put(buf []byte) {
if buf == nil || cap(buf) != p.bufferSize { // Fast validation - reject buffers that are too small or too large
return // Invalid buffer bufCap := cap(buf)
if bufCap < p.bufferSize || bufCap > p.bufferSize*2 {
return // Buffer size mismatch, don't pool it to prevent memory bloat
} }
// Reset the buffer // Enhanced buffer clearing - only clear if buffer contains sensitive data
buf = buf[:0] // For audio buffers, we can skip clearing for performance unless needed
// This reduces CPU overhead significantly
// Try to return to pool var resetBuf []byte
select { if cap(buf) > p.bufferSize {
case p.pool <- buf: // If capacity is larger than expected, create a new properly sized buffer
// Successfully returned to pool resetBuf = make([]byte, 0, p.bufferSize)
default: } else {
// Pool is full, discard buffer // Reset length but keep capacity for reuse efficiency
resetBuf = buf[:0]
} }
// Fast path: Try to put in lock-free per-goroutine cache
gid := getGoroutineID()
goroutineCacheMutex.RLock()
entryWithTTL, exists := goroutineCacheWithTTL[gid]
goroutineCacheMutex.RUnlock()
var cache *lockFreeBufferCache
if exists && entryWithTTL != nil {
cache = entryWithTTL.cache
// Update access time only when we successfully use the cache
} else {
// Create new cache for this goroutine
cache = &lockFreeBufferCache{}
now := time.Now().Unix()
goroutineCacheMutex.Lock()
goroutineCacheWithTTL[gid] = &cacheEntry{
cache: cache,
lastAccess: now,
gid: gid,
}
goroutineCacheMutex.Unlock()
}
if cache != nil {
// Try to store in lock-free cache
for i := 0; i < len(cache.buffers); i++ {
bufPtr := (*unsafe.Pointer)(unsafe.Pointer(&cache.buffers[i]))
if atomic.CompareAndSwapPointer(bufPtr, nil, unsafe.Pointer(&resetBuf)) {
// Update access time only on successful cache
if exists && entryWithTTL != nil {
entryWithTTL.lastAccess = time.Now().Unix()
}
return // Successfully cached
}
}
}
// Fallback: Try to return to pre-allocated pool for fastest reuse
p.mutex.Lock()
if len(p.preallocated) < p.preallocSize {
p.preallocated = append(p.preallocated, &resetBuf)
p.mutex.Unlock()
return
}
p.mutex.Unlock()
// Check sync.Pool size limit to prevent excessive memory usage
if atomic.LoadInt64(&p.currentSize) >= int64(p.maxPoolSize) {
return // Pool is full, let GC handle this buffer
}
// Return to sync.Pool and update counter atomically
p.pool.Put(&resetBuf)
atomic.AddInt64(&p.currentSize, 1)
} }
// GetStats returns pool statistics // Enhanced global buffer pools for different audio frame types with improved sizing
func (p *AudioBufferPool) GetStats() AudioBufferPoolStats { var (
// Main audio frame pool with enhanced capacity
audioFramePool = NewAudioBufferPool(GetConfig().AudioFramePoolSize)
// Control message pool with enhanced capacity for better throughput
audioControlPool = NewAudioBufferPool(512) // Increased from GetConfig().OutputHeaderSize to 512 for better control message handling
)
func GetAudioFrameBuffer() []byte {
return audioFramePool.Get()
}
func PutAudioFrameBuffer(buf []byte) {
audioFramePool.Put(buf)
}
func GetAudioControlBuffer() []byte {
return audioControlPool.Get()
}
func PutAudioControlBuffer(buf []byte) {
audioControlPool.Put(buf)
}
// GetPoolStats returns detailed statistics about this buffer pool
func (p *AudioBufferPool) GetPoolStats() AudioBufferPoolDetailedStats {
p.mutex.RLock()
preallocatedCount := len(p.preallocated)
currentSize := p.currentSize
p.mutex.RUnlock()
hitCount := atomic.LoadInt64(&p.hitCount) hitCount := atomic.LoadInt64(&p.hitCount)
missCount := atomic.LoadInt64(&p.missCount) missCount := atomic.LoadInt64(&p.missCount)
totalRequests := hitCount + missCount totalRequests := hitCount + missCount
var hitRate float64 var hitRate float64
if totalRequests > 0 { if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * Config.BufferPoolHitRateBase hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
} }
return AudioBufferPoolDetailedStats{
BufferSize: p.bufferSize,
MaxPoolSize: p.maxPoolSize,
CurrentPoolSize: currentSize,
PreallocatedCount: int64(preallocatedCount),
PreallocatedMax: int64(p.preallocSize),
HitCount: hitCount,
MissCount: missCount,
HitRate: hitRate,
}
}
// AudioBufferPoolDetailedStats provides detailed pool statistics
type AudioBufferPoolDetailedStats struct {
BufferSize int
MaxPoolSize int
CurrentPoolSize int64
PreallocatedCount int64
PreallocatedMax int64
HitCount int64
MissCount int64
HitRate float64 // Percentage
TotalBytes int64 // Total memory usage in bytes
AverageBufferSize float64 // Average size of buffers in the pool
}
// GetAudioBufferPoolStats returns statistics about the audio buffer pools
type AudioBufferPoolStats struct {
FramePoolSize int64
FramePoolMax int
ControlPoolSize int64
ControlPoolMax int
// Enhanced statistics
FramePoolHitRate float64
ControlPoolHitRate float64
FramePoolDetails AudioBufferPoolDetailedStats
ControlPoolDetails AudioBufferPoolDetailedStats
}
func GetAudioBufferPoolStats() AudioBufferPoolStats {
audioFramePool.mutex.RLock()
frameSize := audioFramePool.currentSize
frameMax := audioFramePool.maxPoolSize
audioFramePool.mutex.RUnlock()
audioControlPool.mutex.RLock()
controlSize := audioControlPool.currentSize
controlMax := audioControlPool.maxPoolSize
audioControlPool.mutex.RUnlock()
// Get detailed statistics
frameDetails := audioFramePool.GetPoolStats()
controlDetails := audioControlPool.GetPoolStats()
return AudioBufferPoolStats{ return AudioBufferPoolStats{
BufferSize: p.bufferSize, FramePoolSize: frameSize,
MaxPoolSize: p.maxSize, FramePoolMax: frameMax,
CurrentSize: int64(len(p.pool)), ControlPoolSize: controlSize,
HitCount: hitCount, ControlPoolMax: controlMax,
MissCount: missCount, FramePoolHitRate: frameDetails.HitRate,
HitRate: hitRate, ControlPoolHitRate: controlDetails.HitRate,
FramePoolDetails: frameDetails,
ControlPoolDetails: controlDetails,
} }
} }
// AudioBufferPoolStats represents pool statistics // AdaptiveResize dynamically adjusts pool parameters based on performance metrics
type AudioBufferPoolStats struct { func (p *AudioBufferPool) AdaptiveResize() {
BufferSize int hitCount := atomic.LoadInt64(&p.hitCount)
MaxPoolSize int missCount := atomic.LoadInt64(&p.missCount)
CurrentSize int64 totalRequests := hitCount + missCount
HitCount int64
MissCount int64
HitRate float64
}
// Global buffer pools if totalRequests < 100 {
var ( return // Not enough data for meaningful adaptation
audioFramePool = NewAudioBufferPool(Config.AudioFramePoolSize) }
audioControlPool = NewAudioBufferPool(Config.BufferPoolControlSize)
)
// GetAudioFrameBuffer gets a buffer for audio frames hitRate := float64(hitCount) / float64(totalRequests)
func GetAudioFrameBuffer() []byte { currentSize := atomic.LoadInt64(&p.currentSize)
return audioFramePool.Get()
}
// PutAudioFrameBuffer returns a buffer to the frame pool // If hit rate is low (< 80%), consider increasing pool size
func PutAudioFrameBuffer(buf []byte) { if hitRate < 0.8 && currentSize < int64(p.maxPoolSize) {
audioFramePool.Put(buf) // Increase preallocation by 25% up to max pool size
} newPreallocSize := int(float64(len(p.preallocated)) * 1.25)
if newPreallocSize > p.maxPoolSize {
newPreallocSize = p.maxPoolSize
}
// GetAudioControlBuffer gets a buffer for control messages // Preallocate additional buffers
func GetAudioControlBuffer() []byte { for len(p.preallocated) < newPreallocSize {
return audioControlPool.Get() buf := make([]byte, p.bufferSize)
} p.preallocated = append(p.preallocated, &buf)
}
}
// PutAudioControlBuffer returns a buffer to the control pool // If hit rate is very high (> 95%) and pool is large, consider shrinking
func PutAudioControlBuffer(buf []byte) { if hitRate > 0.95 && len(p.preallocated) > p.preallocSize {
audioControlPool.Put(buf) // Reduce preallocation by 10% but not below original size
} newSize := int(float64(len(p.preallocated)) * 0.9)
if newSize < p.preallocSize {
newSize = p.preallocSize
}
// GetAudioBufferPoolStats returns statistics for all pools // Remove excess preallocated buffers
func GetAudioBufferPoolStats() map[string]AudioBufferPoolStats { if newSize < len(p.preallocated) {
return map[string]AudioBufferPoolStats{ p.preallocated = p.preallocated[:newSize]
"frame_pool": audioFramePool.GetStats(), }
"control_pool": audioControlPool.GetStats(), }
}
// WarmupCache pre-populates goroutine-local caches for better initial performance
func (p *AudioBufferPool) WarmupCache() {
// Only warmup if we have sufficient request history
hitCount := atomic.LoadInt64(&p.hitCount)
missCount := atomic.LoadInt64(&p.missCount)
totalRequests := hitCount + missCount
if totalRequests < int64(cacheWarmupThreshold) {
return
}
// Get or create cache for current goroutine
gid := getGoroutineID()
goroutineCacheMutex.RLock()
entryWithTTL, exists := goroutineCacheWithTTL[gid]
goroutineCacheMutex.RUnlock()
var cache *lockFreeBufferCache
if exists && entryWithTTL != nil {
cache = entryWithTTL.cache
} else {
// Create new cache for this goroutine
cache = &lockFreeBufferCache{}
now := time.Now().Unix()
goroutineCacheMutex.Lock()
goroutineCacheWithTTL[gid] = &cacheEntry{
cache: cache,
lastAccess: now,
gid: gid,
}
goroutineCacheMutex.Unlock()
}
if cache != nil {
// Fill cache to optimal level based on hit rate
hitRate := float64(hitCount) / float64(totalRequests)
optimalCacheSize := int(float64(cacheSize) * hitRate)
if optimalCacheSize < 2 {
optimalCacheSize = 2
}
// Pre-allocate buffers for cache
for i := 0; i < optimalCacheSize && i < len(cache.buffers); i++ {
if cache.buffers[i] == nil {
// Get buffer from main pool
buf := p.Get()
if len(buf) > 0 {
cache.buffers[i] = &buf
}
}
}
}
}
// OptimizeCache performs periodic cache optimization based on usage patterns
func (p *AudioBufferPool) OptimizeCache() {
hitCount := atomic.LoadInt64(&p.hitCount)
missCount := atomic.LoadInt64(&p.missCount)
totalRequests := hitCount + missCount
if totalRequests < 100 {
return
}
hitRate := float64(hitCount) / float64(totalRequests)
// If hit rate is below target, trigger cache warmup
if hitRate < cacheHitRateTarget {
p.WarmupCache()
}
// Reset counters periodically to avoid overflow and get fresh metrics
if totalRequests > 10000 {
atomic.StoreInt64(&p.hitCount, hitCount/2)
atomic.StoreInt64(&p.missCount, missCount/2)
} }
} }

View File

@ -21,12 +21,12 @@ func getEnvInt(key string, defaultValue int) int {
// with fallback to default config values // with fallback to default config values
func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) { func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
// Read configuration from environment variables with config defaults // Read configuration from environment variables with config defaults
bitrate = getEnvInt("JETKVM_OPUS_BITRATE", Config.CGOOpusBitrate) bitrate = getEnvInt("JETKVM_OPUS_BITRATE", GetConfig().CGOOpusBitrate)
complexity = getEnvInt("JETKVM_OPUS_COMPLEXITY", Config.CGOOpusComplexity) complexity = getEnvInt("JETKVM_OPUS_COMPLEXITY", GetConfig().CGOOpusComplexity)
vbr = getEnvInt("JETKVM_OPUS_VBR", Config.CGOOpusVBR) vbr = getEnvInt("JETKVM_OPUS_VBR", GetConfig().CGOOpusVBR)
signalType = getEnvInt("JETKVM_OPUS_SIGNAL_TYPE", Config.CGOOpusSignalType) signalType = getEnvInt("JETKVM_OPUS_SIGNAL_TYPE", GetConfig().CGOOpusSignalType)
bandwidth = getEnvInt("JETKVM_OPUS_BANDWIDTH", Config.CGOOpusBandwidth) bandwidth = getEnvInt("JETKVM_OPUS_BANDWIDTH", GetConfig().CGOOpusBandwidth)
dtx = getEnvInt("JETKVM_OPUS_DTX", Config.CGOOpusDTX) dtx = getEnvInt("JETKVM_OPUS_DTX", GetConfig().CGOOpusDTX)
return bitrate, complexity, vbr, signalType, bandwidth, dtx return bitrate, complexity, vbr, signalType, bandwidth, dtx
} }
@ -34,7 +34,7 @@ func parseOpusConfig() (bitrate, complexity, vbr, signalType, bandwidth, dtx int
// applyOpusConfig applies OPUS configuration to the global config // applyOpusConfig applies OPUS configuration to the global config
// with optional logging for the specified component // with optional logging for the specified component
func applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int, component string, enableLogging bool) { func applyOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int, component string, enableLogging bool) {
config := Config config := GetConfig()
config.CGOOpusBitrate = bitrate config.CGOOpusBitrate = bitrate
config.CGOOpusComplexity = complexity config.CGOOpusComplexity = complexity
config.CGOOpusVBR = vbr config.CGOOpusVBR = vbr

View File

@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"reflect" "reflect"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/logging"
@ -119,7 +118,9 @@ func (r *AudioRelay) IsMuted() bool {
// GetStats returns relay statistics // GetStats returns relay statistics
func (r *AudioRelay) GetStats() (framesRelayed, framesDropped int64) { func (r *AudioRelay) GetStats() (framesRelayed, framesDropped int64) {
return atomic.LoadInt64(&r.framesRelayed), atomic.LoadInt64(&r.framesDropped) r.mutex.RLock()
defer r.mutex.RUnlock()
return r.framesRelayed, r.framesDropped
} }
// UpdateTrack updates the WebRTC audio track for the relay // UpdateTrack updates the WebRTC audio track for the relay
@ -131,43 +132,34 @@ func (r *AudioRelay) UpdateTrack(audioTrack AudioTrackWriter) {
func (r *AudioRelay) relayLoop() { func (r *AudioRelay) relayLoop() {
defer r.wg.Done() defer r.wg.Done()
r.logger.Debug().Msg("Audio relay loop started")
var maxConsecutiveErrors = Config.MaxConsecutiveErrors var maxConsecutiveErrors = GetConfig().MaxConsecutiveErrors
consecutiveErrors := 0 consecutiveErrors := 0
backoffDelay := time.Millisecond * 10
maxBackoff := time.Second * 5
for { for {
select { select {
case <-r.ctx.Done(): case <-r.ctx.Done():
r.logger.Debug().Msg("audio relay loop stopping")
return return
default: default:
frame, err := r.client.ReceiveFrame() frame, err := r.client.ReceiveFrame()
if err != nil { if err != nil {
consecutiveErrors++ consecutiveErrors++
r.logger.Error().Err(err).Int("consecutive_errors", consecutiveErrors).Msg("error reading frame from audio output server")
r.incrementDropped() r.incrementDropped()
// Exponential backoff for stability
if consecutiveErrors >= maxConsecutiveErrors { if consecutiveErrors >= maxConsecutiveErrors {
// Attempt reconnection r.logger.Error().Int("consecutive_errors", consecutiveErrors).Int("max_errors", maxConsecutiveErrors).Msg("too many consecutive read errors, stopping audio relay")
if r.attemptReconnection() {
consecutiveErrors = 0
backoffDelay = time.Millisecond * 10
continue
}
return return
} }
time.Sleep(GetConfig().ShortSleepDuration)
time.Sleep(backoffDelay)
if backoffDelay < maxBackoff {
backoffDelay *= 2
}
continue continue
} }
consecutiveErrors = 0 consecutiveErrors = 0
backoffDelay = time.Millisecond * 10
if err := r.forwardToWebRTC(frame); err != nil { if err := r.forwardToWebRTC(frame); err != nil {
r.logger.Warn().Err(err).Msg("failed to forward frame to webrtc")
r.incrementDropped() r.incrementDropped()
} else { } else {
r.incrementRelayed() r.incrementRelayed()
@ -226,24 +218,14 @@ func (r *AudioRelay) forwardToWebRTC(frame []byte) error {
// incrementRelayed atomically increments the relayed frames counter // incrementRelayed atomically increments the relayed frames counter
func (r *AudioRelay) incrementRelayed() { func (r *AudioRelay) incrementRelayed() {
atomic.AddInt64(&r.framesRelayed, 1) r.mutex.Lock()
r.framesRelayed++
r.mutex.Unlock()
} }
// incrementDropped atomically increments the dropped frames counter // incrementDropped atomically increments the dropped frames counter
func (r *AudioRelay) incrementDropped() { func (r *AudioRelay) incrementDropped() {
atomic.AddInt64(&r.framesDropped, 1) r.mutex.Lock()
} r.framesDropped++
r.mutex.Unlock()
// attemptReconnection tries to reconnect the audio client for stability
func (r *AudioRelay) attemptReconnection() bool {
if r.client == nil {
return false
}
// Disconnect and reconnect
r.client.Disconnect()
time.Sleep(time.Millisecond * 100)
err := r.client.Connect()
return err == nil
} }

View File

@ -224,7 +224,7 @@ func (aeb *AudioEventBroadcaster) sendToSubscriber(subscriber *AudioEventSubscri
return false return false
} }
ctx, cancel := context.WithTimeout(subscriber.ctx, time.Duration(Config.EventTimeoutSeconds)*time.Second) ctx, cancel := context.WithTimeout(subscriber.ctx, time.Duration(GetConfig().EventTimeoutSeconds)*time.Second)
defer cancel() defer cancel()
err := wsjson.Write(ctx, subscriber.conn, event) err := wsjson.Write(ctx, subscriber.conn, event)

View File

@ -98,16 +98,16 @@ type ZeroCopyFramePool struct {
// NewZeroCopyFramePool creates a new zero-copy frame pool // NewZeroCopyFramePool creates a new zero-copy frame pool
func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool { func NewZeroCopyFramePool(maxFrameSize int) *ZeroCopyFramePool {
// Pre-allocate frames for immediate availability // Pre-allocate frames for immediate availability
preallocSizeBytes := Config.ZeroCopyPreallocSizeBytes preallocSizeBytes := GetConfig().PreallocSize
maxPoolSize := Config.MaxPoolSize // Limit total pool size maxPoolSize := GetConfig().MaxPoolSize // Limit total pool size
// Calculate number of frames based on memory budget, not frame count // Calculate number of frames based on memory budget, not frame count
preallocFrameCount := preallocSizeBytes / maxFrameSize preallocFrameCount := preallocSizeBytes / maxFrameSize
if preallocFrameCount > maxPoolSize { if preallocFrameCount > maxPoolSize {
preallocFrameCount = maxPoolSize preallocFrameCount = maxPoolSize
} }
if preallocFrameCount < Config.ZeroCopyMinPreallocFrames { if preallocFrameCount < 1 {
preallocFrameCount = Config.ZeroCopyMinPreallocFrames preallocFrameCount = 1 // Always preallocate at least one frame
} }
preallocated := make([]*ZeroCopyAudioFrame, 0, preallocFrameCount) preallocated := make([]*ZeroCopyAudioFrame, 0, preallocFrameCount)
@ -147,7 +147,7 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
// If we've allocated too many frames, force pool reuse // If we've allocated too many frames, force pool reuse
frame := p.pool.Get().(*ZeroCopyAudioFrame) frame := p.pool.Get().(*ZeroCopyAudioFrame)
frame.mutex.Lock() frame.mutex.Lock()
atomic.StoreInt32(&frame.refCount, 1) frame.refCount = 1
frame.length = 0 frame.length = 0
frame.data = frame.data[:0] frame.data = frame.data[:0]
frame.mutex.Unlock() frame.mutex.Unlock()
@ -163,12 +163,11 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
p.mutex.Unlock() p.mutex.Unlock()
frame.mutex.Lock() frame.mutex.Lock()
atomic.StoreInt32(&frame.refCount, 1) frame.refCount = 1
frame.length = 0 frame.length = 0
frame.data = frame.data[:0] frame.data = frame.data[:0]
frame.mutex.Unlock() frame.mutex.Unlock()
atomic.AddInt64(&p.hitCount, 1)
return frame return frame
} }
p.mutex.Unlock() p.mutex.Unlock()
@ -176,7 +175,7 @@ func (p *ZeroCopyFramePool) Get() *ZeroCopyAudioFrame {
// Try sync.Pool next and track allocation // Try sync.Pool next and track allocation
frame := p.pool.Get().(*ZeroCopyAudioFrame) frame := p.pool.Get().(*ZeroCopyAudioFrame)
frame.mutex.Lock() frame.mutex.Lock()
atomic.StoreInt32(&frame.refCount, 1) frame.refCount = 1
frame.length = 0 frame.length = 0
frame.data = frame.data[:0] frame.data = frame.data[:0]
frame.mutex.Unlock() frame.mutex.Unlock()
@ -192,34 +191,43 @@ func (p *ZeroCopyFramePool) Put(frame *ZeroCopyAudioFrame) {
return return
} }
// Reset frame state for reuse
frame.mutex.Lock() frame.mutex.Lock()
atomic.StoreInt32(&frame.refCount, 0) frame.refCount--
frame.length = 0 if frame.refCount <= 0 {
frame.data = frame.data[:0] frame.refCount = 0
frame.mutex.Unlock() frame.length = 0
frame.data = frame.data[:0]
frame.mutex.Unlock()
// First try to return to pre-allocated pool for fastest reuse // First try to return to pre-allocated pool for fastest reuse
p.mutex.Lock() p.mutex.Lock()
if len(p.preallocated) < p.preallocSize { if len(p.preallocated) < p.preallocSize {
p.preallocated = append(p.preallocated, frame) p.preallocated = append(p.preallocated, frame)
p.mutex.Unlock()
return
}
p.mutex.Unlock() p.mutex.Unlock()
return
}
p.mutex.Unlock()
// Check pool size limit to prevent excessive memory usage // Check pool size limit to prevent excessive memory usage
p.mutex.RLock() p.mutex.RLock()
currentCount := atomic.LoadInt64(&p.counter) currentCount := atomic.LoadInt64(&p.counter)
p.mutex.RUnlock() p.mutex.RUnlock()
if currentCount >= int64(p.maxPoolSize) { if currentCount >= int64(p.maxPoolSize) {
return // Pool is full, let GC handle this frame return // Pool is full, let GC handle this frame
}
// Return to sync.Pool
p.pool.Put(frame)
// Metrics collection removed
if false {
atomic.AddInt64(&p.counter, 1)
}
} else {
frame.mutex.Unlock()
} }
// Return to sync.Pool // Metrics recording removed - granular metrics collector was unused
p.pool.Put(frame)
atomic.AddInt64(&p.counter, 1)
} }
// Data returns the frame data as a slice (zero-copy view) // Data returns the frame data as a slice (zero-copy view)
@ -263,28 +271,18 @@ func (f *ZeroCopyAudioFrame) SetDataDirect(data []byte) {
f.pooled = false // Direct assignment means we can't pool this frame f.pooled = false // Direct assignment means we can't pool this frame
} }
// AddRef increments the reference count atomically // AddRef increments the reference count for shared access
func (f *ZeroCopyAudioFrame) AddRef() { func (f *ZeroCopyAudioFrame) AddRef() {
atomic.AddInt32(&f.refCount, 1) f.mutex.Lock()
f.refCount++
f.mutex.Unlock()
} }
// Release decrements the reference count atomically // Release decrements the reference count
// Returns true if this was the final reference func (f *ZeroCopyAudioFrame) Release() {
func (f *ZeroCopyAudioFrame) Release() bool { f.mutex.Lock()
newCount := atomic.AddInt32(&f.refCount, -1) f.refCount--
if newCount == 0 { f.mutex.Unlock()
// Final reference released, return to pool if pooled
if f.pooled {
globalZeroCopyPool.Put(f)
}
return true
}
return false
}
// RefCount returns the current reference count atomically
func (f *ZeroCopyAudioFrame) RefCount() int32 {
return atomic.LoadInt32(&f.refCount)
} }
// Length returns the current data length // Length returns the current data length
@ -327,7 +325,7 @@ func (p *ZeroCopyFramePool) GetZeroCopyPoolStats() ZeroCopyFramePoolStats {
var hitRate float64 var hitRate float64
if totalRequests > 0 { if totalRequests > 0 {
hitRate = float64(hitCount) / float64(totalRequests) * Config.PercentageMultiplier hitRate = float64(hitCount) / float64(totalRequests) * GetConfig().PercentageMultiplier
} }
return ZeroCopyFramePoolStats{ return ZeroCopyFramePoolStats{

50
main.go
View File

@ -35,6 +35,12 @@ func startAudioSubprocess() error {
// Initialize validation cache for optimal performance // Initialize validation cache for optimal performance
audio.InitValidationCache() audio.InitValidationCache()
// Start adaptive buffer management for optimal performance
audio.StartAdaptiveBuffering()
// Start goroutine monitoring to detect and prevent leaks
audio.StartGoroutineMonitoring()
// Enable batch audio processing to reduce CGO call overhead // Enable batch audio processing to reduce CGO call overhead
if err := audio.EnableBatchAudioProcessing(); err != nil { if err := audio.EnableBatchAudioProcessing(); err != nil {
logger.Warn().Err(err).Msg("failed to enable batch audio processing") logger.Warn().Err(err).Msg("failed to enable batch audio processing")
@ -52,7 +58,7 @@ func startAudioSubprocess() error {
audio.SetAudioInputSupervisor(audioInputSupervisor) audio.SetAudioInputSupervisor(audioInputSupervisor)
// Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106) // Set default OPUS configuration for audio input supervisor (low quality for single-core RV1106)
config := audio.Config config := audio.GetConfig()
audioInputSupervisor.SetOpusConfig( audioInputSupervisor.SetOpusConfig(
config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps config.AudioQualityLowInputBitrate*1000, // Convert kbps to bps
config.AudioQualityLowOpusComplexity, config.AudioQualityLowOpusComplexity,
@ -71,32 +77,19 @@ func startAudioSubprocess() error {
func(pid int) { func(pid int) {
logger.Info().Int("pid", pid).Msg("audio server process started") logger.Info().Int("pid", pid).Msg("audio server process started")
// Wait for audio output server to be fully ready before starting relay // Start audio relay system for main process
// This prevents "no client connected" errors during quality changes // If there's an active WebRTC session, use its audio track
go func() { var audioTrack *webrtc.TrackLocalStaticSample
// Give the audio output server time to initialize and start listening if currentSession != nil && currentSession.AudioTrack != nil {
// Increased delay to reduce frame drops during connection establishment audioTrack = currentSession.AudioTrack
time.Sleep(1 * time.Second) logger.Info().Msg("restarting audio relay with existing WebRTC audio track")
} else {
logger.Info().Msg("starting audio relay without WebRTC track (will be updated when session is created)")
}
// Start audio relay system for main process if err := audio.StartAudioRelay(audioTrack); err != nil {
// If there's an active WebRTC session, use its audio track logger.Error().Err(err).Msg("failed to start audio relay")
var audioTrack *webrtc.TrackLocalStaticSample }
if currentSession != nil && currentSession.AudioTrack != nil {
audioTrack = currentSession.AudioTrack
logger.Info().Msg("restarting audio relay with existing WebRTC audio track")
} else {
logger.Info().Msg("starting audio relay without WebRTC track (will be updated when session is created)")
}
if err := audio.StartAudioRelay(audioTrack); err != nil {
logger.Error().Err(err).Msg("failed to start audio relay")
// Retry once after additional delay if initial attempt fails
time.Sleep(1 * time.Second)
if err := audio.StartAudioRelay(audioTrack); err != nil {
logger.Error().Err(err).Msg("failed to start audio relay after retry")
}
}
}()
}, },
// onProcessExit // onProcessExit
func(pid int, exitCode int, crashed bool) { func(pid int, exitCode int, crashed bool) {
@ -108,7 +101,10 @@ func startAudioSubprocess() error {
// Stop audio relay when process exits // Stop audio relay when process exits
audio.StopAudioRelay() audio.StopAudioRelay()
// Stop adaptive buffering
audio.StopAdaptiveBuffering()
// Stop goroutine monitoring
audio.StopGoroutineMonitoring()
// Disable batch audio processing // Disable batch audio processing
audio.DisableBatchAudioProcessing() audio.DisableBatchAudioProcessing()
}, },

View File

@ -1,25 +1,25 @@
import Card from "@components/Card"; import Card from "@components/Card";
export interface CustomTooltipProps { export interface CustomTooltipProps {
payload: { payload: { date: number; metric: number }; unit: string }[]; payload: { payload: { date: number; stat: number }; unit: string }[];
} }
export default function CustomTooltip({ payload }: CustomTooltipProps) { export default function CustomTooltip({ payload }: CustomTooltipProps) {
if (payload?.length) { if (payload?.length) {
const toolTipData = payload[0]; const toolTipData = payload[0];
const { date, metric } = toolTipData.payload; const { date, stat } = toolTipData.payload;
return ( return (
<Card> <Card>
<div className="px-2 py-1.5 text-black dark:text-white"> <div className="p-2 text-black dark:text-white">
<div className="text-[13px] font-semibold"> <div className="font-semibold">
{new Date(date * 1000).toLocaleTimeString()} {new Date(date * 1000).toLocaleTimeString()}
</div> </div>
<div className="space-y-1"> <div className="space-y-1">
<div className="flex items-center gap-x-1"> <div className="flex items-center gap-x-1">
<div className="h-[2px] w-2 bg-blue-700" /> <div className="h-[2px] w-2 bg-blue-700" />
<span className="text-[13px]"> <span >
{metric} {toolTipData?.unit} {stat} {toolTipData?.unit}
</span> </span>
</div> </div>
</div> </div>

View File

@ -103,7 +103,7 @@ export default function DashboardNavbar({
<hr className="h-[20px] w-px self-center border-none bg-slate-800/20 dark:bg-slate-300/20" /> <hr className="h-[20px] w-px self-center border-none bg-slate-800/20 dark:bg-slate-300/20" />
<div className="relative inline-block text-left"> <div className="relative inline-block text-left">
<Menu> <Menu>
<MenuButton as="div" className="h-full"> <MenuButton className="h-full">
<Button className="flex h-full items-center gap-x-3 rounded-md border border-slate-800/20 bg-white px-2 py-1.5 dark:border-slate-600 dark:bg-slate-800 dark:text-white"> <Button className="flex h-full items-center gap-x-3 rounded-md border border-slate-800/20 bg-white px-2 py-1.5 dark:border-slate-600 dark:bg-slate-800 dark:text-white">
{picture ? ( {picture ? (
<img <img

View File

@ -100,12 +100,15 @@ export default function KvmCard({
)} )}
</div> </div>
<Menu as="div" className="relative inline-block text-left"> <Menu as="div" className="relative inline-block text-left">
<MenuButton <div>
as={Button} <MenuButton
theme="light" as={Button}
TrailingIcon={LuEllipsisVertical} theme="light"
size="MD" TrailingIcon={LuEllipsisVertical}
></MenuButton> size="MD"
></MenuButton>
</div>
<MenuItems <MenuItems
transition transition
className="data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-leave:duration-75 data-enter:ease-out data-leave:ease-in" className="data-closed:scale-95 data-closed:transform data-closed:opacity-0 data-enter:duration-100 data-leave:duration-75 data-enter:ease-out data-leave:ease-in"

View File

@ -1,180 +0,0 @@
/* eslint-disable react-refresh/only-export-components */
import { ComponentProps } from "react";
import { cva, cx } from "cva";
import { someIterable } from "../utils";
import { GridCard } from "./Card";
import MetricsChart from "./MetricsChart";
interface ChartPoint {
date: number;
metric: number | null;
}
interface MetricProps<T, K extends keyof T> {
title: string;
description: string;
stream?: Map<number, T>;
metric?: K;
data?: ChartPoint[];
gate?: Map<number, unknown>;
supported?: boolean;
map?: (p: { date: number; metric: number | null }) => ChartPoint;
domain?: [number, number];
unit: string;
heightClassName?: string;
referenceValue?: number;
badge?: ComponentProps<typeof MetricHeader>["badge"];
badgeTheme?: ComponentProps<typeof MetricHeader>["badgeTheme"];
}
/**
* Creates a chart array from a metrics map and a metric name.
*
* @param metrics - Expected to be ordered from oldest to newest.
* @param metricName - Name of the metric to create a chart array for.
*/
export function createChartArray<T, K extends keyof T>(
metrics: Map<number, T>,
metricName: K,
) {
const result: { date: number; metric: number | null }[] = [];
const iter = metrics.entries();
let next = iter.next() as IteratorResult<[number, T]>;
const nowSeconds = Math.floor(Date.now() / 1000);
// We want 120 data points, in the chart.
const firstDate = Math.min(next.value?.[0] ?? nowSeconds, nowSeconds - 120);
for (let t = firstDate; t < nowSeconds; t++) {
while (!next.done && next.value[0] < t) next = iter.next();
const has = !next.done && next.value[0] === t;
let metric = null;
if (has) metric = next.value[1][metricName] as number;
result.push({ date: t, metric });
if (has) next = iter.next();
}
return result;
}
function computeReferenceValue(points: ChartPoint[]): number | undefined {
const values = points
.filter(p => p.metric != null && Number.isFinite(p.metric))
.map(p => Number(p.metric));
if (values.length === 0) return undefined;
const sum = values.reduce((acc, v) => acc + v, 0);
const mean = sum / values.length;
return Math.round(mean);
}
const theme = {
light:
"bg-white text-black border border-slate-800/20 dark:border dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300",
danger: "bg-red-500 dark:border-red-700 dark:bg-red-800 dark:text-red-50",
primary: "bg-blue-500 dark:border-blue-700 dark:bg-blue-800 dark:text-blue-50",
};
interface SettingsItemProps {
readonly title: string;
readonly description: string | React.ReactNode;
readonly badge?: string;
readonly className?: string;
readonly children?: React.ReactNode;
readonly badgeTheme?: keyof typeof theme;
}
export function MetricHeader(props: SettingsItemProps) {
const { title, description, badge } = props;
const badgeVariants = cva({ variants: { theme: theme } });
return (
<div className="space-y-0.5">
<div className="flex items-center gap-x-2">
<div className="flex w-full items-center justify-between text-base font-semibold text-black dark:text-white">
{title}
{badge && (
<span
className={cx(
"ml-2 rounded-sm px-2 py-1 font-mono text-[10px] leading-none font-medium",
badgeVariants({ theme: props.badgeTheme ?? "light" }),
)}
>
{badge}
</span>
)}
</div>
</div>
<div className="text-sm text-slate-700 dark:text-slate-300">{description}</div>
</div>
);
}
export function Metric<T, K extends keyof T>({
title,
description,
stream,
metric,
data,
gate,
supported,
map,
domain = [0, 600],
unit = "",
heightClassName = "h-[127px]",
badge,
badgeTheme,
}: MetricProps<T, K>) {
const ready = gate ? gate.size > 0 : stream ? stream.size > 0 : true;
const supportedFinal =
supported ??
(stream && metric ? someIterable(stream, ([, s]) => s[metric] !== undefined) : true);
// Either we let the consumer provide their own chartArray, or we create one from the stream and metric.
const raw = data ?? ((stream && metric && createChartArray(stream, metric)) || []);
// If the consumer provides a map function, we apply it to the raw data.
const dataFinal: ChartPoint[] = map ? raw.map(map) : raw;
// Compute the average value of the metric.
const referenceValue = computeReferenceValue(dataFinal);
return (
<div className="space-y-2">
<MetricHeader
title={title}
description={description}
badge={badge}
badgeTheme={badgeTheme}
/>
<GridCard>
<div
className={`flex ${heightClassName} w-full items-center justify-center text-sm text-slate-500`}
>
{!ready ? (
<div className="flex flex-col items-center space-y-1">
<p className="text-slate-700">Waiting for data...</p>
</div>
) : supportedFinal ? (
<MetricsChart
data={dataFinal}
domain={domain}
unit={unit}
referenceValue={referenceValue}
/>
) : (
<div className="flex flex-col items-center space-y-1">
<p className="text-black">Metric not supported</p>
</div>
)}
</div>
</GridCard>
</div>
);
}

View File

@ -12,13 +12,13 @@ import {
import CustomTooltip, { CustomTooltipProps } from "@components/CustomTooltip"; import CustomTooltip, { CustomTooltipProps } from "@components/CustomTooltip";
export default function MetricsChart({ export default function StatChart({
data, data,
domain, domain,
unit, unit,
referenceValue, referenceValue,
}: { }: {
data: { date: number; metric: number | null | undefined }[]; data: { date: number; stat: number | null | undefined }[];
domain?: [string | number, string | number]; domain?: [string | number, string | number];
unit?: string; unit?: string;
referenceValue?: number; referenceValue?: number;
@ -33,7 +33,7 @@ export default function MetricsChart({
strokeLinecap="butt" strokeLinecap="butt"
stroke="rgba(30, 41, 59, 0.1)" stroke="rgba(30, 41, 59, 0.1)"
/> />
{referenceValue !== undefined && ( {referenceValue && (
<ReferenceLine <ReferenceLine
y={referenceValue} y={referenceValue}
strokeDasharray="3 3" strokeDasharray="3 3"
@ -64,7 +64,7 @@ export default function MetricsChart({
.map(x => x.date)} .map(x => x.date)}
/> />
<YAxis <YAxis
dataKey="metric" dataKey="stat"
axisLine={false} axisLine={false}
orientation="right" orientation="right"
tick={{ tick={{
@ -73,7 +73,6 @@ export default function MetricsChart({
fill: "rgba(107, 114, 128, 1)", fill: "rgba(107, 114, 128, 1)",
}} }}
padding={{ top: 0, bottom: 0 }} padding={{ top: 0, bottom: 0 }}
allowDecimals
tickLine={false} tickLine={false}
unit={unit} unit={unit}
domain={domain || ["auto", "auto"]} domain={domain || ["auto", "auto"]}
@ -88,7 +87,7 @@ export default function MetricsChart({
<Line <Line
type="monotone" type="monotone"
isAnimationActive={false} isAnimationActive={false}
dataKey="metric" dataKey="stat"
stroke="rgb(29 78 216)" stroke="rgb(29 78 216)"
strokeLinecap="round" strokeLinecap="round"
strokeWidth={2} strokeWidth={2}

View File

@ -345,13 +345,8 @@ export default function WebRTCVideo({ microphone }: WebRTCVideoProps) {
peerConnection.addEventListener( peerConnection.addEventListener(
"track", "track",
(_e: RTCTrackEvent) => { (e: RTCTrackEvent) => {
// The combined MediaStream is now managed in the main component addStreamToVideoElm(e.streams[0]);
// We'll use the mediaStream from the store instead of individual track streams
const { mediaStream } = useRTCStore.getState();
if (mediaStream) {
addStreamToVideoElm(mediaStream);
}
}, },
{ signal }, { signal },
); );

View File

@ -1,40 +1,74 @@
import { useInterval } from "usehooks-ts"; import { useInterval } from "usehooks-ts";
import SidebarHeader from "@/components/SidebarHeader"; import SidebarHeader from "@/components/SidebarHeader";
import { GridCard } from "@/components/Card";
import { useRTCStore, useUiStore } from "@/hooks/stores"; import { useRTCStore, useUiStore } from "@/hooks/stores";
import { someIterable } from "@/utils"; import StatChart from "@/components/StatChart";
import { createChartArray, Metric } from "../Metric"; function createChartArray<T, K extends keyof T>(
import { SettingsSectionHeader } from "../SettingsSectionHeader"; stream: Map<number, T>,
metric: K,
): { date: number; stat: T[K] | null }[] {
const stat = Array.from(stream).map(([key, stats]) => {
return { date: key, stat: stats[metric] };
});
// Sort the dates to ensure they are in chronological order
const sortedStat = stat.map(x => x.date).sort((a, b) => a - b);
// Determine the earliest statistic date
const earliestStat = sortedStat[0];
// Current time in seconds since the Unix epoch
const now = Math.floor(Date.now() / 1000);
// Determine the starting point for the chart data
const firstChartDate = earliestStat ? Math.min(earliestStat, now - 120) : now - 120;
// Generate the chart array for the range between 'firstChartDate' and 'now'
return Array.from({ length: now - firstChartDate }, (_, i) => {
const currentDate = firstChartDate + i;
return {
date: currentDate,
// Find the statistic for 'currentDate', or use the last known statistic if none exists for that date
stat: stat.find(x => x.date === currentDate)?.stat ?? null,
};
});
}
export default function ConnectionStatsSidebar() { export default function ConnectionStatsSidebar() {
const { sidebarView, setSidebarView } = useUiStore(); const { sidebarView, setSidebarView } = useUiStore();
const { const {
mediaStream, mediaStream,
peerConnection, peerConnection,
inboundRtpStats: inboundVideoRtpStats, inboundRtpStats,
appendInboundRtpStats: appendInboundVideoRtpStats, appendInboundRtpStats,
candidatePairStats: iceCandidatePairStats, candidatePairStats,
appendCandidatePairStats, appendCandidatePairStats,
appendLocalCandidateStats, appendLocalCandidateStats,
appendRemoteCandidateStats, appendRemoteCandidateStats,
appendDiskDataChannelStats, appendDiskDataChannelStats,
} = useRTCStore(); } = useRTCStore();
function isMetricSupported<T, K extends keyof T>(
stream: Map<number, T>,
metric: K,
): boolean {
return Array.from(stream).some(([, stat]) => stat[metric] !== undefined);
}
useInterval(function collectWebRTCStats() { useInterval(function collectWebRTCStats() {
(async () => { (async () => {
if (!mediaStream) return; if (!mediaStream) return;
const videoTrack = mediaStream.getVideoTracks()[0]; const videoTrack = mediaStream.getVideoTracks()[0];
if (!videoTrack) return; if (!videoTrack) return;
const stats = await peerConnection?.getStats(); const stats = await peerConnection?.getStats();
let successfulLocalCandidateId: string | null = null; let successfulLocalCandidateId: string | null = null;
let successfulRemoteCandidateId: string | null = null; let successfulRemoteCandidateId: string | null = null;
stats?.forEach(report => { stats?.forEach(report => {
if (report.type === "inbound-rtp" && report.kind === "video") { if (report.type === "inbound-rtp") {
appendInboundVideoRtpStats(report); appendInboundRtpStats(report);
} else if (report.type === "candidate-pair" && report.nominated) { } else if (report.type === "candidate-pair" && report.nominated) {
if (report.state === "succeeded") { if (report.state === "succeeded") {
successfulLocalCandidateId = report.localCandidateId; successfulLocalCandidateId = report.localCandidateId;
@ -57,133 +91,144 @@ export default function ConnectionStatsSidebar() {
})(); })();
}, 500); }, 500);
const jitterBufferDelay = createChartArray(inboundVideoRtpStats, "jitterBufferDelay");
const jitterBufferEmittedCount = createChartArray(
inboundVideoRtpStats,
"jitterBufferEmittedCount",
);
const jitterBufferAvgDelayData = jitterBufferDelay.map((d, idx) => {
if (idx === 0) return { date: d.date, metric: null };
const prevDelay = jitterBufferDelay[idx - 1]?.metric as number | null | undefined;
const currDelay = d.metric as number | null | undefined;
const prevCountEmitted =
(jitterBufferEmittedCount[idx - 1]?.metric as number | null | undefined) ?? null;
const currCountEmitted =
(jitterBufferEmittedCount[idx]?.metric as number | null | undefined) ?? null;
if (
prevDelay == null ||
currDelay == null ||
prevCountEmitted == null ||
currCountEmitted == null
) {
return { date: d.date, metric: null };
}
const deltaDelay = currDelay - prevDelay;
const deltaEmitted = currCountEmitted - prevCountEmitted;
// Guard counter resets or no emitted frames
if (deltaDelay < 0 || deltaEmitted <= 0) {
return { date: d.date, metric: null };
}
const valueMs = Math.round((deltaDelay / deltaEmitted) * 1000);
return { date: d.date, metric: valueMs };
});
return ( return (
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs"> <div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} /> <SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900"> <div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
<div className="space-y-4"> <div className="space-y-4">
{/*
The entire sidebar component is always rendered, with a display none when not visible
The charts below, need a height and width, otherwise they throw. So simply don't render them unless the thing is visible
*/}
{sidebarView === "connection-stats" && ( {sidebarView === "connection-stats" && (
<div className="space-y-8"> <div className="space-y-4">
{/* Connection Group */} <div className="space-y-2">
<div className="space-y-3"> <div>
<SettingsSectionHeader <h2 className="text-lg font-semibold text-black dark:text-white">
title="Connection" Packets Lost
description="The connection between the client and the JetKVM." </h2>
/> <p className="text-sm text-slate-700 dark:text-slate-300">
<Metric Number of data packets lost during transmission.
title="Round-Trip Time" </p>
description="Round-trip time for the active ICE candidate pair between peers." </div>
stream={iceCandidatePairStats} <GridCard>
metric="currentRoundTripTime" <div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
map={x => ({ {inboundRtpStats.size === 0 ? (
date: x.date, <div className="flex flex-col items-center space-y-1">
metric: x.metric != null ? Math.round(x.metric * 1000) : null, <p className="text-slate-700">Waiting for data...</p>
})} </div>
domain={[0, 600]} ) : isMetricSupported(inboundRtpStats, "packetsLost") ? (
unit=" ms" <StatChart
/> data={createChartArray(inboundRtpStats, "packetsLost")}
domain={[0, 100]}
unit=" packets"
/>
) : (
<div className="flex flex-col items-center space-y-1">
<p className="text-black">Metric not supported</p>
</div>
)}
</div>
</GridCard>
</div> </div>
<div className="space-y-2">
{/* Video Group */} <div>
<div className="space-y-3"> <h2 className="text-lg font-semibold text-black dark:text-white">
<SettingsSectionHeader Round-Trip Time
title="Video" </h2>
description="The video stream from the JetKVM to the client." <p className="text-sm text-slate-700 dark:text-slate-300">
/> Time taken for data to travel from source to destination and back
</p>
{/* RTP Jitter */} </div>
<Metric <GridCard>
title="Network Stability" <div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
badge="Jitter" {inboundRtpStats.size === 0 ? (
badgeTheme="light" <div className="flex flex-col items-center space-y-1">
description="How steady the flow of inbound video packets is across the network." <p className="text-slate-700">Waiting for data...</p>
stream={inboundVideoRtpStats} </div>
metric="jitter" ) : isMetricSupported(candidatePairStats, "currentRoundTripTime") ? (
map={x => ({ <StatChart
date: x.date, data={createChartArray(
metric: x.metric != null ? Math.round(x.metric * 1000) : null, candidatePairStats,
})} "currentRoundTripTime",
domain={[0, 10]} ).map(x => {
unit=" ms" return {
/> date: x.date,
stat: x.stat ? Math.round(x.stat * 1000) : null,
{/* Playback Delay */} };
<Metric })}
title="Playback Delay" domain={[0, 600]}
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly." unit=" ms"
badge="Jitter Buffer Avg. Delay" />
badgeTheme="light" ) : (
data={jitterBufferAvgDelayData} <div className="flex flex-col items-center space-y-1">
gate={inboundVideoRtpStats} <p className="text-black">Metric not supported</p>
supported={ </div>
someIterable( )}
inboundVideoRtpStats, </div>
([, x]) => x.jitterBufferDelay != null, </GridCard>
) && </div>
someIterable( <div className="space-y-2">
inboundVideoRtpStats, <div>
([, x]) => x.jitterBufferEmittedCount != null, <h2 className="text-lg font-semibold text-black dark:text-white">
) Jitter
} </h2>
domain={[0, 30]} <p className="text-sm text-slate-700 dark:text-slate-300">
unit=" ms" Variation in packet delay, affecting video smoothness.{" "}
/> </p>
</div>
{/* Packets Lost */} <GridCard>
<Metric <div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
title="Packets Lost" {inboundRtpStats.size === 0 ? (
description="Count of lost inbound video RTP packets." <div className="flex flex-col items-center space-y-1">
stream={inboundVideoRtpStats} <p className="text-slate-700">Waiting for data...</p>
metric="packetsLost" </div>
domain={[0, 100]} ) : (
unit=" packets" <StatChart
/> data={createChartArray(inboundRtpStats, "jitter").map(x => {
return {
{/* Frames Per Second */} date: x.date,
<Metric stat: x.stat ? Math.round(x.stat * 1000) : null,
title="Frames per second" };
description="Number of inbound video frames displayed per second." })}
stream={inboundVideoRtpStats} domain={[0, 300]}
metric="framesPerSecond" unit=" ms"
domain={[0, 80]} />
unit=" fps" )}
/> </div>
</GridCard>
</div>
<div className="space-y-2">
<div>
<h2 className="text-lg font-semibold text-black dark:text-white">
Frames per second
</h2>
<p className="text-sm text-slate-700 dark:text-slate-300">
Number of video frames displayed per second.
</p>
</div>
<GridCard>
<div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
{inboundRtpStats.size === 0 ? (
<div className="flex flex-col items-center space-y-1">
<p className="text-slate-700">Waiting for data...</p>
</div>
) : (
<StatChart
data={createChartArray(inboundRtpStats, "framesPerSecond").map(
x => {
return {
date: x.date,
stat: x.stat ? x.stat : null,
};
},
)}
domain={[0, 80]}
unit=" fps"
/>
)}
</div>
</GridCard>
</div> </div>
</div> </div>
)} )}

View File

@ -355,10 +355,6 @@ export interface SettingsState {
setVideoBrightness: (value: number) => void; setVideoBrightness: (value: number) => void;
videoContrast: number; videoContrast: number;
setVideoContrast: (value: number) => void; setVideoContrast: (value: number) => void;
// Microphone persistence settings
microphoneWasEnabled: boolean;
setMicrophoneWasEnabled: (enabled: boolean) => void;
} }
export const useSettingsStore = create( export const useSettingsStore = create(
@ -404,10 +400,6 @@ export const useSettingsStore = create(
setVideoBrightness: (value: number) => set({ videoBrightness: value }), setVideoBrightness: (value: number) => set({ videoBrightness: value }),
videoContrast: 1.0, videoContrast: 1.0,
setVideoContrast: (value: number) => set({ videoContrast: value }), setVideoContrast: (value: number) => set({ videoContrast: value }),
// Microphone persistence settings
microphoneWasEnabled: false,
setMicrophoneWasEnabled: (enabled: boolean) => set({ microphoneWasEnabled: enabled }),
}), }),
{ {
name: "settings", name: "settings",

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import { useRTCStore, useSettingsStore } from "@/hooks/stores"; import { useRTCStore } from "@/hooks/stores";
import api from "@/api"; import api from "@/api";
import { devLog, devInfo, devWarn, devError, devOnly } from "@/utils/debug"; import { devLog, devInfo, devWarn, devError, devOnly } from "@/utils/debug";
import { AUDIO_CONFIG } from "@/config/constants"; import { AUDIO_CONFIG } from "@/config/constants";
@ -23,8 +23,6 @@ export function useMicrophone() {
setMicrophoneMuted, setMicrophoneMuted,
} = useRTCStore(); } = useRTCStore();
const { microphoneWasEnabled, setMicrophoneWasEnabled } = useSettingsStore();
const microphoneStreamRef = useRef<MediaStream | null>(null); const microphoneStreamRef = useRef<MediaStream | null>(null);
// Loading states // Loading states
@ -63,7 +61,7 @@ export function useMicrophone() {
// Cleaning up microphone stream // Cleaning up microphone stream
if (microphoneStreamRef.current) { if (microphoneStreamRef.current) {
microphoneStreamRef.current.getTracks().forEach((track: MediaStreamTrack) => { microphoneStreamRef.current.getTracks().forEach(track => {
track.stop(); track.stop();
}); });
microphoneStreamRef.current = null; microphoneStreamRef.current = null;
@ -195,7 +193,7 @@ export function useMicrophone() {
// Find the audio transceiver (should already exist with sendrecv direction) // Find the audio transceiver (should already exist with sendrecv direction)
const transceivers = peerConnection.getTransceivers(); const transceivers = peerConnection.getTransceivers();
devLog("Available transceivers:", transceivers.map((t: RTCRtpTransceiver) => ({ devLog("Available transceivers:", transceivers.map(t => ({
direction: t.direction, direction: t.direction,
mid: t.mid, mid: t.mid,
senderTrack: t.sender.track?.kind, senderTrack: t.sender.track?.kind,
@ -203,7 +201,7 @@ export function useMicrophone() {
}))); })));
// Look for an audio transceiver that can send (has sendrecv or sendonly direction) // Look for an audio transceiver that can send (has sendrecv or sendonly direction)
const audioTransceiver = transceivers.find((transceiver: RTCRtpTransceiver) => { const audioTransceiver = transceivers.find(transceiver => {
// Check if this transceiver is for audio and can send // Check if this transceiver is for audio and can send
const canSend = transceiver.direction === 'sendrecv' || transceiver.direction === 'sendonly'; const canSend = transceiver.direction === 'sendrecv' || transceiver.direction === 'sendonly';
@ -391,9 +389,6 @@ export function useMicrophone() {
setMicrophoneActive(true); setMicrophoneActive(true);
setMicrophoneMuted(false); setMicrophoneMuted(false);
// Save microphone enabled state for auto-restore on page reload
setMicrophoneWasEnabled(true);
devLog("Microphone state set to active. Verifying state:", { devLog("Microphone state set to active. Verifying state:", {
streamInRef: !!microphoneStreamRef.current, streamInRef: !!microphoneStreamRef.current,
streamInStore: !!microphoneStream, streamInStore: !!microphoneStream,
@ -452,7 +447,7 @@ export function useMicrophone() {
setIsStarting(false); setIsStarting(false);
return { success: false, error: micError }; return { success: false, error: micError };
} }
}, [peerConnection, setMicrophoneStream, setMicrophoneSender, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, stopMicrophoneStream, isMicrophoneActive, isMicrophoneMuted, microphoneStream, isStarting, isStopping, isToggling]); }, [peerConnection, setMicrophoneStream, setMicrophoneSender, setMicrophoneActive, setMicrophoneMuted, stopMicrophoneStream, isMicrophoneActive, isMicrophoneMuted, microphoneStream, isStarting, isStopping, isToggling]);
@ -481,9 +476,6 @@ export function useMicrophone() {
setMicrophoneActive(false); setMicrophoneActive(false);
setMicrophoneMuted(false); setMicrophoneMuted(false);
// Save microphone disabled state for persistence
setMicrophoneWasEnabled(false);
// Sync state after stopping to ensure consistency (with longer delay) // Sync state after stopping to ensure consistency (with longer delay)
setTimeout(() => syncMicrophoneState(), 500); setTimeout(() => syncMicrophoneState(), 500);
@ -500,7 +492,7 @@ export function useMicrophone() {
} }
}; };
} }
}, [stopMicrophoneStream, syncMicrophoneState, setMicrophoneActive, setMicrophoneMuted, setMicrophoneWasEnabled, isStarting, isStopping, isToggling]); }, [stopMicrophoneStream, syncMicrophoneState, setMicrophoneActive, setMicrophoneMuted, isStarting, isStopping, isToggling]);
// Toggle microphone mute // Toggle microphone mute
const toggleMicrophoneMute = useCallback(async (): Promise<{ success: boolean; error?: MicrophoneError }> => { const toggleMicrophoneMute = useCallback(async (): Promise<{ success: boolean; error?: MicrophoneError }> => {
@ -568,7 +560,7 @@ export function useMicrophone() {
const newMutedState = !isMicrophoneMuted; const newMutedState = !isMicrophoneMuted;
// Mute/unmute the audio track // Mute/unmute the audio track
audioTracks.forEach((track: MediaStreamTrack) => { audioTracks.forEach(track => {
track.enabled = !newMutedState; track.enabled = !newMutedState;
devLog(`Audio track ${track.id} enabled: ${track.enabled}`); devLog(`Audio track ${track.id} enabled: ${track.enabled}`);
}); });
@ -615,30 +607,10 @@ export function useMicrophone() {
// Sync state on mount and auto-restore microphone if it was enabled before page reload // Sync state on mount
useEffect(() => { useEffect(() => {
const autoRestoreMicrophone = async () => { syncMicrophoneState();
// First sync the current state }, [syncMicrophoneState]);
await syncMicrophoneState();
// If microphone was enabled before page reload and is not currently active, restore it
if (microphoneWasEnabled && !isMicrophoneActive && peerConnection) {
devLog("Auto-restoring microphone after page reload");
try {
const result = await startMicrophone();
if (result.success) {
devInfo("Microphone auto-restored successfully after page reload");
} else {
devWarn("Failed to auto-restore microphone:", result.error);
}
} catch (error) {
devWarn("Error during microphone auto-restoration:", error);
}
}
};
autoRestoreMicrophone();
}, [syncMicrophoneState, microphoneWasEnabled, isMicrophoneActive, peerConnection, startMicrophone]);
// Cleanup on unmount - use ref to avoid dependency on stopMicrophoneStream // Cleanup on unmount - use ref to avoid dependency on stopMicrophoneStream
useEffect(() => { useEffect(() => {
@ -647,7 +619,7 @@ export function useMicrophone() {
const stream = microphoneStreamRef.current; const stream = microphoneStreamRef.current;
if (stream) { if (stream) {
devLog("Cleanup: stopping microphone stream on unmount"); devLog("Cleanup: stopping microphone stream on unmount");
stream.getAudioTracks().forEach((track: MediaStreamTrack) => { stream.getAudioTracks().forEach(track => {
track.stop(); track.stop();
devLog(`Cleanup: stopped audio track ${track.id}`); devLog(`Cleanup: stopped audio track ${track.id}`);
}); });

View File

@ -116,7 +116,6 @@ if (isOnDevice) {
path: "/", path: "/",
errorElement: <ErrorBoundary />, errorElement: <ErrorBoundary />,
element: <DeviceRoute />, element: <DeviceRoute />,
HydrateFallback: () => <div className="p-4">Loading...</div>,
loader: DeviceRoute.loader, loader: DeviceRoute.loader,
children: [ children: [
{ {

View File

@ -355,7 +355,7 @@ function UrlView({
const popularImages = [ const popularImages = [
{ {
name: "Ubuntu 24.04 LTS", name: "Ubuntu 24.04 LTS",
url: "https://releases.ubuntu.com/24.04.3/ubuntu-24.04.3-desktop-amd64.iso", url: "https://releases.ubuntu.com/24.04.2/ubuntu-24.04.2-desktop-amd64.iso",
icon: UbuntuIcon, icon: UbuntuIcon,
}, },
{ {
@ -369,8 +369,8 @@ function UrlView({
icon: DebianIcon, icon: DebianIcon,
}, },
{ {
name: "Fedora 42", name: "Fedora 41",
url: "https://download.fedoraproject.org/pub/fedora/linux/releases/42/Workstation/x86_64/iso/Fedora-Workstation-Live-42-1.1.x86_64.iso", url: "https://download.fedoraproject.org/pub/fedora/linux/releases/41/Workstation/x86_64/iso/Fedora-Workstation-Live-x86_64-41-1.4.iso",
icon: FedoraIcon, icon: FedoraIcon,
}, },
{ {
@ -385,7 +385,7 @@ function UrlView({
}, },
{ {
name: "Arch Linux", name: "Arch Linux",
url: "https://archlinux.doridian.net/iso/latest/archlinux-x86_64.iso", url: "https://archlinux.doridian.net/iso/2025.02.01/archlinux-2025.02.01-x86_64.iso",
icon: ArchIcon, icon: ArchIcon,
}, },
{ {

View File

@ -1,16 +1,15 @@
import { useEffect, useState } from "react"; import { useState, useEffect } from "react";
import { Button } from "@/components/Button"; import { Button } from "@/components/Button";
import { TextAreaWithLabel } from "@/components/TextArea"; import { TextAreaWithLabel } from "@/components/TextArea";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { useSettingsStore } from "@/hooks/stores"; import { useSettingsStore } from "@/hooks/stores";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import Fieldset from "@components/Fieldset"; import notifications from "../notifications";
import notifications from "@/notifications"; import { SelectMenuBasic } from "../components/SelectMenuBasic";
import { SettingsItem } from "./devices.$id.settings"; import { SettingsItem } from "./devices.$id.settings";
const defaultEdid = const defaultEdid =
"00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b"; "00ffffffffffff0052620188008888881c150103800000780a0dc9a05747982712484c00000001010101010101010101010101010101023a801871382d40582c4500c48e2100001e011d007251d01e206e285500c48e2100001e000000fc00543734392d6648443732300a20000000fd00147801ff1d000a202020202020017b";
const edids = [ const edids = [
@ -51,27 +50,21 @@ export default function SettingsVideoRoute() {
const [streamQuality, setStreamQuality] = useState("1"); const [streamQuality, setStreamQuality] = useState("1");
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null); const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
const [edid, setEdid] = useState<string | null>(null); const [edid, setEdid] = useState<string | null>(null);
const [edidLoading, setEdidLoading] = useState(false);
// Video enhancement settings from store // Video enhancement settings from store
const { const {
videoSaturation, videoSaturation, setVideoSaturation,
setVideoSaturation, videoBrightness, setVideoBrightness,
videoBrightness, videoContrast, setVideoContrast
setVideoBrightness,
videoContrast,
setVideoContrast,
} = useSettingsStore(); } = useSettingsStore();
useEffect(() => { useEffect(() => {
setEdidLoading(true);
send("getStreamQualityFactor", {}, (resp: JsonRpcResponse) => { send("getStreamQualityFactor", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return; if ("error" in resp) return;
setStreamQuality(String(resp.result)); setStreamQuality(String(resp.result));
}); });
send("getEDID", {}, (resp: JsonRpcResponse) => { send("getEDID", {}, (resp: JsonRpcResponse) => {
setEdidLoading(false);
if ("error" in resp) { if ("error" in resp) {
notifications.error(`Failed to get EDID: ${resp.error.data || "Unknown error"}`); notifications.error(`Failed to get EDID: ${resp.error.data || "Unknown error"}`);
return; return;
@ -96,36 +89,28 @@ export default function SettingsVideoRoute() {
}, [send]); }, [send]);
const handleStreamQualityChange = (factor: string) => { const handleStreamQualityChange = (factor: string) => {
send( send("setStreamQualityFactor", { factor: Number(factor) }, (resp: JsonRpcResponse) => {
"setStreamQualityFactor", if ("error" in resp) {
{ factor: Number(factor) }, notifications.error(
(resp: JsonRpcResponse) => { `Failed to set stream quality: ${resp.error.data || "Unknown error"}`,
if ("error" in resp) {
notifications.error(
`Failed to set stream quality: ${resp.error.data || "Unknown error"}`,
);
return;
}
notifications.success(
`Stream quality set to ${streamQualityOptions.find(x => x.value === factor)?.label}`,
); );
setStreamQuality(factor); return;
}, }
);
notifications.success(`Stream quality set to ${streamQualityOptions.find(x => x.value === factor)?.label}`);
setStreamQuality(factor);
});
}; };
const handleEDIDChange = (newEdid: string) => { const handleEDIDChange = (newEdid: string) => {
setEdidLoading(true);
send("setEDID", { edid: newEdid }, (resp: JsonRpcResponse) => { send("setEDID", { edid: newEdid }, (resp: JsonRpcResponse) => {
setEdidLoading(false);
if ("error" in resp) { if ("error" in resp) {
notifications.error(`Failed to set EDID: ${resp.error.data || "Unknown error"}`); notifications.error(`Failed to set EDID: ${resp.error.data || "Unknown error"}`);
return; return;
} }
notifications.success( notifications.success(
`EDID set successfully to ${edids.find(x => x.value === newEdid)?.label ?? "the custom EDID"}`, `EDID set successfully to ${edids.find(x => x.value === newEdid)?.label}`,
); );
// Update the EDID value in the UI // Update the EDID value in the UI
setEdid(newEdid); setEdid(newEdid);
@ -173,7 +158,7 @@ export default function SettingsVideoRoute() {
step="0.1" step="0.1"
value={videoSaturation} value={videoSaturation}
onChange={e => setVideoSaturation(parseFloat(e.target.value))} onChange={e => setVideoSaturation(parseFloat(e.target.value))}
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700" className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</SettingsItem> </SettingsItem>
@ -188,7 +173,7 @@ export default function SettingsVideoRoute() {
step="0.1" step="0.1"
value={videoBrightness} value={videoBrightness}
onChange={e => setVideoBrightness(parseFloat(e.target.value))} onChange={e => setVideoBrightness(parseFloat(e.target.value))}
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700" className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</SettingsItem> </SettingsItem>
@ -203,7 +188,7 @@ export default function SettingsVideoRoute() {
step="0.1" step="0.1"
value={videoContrast} value={videoContrast}
onChange={e => setVideoContrast(parseFloat(e.target.value))} onChange={e => setVideoContrast(parseFloat(e.target.value))}
className="h-2 w-32 cursor-pointer appearance-none rounded-lg bg-gray-200 dark:bg-gray-700" className="w-32 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer dark:bg-gray-700"
/> />
</SettingsItem> </SettingsItem>
@ -220,64 +205,60 @@ export default function SettingsVideoRoute() {
/> />
</div> </div>
</div> </div>
<Fieldset disabled={edidLoading} className="space-y-2">
<SettingsItem <SettingsItem
title="EDID" title="EDID"
description="Adjust the EDID settings for the display" description="Adjust the EDID settings for the display"
loading={edidLoading} >
> <SelectMenuBasic
<SelectMenuBasic size="SM"
size="SM" label=""
label="" fullWidth
fullWidth value={customEdidValue ? "custom" : edid || "asd"}
value={customEdidValue ? "custom" : edid || "asd"} onChange={e => {
onChange={e => { if (e.target.value === "custom") {
if (e.target.value === "custom") { setEdid("custom");
setEdid("custom"); setCustomEdidValue("");
setCustomEdidValue(""); } else {
} else { setCustomEdidValue(null);
setCustomEdidValue(null); handleEDIDChange(e.target.value as string);
handleEDIDChange(e.target.value as string); }
} }}
}} options={[...edids, { value: "custom", label: "Custom" }]}
options={[...edids, { value: "custom", label: "Custom" }]} />
</SettingsItem>
{customEdidValue !== null && (
<>
<SettingsItem
title="Custom EDID"
description="EDID details video mode compatibility. Default settings works in most cases, but unique UEFI/BIOS might need adjustments."
/> />
</SettingsItem> <TextAreaWithLabel
{customEdidValue !== null && ( label="EDID File"
<> placeholder="00F..."
<SettingsItem rows={3}
title="Custom EDID" value={customEdidValue}
description="EDID details video mode compatibility. Default settings works in most cases, but unique UEFI/BIOS might need adjustments." onChange={e => setCustomEdidValue(e.target.value)}
/>
<div className="flex justify-start gap-x-2">
<Button
size="SM"
theme="primary"
text="Set Custom EDID"
onClick={() => handleEDIDChange(customEdidValue)}
/> />
<TextAreaWithLabel <Button
label="EDID File" size="SM"
placeholder="00F..." theme="light"
rows={3} text="Restore to default"
value={customEdidValue} onClick={() => {
onChange={e => setCustomEdidValue(e.target.value)} setCustomEdidValue(null);
handleEDIDChange(defaultEdid);
}}
/> />
<div className="flex justify-start gap-x-2"> </div>
<Button </>
size="SM" )}
theme="primary"
text="Set Custom EDID"
loading={edidLoading}
onClick={() => handleEDIDChange(customEdidValue)}
/>
<Button
size="SM"
theme="light"
text="Restore to default"
loading={edidLoading}
onClick={() => {
setCustomEdidValue(null);
handleEDIDChange(defaultEdid);
}}
/>
</div>
</>
)}
</Fieldset>
</div> </div>
</div> </div>
</div> </div>

View File

@ -54,7 +54,6 @@ import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider"; import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider";
import { DeviceStatus } from "@routes/welcome-local"; import { DeviceStatus } from "@routes/welcome-local";
import { SystemVersionInfo } from "@routes/devices.$id.settings.general.update"; import { SystemVersionInfo } from "@routes/devices.$id.settings.general.update";
import audioQualityService from "@/services/audioQualityService";
interface LocalLoaderResp { interface LocalLoaderResp {
authMode: "password" | "noPassword" | null; authMode: "password" | "noPassword" | null;
@ -475,27 +474,8 @@ export default function KvmIdRoute() {
} }
}; };
pc.ontrack = function (event: RTCTrackEvent) { pc.ontrack = function (event) {
// Handle separate MediaStreams for audio and video tracks setMediaStream(event.streams[0]);
const track = event.track;
const streams = event.streams;
if (streams && streams.length > 0) {
// Get existing MediaStream or create a new one
const existingStream = useRTCStore.getState().mediaStream;
let combinedStream: MediaStream;
if (existingStream) {
combinedStream = existingStream;
// Add the new track to the existing stream
combinedStream.addTrack(track);
} else {
// Create a new MediaStream with the track
combinedStream = new MediaStream([track]);
}
setMediaStream(combinedStream);
}
}; };
setTransceiver(pc.addTransceiver("video", { direction: "recvonly" })); setTransceiver(pc.addTransceiver("video", { direction: "recvonly" }));
@ -553,11 +533,6 @@ export default function KvmIdRoute() {
}; };
}, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]); }, [clearCandidatePairStats, clearInboundRtpStats, setPeerConnection, setSidebarView]);
// Register callback with audioQualityService
useEffect(() => {
audioQualityService.setReconnectionCallback(setupPeerConnection);
}, [setupPeerConnection]);
// TURN server usage detection // TURN server usage detection
useEffect(() => { useEffect(() => {
if (peerConnectionState !== "connected") return; if (peerConnectionState !== "connected") return;

View File

@ -24,7 +24,6 @@ class AudioQualityService {
2: 'High', 2: 'High',
3: 'Ultra' 3: 'Ultra'
}; };
private reconnectionCallback: (() => Promise<void>) | null = null;
/** /**
* Fetch audio quality presets from the backend * Fetch audio quality presets from the backend
@ -97,34 +96,12 @@ class AudioQualityService {
} }
/** /**
* Set reconnection callback for WebRTC reset * Set audio quality
*/
setReconnectionCallback(callback: () => Promise<void>): void {
this.reconnectionCallback = callback;
}
/**
* Trigger audio track replacement using backend's track replacement mechanism
*/
private async replaceAudioTrack(): Promise<void> {
if (this.reconnectionCallback) {
await this.reconnectionCallback();
}
}
/**
* Set audio quality with track replacement
*/ */
async setAudioQuality(quality: number): Promise<boolean> { async setAudioQuality(quality: number): Promise<boolean> {
try { try {
const response = await api.POST('/audio/quality', { quality }); const response = await api.POST('/audio/quality', { quality });
return response.ok;
if (!response.ok) {
return false;
}
await this.replaceAudioTrack();
return true;
} catch (error) { } catch (error) {
console.error('Failed to set audio quality:', error); console.error('Failed to set audio quality:', error);
return false; return false;

View File

@ -94,17 +94,6 @@ export const formatters = {
}, },
}; };
export function someIterable<T>(
iterable: Iterable<T>,
predicate: (item: T) => boolean,
): boolean {
for (const item of iterable) {
if (predicate(item)) return true;
}
return false;
}
export const VIDEO = new Blob( export const VIDEO = new Blob(
[ [
new Uint8Array([ new Uint8Array([

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt"
"net" "net"
"runtime" "runtime"
"strings" "strings"
@ -25,7 +24,6 @@ type Session struct {
peerConnection *webrtc.PeerConnection peerConnection *webrtc.PeerConnection
VideoTrack *webrtc.TrackLocalStaticSample VideoTrack *webrtc.TrackLocalStaticSample
AudioTrack *webrtc.TrackLocalStaticSample AudioTrack *webrtc.TrackLocalStaticSample
AudioRtpSender *webrtc.RTPSender
ControlChannel *webrtc.DataChannel ControlChannel *webrtc.DataChannel
RPCChannel *webrtc.DataChannel RPCChannel *webrtc.DataChannel
HidChannel *webrtc.DataChannel HidChannel *webrtc.DataChannel
@ -233,21 +231,22 @@ func newSession(config SessionConfig) (*Session, error) {
} }
}) })
session.VideoTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "kvm-video") session.VideoTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264}, "video", "kvm")
if err != nil { if err != nil {
scopedLogger.Warn().Err(err).Msg("Failed to create VideoTrack") scopedLogger.Warn().Err(err).Msg("Failed to create VideoTrack")
return nil, err return nil, err
} }
session.AudioTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "kvm-audio") session.AudioTrack, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus}, "audio", "kvm")
if err != nil { if err != nil {
scopedLogger.Warn().Err(err).Msg("Failed to add VideoTrack to PeerConnection") scopedLogger.Warn().Err(err).Msg("Failed to add VideoTrack to PeerConnection")
return nil, err return nil, err
} }
// Update the audio relay with the new WebRTC audio track asynchronously // Update the audio relay with the new WebRTC audio track
// This prevents blocking during session creation and avoids mutex deadlocks if err := audio.UpdateAudioRelayTrack(session.AudioTrack); err != nil {
audio.UpdateAudioRelayTrackAsync(session.AudioTrack) scopedLogger.Warn().Err(err).Msg("Failed to update audio relay track")
}
videoRtpSender, err := peerConnection.AddTrack(session.VideoTrack) videoRtpSender, err := peerConnection.AddTrack(session.VideoTrack)
if err != nil { if err != nil {
@ -262,7 +261,6 @@ func newSession(config SessionConfig) (*Session, error) {
return nil, err return nil, err
} }
audioRtpSender := audioTransceiver.Sender() audioRtpSender := audioTransceiver.Sender()
session.AudioRtpSender = audioRtpSender
// Handle incoming audio track (microphone from browser) // Handle incoming audio track (microphone from browser)
peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { peerConnection.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
@ -412,22 +410,6 @@ func (s *Session) stopAudioProcessor() {
s.audioWg.Wait() s.audioWg.Wait()
} }
// ReplaceAudioTrack replaces the current audio track with a new one
func (s *Session) ReplaceAudioTrack(newTrack *webrtc.TrackLocalStaticSample) error {
if s.AudioRtpSender == nil {
return fmt.Errorf("audio RTP sender not available")
}
// Replace the track using the RTP sender
if err := s.AudioRtpSender.ReplaceTrack(newTrack); err != nil {
return fmt.Errorf("failed to replace audio track: %w", err)
}
// Update the session's audio track reference
s.AudioTrack = newTrack
return nil
}
func drainRtpSender(rtpSender *webrtc.RTPSender) { func drainRtpSender(rtpSender *webrtc.RTPSender) {
// Lock to OS thread to isolate RTCP processing // Lock to OS thread to isolate RTCP processing
runtime.LockOSThread() runtime.LockOSThread()