mirror of https://github.com/jetkvm/kvm.git
Compare commits
No commits in common. "02acee0c75876bf9a4814aa4988982d211bf7202" and "6890f17a5429c7bf42b370194ae47d2247c6793c" have entirely different histories.
02acee0c75
...
6890f17a54
|
|
@ -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,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,6 +91,7 @@ type AdaptiveBufferManager struct {
|
||||||
|
|
||||||
config AdaptiveBufferConfig
|
config AdaptiveBufferConfig
|
||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
|
processMonitor *ProcessMonitor
|
||||||
|
|
||||||
// Control channels
|
// Control channels
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
@ -118,7 +119,7 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
return 0, errAudioReadEncode
|
||||||
|
default:
|
||||||
|
return 0, newAudioReadEncodeError(int(n))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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))
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,7 +131,7 @@ 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
|
||||||
|
|
@ -171,6 +172,9 @@ 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
|
||||||
|
|
@ -182,7 +186,7 @@ type AudioConfigConstants struct {
|
||||||
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
|
||||||
|
|
@ -212,6 +216,27 @@ type AudioConfigConstants struct {
|
||||||
|
|
||||||
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,14 +272,7 @@ type AudioConfigConstants struct {
|
||||||
LatencyPercentile50 int
|
LatencyPercentile50 int
|
||||||
LatencyPercentile95 int
|
LatencyPercentile95 int
|
||||||
LatencyPercentile99 int
|
LatencyPercentile99 int
|
||||||
|
BufferPoolMaxOperations int
|
||||||
// Buffer Pool Configuration
|
|
||||||
BufferPoolDefaultSize int // Default buffer pool size when MaxPoolSize is invalid
|
|
||||||
BufferPoolControlSize int // Control buffer pool size
|
|
||||||
ZeroCopyPreallocSizeBytes int // Zero-copy frame pool preallocation size in bytes
|
|
||||||
ZeroCopyMinPreallocFrames int // Minimum preallocated frames for zero-copy pool
|
|
||||||
BufferPoolHitRateBase float64 // Base for hit rate percentage calculation
|
|
||||||
|
|
||||||
HitRateCalculationBase float64
|
HitRateCalculationBase float64
|
||||||
MaxLatency time.Duration
|
MaxLatency time.Duration
|
||||||
MinMetricsUpdateInterval time.Duration
|
MinMetricsUpdateInterval time.Duration
|
||||||
|
|
@ -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
|
|
||||||
HealthCheckInterval: 10 * time.Second, // Health check interval for connections
|
|
||||||
|
|
||||||
// Quality Change Timeout Configuration
|
|
||||||
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
|
|
||||||
DefaultSleepDuration: 100 * time.Millisecond, // Balanced polling interval
|
|
||||||
ShortSleepDuration: 10 * time.Millisecond, // Balanced high-frequency polling
|
|
||||||
LongSleepDuration: 200 * time.Millisecond, // Balanced background task delay
|
|
||||||
DefaultTickerInterval: 100 * time.Millisecond, // Balanced periodic task interval
|
|
||||||
BufferUpdateInterval: 250 * time.Millisecond, // Faster buffer size update frequency
|
|
||||||
InputSupervisorTimeout: 5 * time.Second, // Input monitoring timeout
|
InputSupervisorTimeout: 5 * time.Second, // Input monitoring timeout
|
||||||
OutputSupervisorTimeout: 5 * time.Second, // Output monitoring timeout
|
OutputSupervisorTimeout: 5 * time.Second, // Output monitoring timeout
|
||||||
BatchProcessingDelay: 5 * time.Millisecond, // Reduced batch processing delay
|
BatchProcessingDelay: 10 * time.Millisecond, // Batch processing delay
|
||||||
|
AdaptiveOptimizerStability: 10 * time.Second, // Adaptive stability period
|
||||||
|
|
||||||
// Adaptive Buffer Configuration - Optimized for single-core RV1106G3
|
LatencyMonitorTarget: 50 * time.Millisecond, // Target latency for monitoring
|
||||||
LowCPUThreshold: 0.40, // Adjusted for single-core ARM system
|
|
||||||
HighCPUThreshold: 0.75, // Adjusted for single-core RV1106G3 (current load ~64%)
|
|
||||||
LowMemoryThreshold: 0.60,
|
|
||||||
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 Buffer Configuration
|
||||||
AdaptiveMinBufferSize: 256, // Further increased minimum to prevent emergency mode
|
LowCPUThreshold: 0.20,
|
||||||
AdaptiveMaxBufferSize: 1024, // Much higher maximum for quality changes
|
HighCPUThreshold: 0.60,
|
||||||
AdaptiveDefaultBufferSize: 512, // Higher default for stability during bursts
|
LowMemoryThreshold: 0.50,
|
||||||
|
HighMemoryThreshold: 0.75,
|
||||||
|
AdaptiveBufferTargetLatency: 20 * time.Millisecond,
|
||||||
|
|
||||||
CooldownPeriod: 15 * time.Second, // Reduced cooldown period
|
// Adaptive Buffer Size Configuration
|
||||||
RollbackThreshold: 200 * time.Millisecond, // Lower rollback threshold
|
AdaptiveMinBufferSize: 3, // Minimum 3 frames for stability
|
||||||
|
AdaptiveMaxBufferSize: 20, // Maximum 20 frames for high load
|
||||||
|
AdaptiveDefaultBufferSize: 6, // Balanced buffer size (6 frames)
|
||||||
|
|
||||||
MaxLatencyThreshold: 150 * time.Millisecond, // Lower max latency threshold
|
// Adaptive Optimizer Configuration
|
||||||
JitterThreshold: 15 * time.Millisecond, // Reduced jitter threshold
|
CooldownPeriod: 30 * time.Second,
|
||||||
LatencyOptimizationInterval: 3 * time.Second, // More frequent optimization
|
RollbackThreshold: 300 * time.Millisecond,
|
||||||
LatencyAdaptiveThreshold: 0.7, // More aggressive adaptive threshold
|
AdaptiveOptimizerLatencyTarget: 50 * time.Millisecond,
|
||||||
|
|
||||||
|
// Latency Monitor Configuration
|
||||||
|
MaxLatencyThreshold: 200 * time.Millisecond,
|
||||||
|
JitterThreshold: 20 * time.Millisecond,
|
||||||
|
LatencyOptimizationInterval: 5 * time.Second,
|
||||||
|
LatencyAdaptiveThreshold: 0.8,
|
||||||
|
|
||||||
// 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,14 +614,8 @@ 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
|
||||||
|
BufferPoolMaxOperations: 1000, // 1000 operations for efficiency tracking
|
||||||
HitRateCalculationBase: 100.0, // 100.0 base for hit rate percentage calculation
|
HitRateCalculationBase: 100.0, // 100.0 base for hit rate percentage calculation
|
||||||
|
|
||||||
// Validation Constants
|
// Validation Constants
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
// Fast path: Check against cached bitrate values
|
||||||
|
cache := GetCachedConfig()
|
||||||
|
minBitrate := int(cache.minOpusBitrate.Load())
|
||||||
|
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
|
bitrateInBps := bitrate * 1000
|
||||||
if bitrateInBps < Config.MinOpusBitrate {
|
if bitrateInBps < minBitrate {
|
||||||
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps",
|
return fmt.Errorf("%w: bitrate %d kbps (%d bps) below minimum %d bps",
|
||||||
ErrInvalidBitrate, bitrate, bitrateInBps, Config.MinOpusBitrate)
|
ErrInvalidBitrate, bitrate, bitrateInBps, minBitrate)
|
||||||
}
|
}
|
||||||
if bitrateInBps > Config.MaxOpusBitrate {
|
if bitrateInBps > maxBitrate {
|
||||||
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, maxBitrate)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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",
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
Timestamp: time.Now().UnixNano(),
|
||||||
Data: frame,
|
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
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,11 @@ 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
|
||||||
|
|
||||||
|
|
@ -23,7 +16,6 @@ type OutputIPCMessage = UnifiedIPCMessage
|
||||||
const (
|
const (
|
||||||
OutputMessageTypeOpusFrame = MessageTypeOpusFrame
|
OutputMessageTypeOpusFrame = MessageTypeOpusFrame
|
||||||
OutputMessageTypeConfig = MessageTypeConfig
|
OutputMessageTypeConfig = MessageTypeConfig
|
||||||
OutputMessageTypeOpusConfig = MessageTypeOpusConfig
|
|
||||||
OutputMessageTypeStop = MessageTypeStop
|
OutputMessageTypeStop = MessageTypeStop
|
||||||
OutputMessageTypeHeartbeat = MessageTypeHeartbeat
|
OutputMessageTypeHeartbeat = MessageTypeHeartbeat
|
||||||
OutputMessageTypeAck = MessageTypeAck
|
OutputMessageTypeAck = MessageTypeAck
|
||||||
|
|
@ -32,365 +24,47 @@ const (
|
||||||
// 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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
s.logger.Warn().Err(err).Int("attempt", i+1).Msg("failed to remove existing socket file, retrying")
|
return fmt.Errorf("failed to remove existing socket: %w", err)
|
||||||
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,21 +361,22 @@ 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 msg.Length > 0 && msg.Data != nil {
|
|
||||||
// Pre-allocate combined buffer to avoid copying
|
|
||||||
combined := make([]byte, len(header)+len(msg.Data))
|
|
||||||
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 {
|
if _, err := conn.Write(header); err != nil {
|
||||||
return fmt.Errorf("failed to write header: %w", err)
|
return fmt.Errorf("failed to write header: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write data if present
|
||||||
|
if msg.Length > 0 && msg.Data != nil {
|
||||||
|
if _, err := conn.Write(msg.Data); err != nil {
|
||||||
|
return fmt.Errorf("failed to write data: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -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
|
||||||
|
|
@ -453,9 +419,7 @@ func NewUnifiedAudioClient(isInput bool) *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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -45,7 +46,7 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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() {
|
|
||||||
// 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")
|
|
||||||
if err := supervisor.SendOpusConfig(opusConfig); err != nil {
|
|
||||||
logger.Warn().Err(err).Msg("failed to send dynamic Opus config update via IPC, falling back to subprocess restart")
|
|
||||||
// Fallback to subprocess restart if IPC update fails
|
|
||||||
supervisor.Stop()
|
supervisor.Stop()
|
||||||
if err := supervisor.Start(); err != nil {
|
|
||||||
logger.Error().Err(err).Msg("failed to restart audio output subprocess after IPC update failure")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.Info().Msg("audio output quality updated dynamically via IPC")
|
|
||||||
|
|
||||||
// Reset audio output stats after config update
|
// Start subprocess with new configuration
|
||||||
go func() {
|
if err := supervisor.Start(); err != nil {
|
||||||
time.Sleep(Config.QualityChangeSettleDelay) // Wait for quality change to settle
|
logger.Error().Err(err).Msg("failed to restart audio output subprocess")
|
||||||
// Reset audio input server stats to clear persistent warnings
|
|
||||||
ResetGlobalAudioInputServerStats()
|
|
||||||
// Attempt recovery if there are still issues
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
RecoverGlobalAudioInputServer()
|
|
||||||
}()
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Info().Bool("supervisor_running", supervisor.IsRunning()).Msg("audio output subprocess not connected, configuration will apply on next start")
|
// Fallback to dynamic update if supervisor is not available
|
||||||
|
vbrConstraint := GetConfig().CGOOpusVBRConstraint
|
||||||
|
if err := updateOpusEncoderParams(config.Bitrate*1000, complexity, vbr, vbrConstraint, signalType, bandwidth, dtx); err != nil {
|
||||||
|
logging.GetDefaultLogger().Error().Err(err).Msg("Failed to update OPUS encoder parameters")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
|
if supervisor := GetAudioInputSupervisor(); supervisor != nil {
|
||||||
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
logger := logging.GetDefaultLogger().With().Str("component", "audio").Logger()
|
||||||
|
logger.Info().Int("quality", int(quality)).Msg("updating audio input subprocess quality settings dynamically")
|
||||||
|
|
||||||
// Set new OPUS configuration for future restarts
|
// Set new OPUS configuration for future restarts
|
||||||
if supervisor := GetAudioInputSupervisor(); supervisor != nil {
|
supervisor.SetOpusConfig(config.Bitrate*1000, complexity, vbr, signalType, bandwidth, dtx)
|
||||||
supervisor.SetOpusConfig(config.Bitrate*1000, params.complexity, params.vbr, params.signalType, params.bandwidth, params.dtx)
|
|
||||||
|
|
||||||
// Check if microphone is active but IPC control is broken
|
// Send dynamic configuration update to running subprocess
|
||||||
inputManager := getAudioInputManager()
|
|
||||||
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
|
|
||||||
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")
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
maxAttempts := 5
|
|
||||||
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 {
|
if err := relay.Start(audioTrack, config); err != nil {
|
||||||
lastErr = err
|
return 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
|
globalRelay = relay
|
||||||
return nil
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("failed to start audio relay after %d attempts: %w", maxAttempts, lastErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Update the track in the existing relay
|
||||||
globalRelay.UpdateTrack(audioTrack)
|
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
|
|
|
||||||
|
|
@ -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)",
|
||||||
|
|
|
||||||
|
|
@ -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 AudioLatencyInfo struct {
|
||||||
|
LatencyMs float64
|
||||||
|
Timestamp time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global latency tracking
|
||||||
|
var (
|
||||||
|
currentAudioLatency = AudioLatencyInfo{}
|
||||||
|
currentAudioLatencyLock sync.RWMutex
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the data is too old (more than 5 seconds)
|
||||||
|
if time.Since(currentAudioLatency.Timestamp) > 5*time.Second {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &AudioLatencyInfo{
|
||||||
|
LatencyMs: currentAudioLatency.LatencyMs,
|
||||||
|
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 {
|
||||||
|
case <-cleanupChannel:
|
||||||
|
// Received explicit cleanup request
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
type AudioBufferPool struct {
|
||||||
// Atomic counters
|
// 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)
|
hitCount int64 // Pool hit counter (atomic)
|
||||||
missCount int64 // Pool miss counter (atomic)
|
missCount int64 // Pool miss counter (atomic)
|
||||||
|
|
||||||
// Pool configuration
|
// Other fields
|
||||||
|
pool sync.Pool
|
||||||
bufferSize int
|
bufferSize int
|
||||||
pool chan []byte
|
maxPoolSize int
|
||||||
maxSize int
|
mutex sync.RWMutex
|
||||||
|
// Memory optimization fields
|
||||||
|
preallocated []*[]byte // Pre-allocated buffers for immediate use
|
||||||
|
preallocSize int // Number of pre-allocated buffers
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAudioBufferPool creates a new simple audio buffer pool
|
|
||||||
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
func NewAudioBufferPool(bufferSize int) *AudioBufferPool {
|
||||||
maxSize := Config.MaxPoolSize
|
// Validate buffer size parameter
|
||||||
if maxSize <= 0 {
|
if err := ValidateBufferSize(bufferSize); err != nil {
|
||||||
maxSize = Config.BufferPoolDefaultSize
|
// Use default value on validation error
|
||||||
|
bufferSize = GetConfig().AudioFramePoolSize
|
||||||
}
|
}
|
||||||
|
|
||||||
pool := &AudioBufferPool{
|
// 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,
|
bufferSize: bufferSize,
|
||||||
pool: make(chan []byte, maxSize),
|
maxPoolSize: GetConfig().MaxPoolSize * 2, // Double the max pool size for better buffering
|
||||||
maxSize: maxSize,
|
preallocated: preallocated,
|
||||||
|
preallocSize: preallocSize,
|
||||||
|
pool: sync.Pool{
|
||||||
|
New: func() interface{} {
|
||||||
|
// Allocate exact size to minimize memory waste
|
||||||
|
buf := make([]byte, 0, bufferSize)
|
||||||
|
return &buf
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pre-populate the pool
|
|
||||||
for i := 0; i < maxSize/2; i++ {
|
|
||||||
buf := make([]byte, bufferSize)
|
|
||||||
select {
|
|
||||||
case pool.pool <- buf:
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
atomic.AddInt64(&p.hitCount, 1)
|
||||||
return buf[:0] // Reset length but keep capacity
|
*buf = (*buf)[:0]
|
||||||
default:
|
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)
|
||||||
|
*buf = (*buf)[:0]
|
||||||
|
return *buf
|
||||||
|
}
|
||||||
|
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)
|
atomic.AddInt64(&p.missCount, 1)
|
||||||
return make([]byte, 0, p.bufferSize)
|
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 AudioBufferPoolStats{
|
return AudioBufferPoolDetailedStats{
|
||||||
BufferSize: p.bufferSize,
|
BufferSize: p.bufferSize,
|
||||||
MaxPoolSize: p.maxSize,
|
MaxPoolSize: p.maxPoolSize,
|
||||||
CurrentSize: int64(len(p.pool)),
|
CurrentPoolSize: currentSize,
|
||||||
|
PreallocatedCount: int64(preallocatedCount),
|
||||||
|
PreallocatedMax: int64(p.preallocSize),
|
||||||
HitCount: hitCount,
|
HitCount: hitCount,
|
||||||
MissCount: missCount,
|
MissCount: missCount,
|
||||||
HitRate: hitRate,
|
HitRate: hitRate,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// AudioBufferPoolStats represents pool statistics
|
// AudioBufferPoolDetailedStats provides detailed pool statistics
|
||||||
type AudioBufferPoolStats struct {
|
type AudioBufferPoolDetailedStats struct {
|
||||||
BufferSize int
|
BufferSize int
|
||||||
MaxPoolSize int
|
MaxPoolSize int
|
||||||
CurrentSize int64
|
CurrentPoolSize int64
|
||||||
|
PreallocatedCount int64
|
||||||
|
PreallocatedMax int64
|
||||||
HitCount int64
|
HitCount int64
|
||||||
MissCount int64
|
MissCount int64
|
||||||
HitRate float64
|
HitRate float64 // Percentage
|
||||||
|
TotalBytes int64 // Total memory usage in bytes
|
||||||
|
AverageBufferSize float64 // Average size of buffers in the pool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Global buffer pools
|
// GetAudioBufferPoolStats returns statistics about the audio buffer pools
|
||||||
var (
|
type AudioBufferPoolStats struct {
|
||||||
audioFramePool = NewAudioBufferPool(Config.AudioFramePoolSize)
|
FramePoolSize int64
|
||||||
audioControlPool = NewAudioBufferPool(Config.BufferPoolControlSize)
|
FramePoolMax int
|
||||||
)
|
ControlPoolSize int64
|
||||||
|
ControlPoolMax int
|
||||||
// GetAudioFrameBuffer gets a buffer for audio frames
|
// Enhanced statistics
|
||||||
func GetAudioFrameBuffer() []byte {
|
FramePoolHitRate float64
|
||||||
return audioFramePool.Get()
|
ControlPoolHitRate float64
|
||||||
|
FramePoolDetails AudioBufferPoolDetailedStats
|
||||||
|
ControlPoolDetails AudioBufferPoolDetailedStats
|
||||||
}
|
}
|
||||||
|
|
||||||
// PutAudioFrameBuffer returns a buffer to the frame pool
|
func GetAudioBufferPoolStats() AudioBufferPoolStats {
|
||||||
func PutAudioFrameBuffer(buf []byte) {
|
audioFramePool.mutex.RLock()
|
||||||
audioFramePool.Put(buf)
|
frameSize := audioFramePool.currentSize
|
||||||
}
|
frameMax := audioFramePool.maxPoolSize
|
||||||
|
audioFramePool.mutex.RUnlock()
|
||||||
|
|
||||||
// GetAudioControlBuffer gets a buffer for control messages
|
audioControlPool.mutex.RLock()
|
||||||
func GetAudioControlBuffer() []byte {
|
controlSize := audioControlPool.currentSize
|
||||||
return audioControlPool.Get()
|
controlMax := audioControlPool.maxPoolSize
|
||||||
}
|
audioControlPool.mutex.RUnlock()
|
||||||
|
|
||||||
// PutAudioControlBuffer returns a buffer to the control pool
|
// Get detailed statistics
|
||||||
func PutAudioControlBuffer(buf []byte) {
|
frameDetails := audioFramePool.GetPoolStats()
|
||||||
audioControlPool.Put(buf)
|
controlDetails := audioControlPool.GetPoolStats()
|
||||||
}
|
|
||||||
|
|
||||||
// GetAudioBufferPoolStats returns statistics for all pools
|
return AudioBufferPoolStats{
|
||||||
func GetAudioBufferPoolStats() map[string]AudioBufferPoolStats {
|
FramePoolSize: frameSize,
|
||||||
return map[string]AudioBufferPoolStats{
|
FramePoolMax: frameMax,
|
||||||
"frame_pool": audioFramePool.GetStats(),
|
ControlPoolSize: controlSize,
|
||||||
"control_pool": audioControlPool.GetStats(),
|
ControlPoolMax: controlMax,
|
||||||
|
FramePoolHitRate: frameDetails.HitRate,
|
||||||
|
ControlPoolHitRate: controlDetails.HitRate,
|
||||||
|
FramePoolDetails: frameDetails,
|
||||||
|
ControlPoolDetails: controlDetails,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdaptiveResize dynamically adjusts pool parameters based on performance metrics
|
||||||
|
func (p *AudioBufferPool) AdaptiveResize() {
|
||||||
|
hitCount := atomic.LoadInt64(&p.hitCount)
|
||||||
|
missCount := atomic.LoadInt64(&p.missCount)
|
||||||
|
totalRequests := hitCount + missCount
|
||||||
|
|
||||||
|
if totalRequests < 100 {
|
||||||
|
return // Not enough data for meaningful adaptation
|
||||||
|
}
|
||||||
|
|
||||||
|
hitRate := float64(hitCount) / float64(totalRequests)
|
||||||
|
currentSize := atomic.LoadInt64(&p.currentSize)
|
||||||
|
|
||||||
|
// If hit rate is low (< 80%), consider increasing pool size
|
||||||
|
if hitRate < 0.8 && currentSize < int64(p.maxPoolSize) {
|
||||||
|
// Increase preallocation by 25% up to max pool size
|
||||||
|
newPreallocSize := int(float64(len(p.preallocated)) * 1.25)
|
||||||
|
if newPreallocSize > p.maxPoolSize {
|
||||||
|
newPreallocSize = p.maxPoolSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preallocate additional buffers
|
||||||
|
for len(p.preallocated) < newPreallocSize {
|
||||||
|
buf := make([]byte, p.bufferSize)
|
||||||
|
p.preallocated = append(p.preallocated, &buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If hit rate is very high (> 95%) and pool is large, consider shrinking
|
||||||
|
if hitRate > 0.95 && len(p.preallocated) > p.preallocSize {
|
||||||
|
// Reduce preallocation by 10% but not below original size
|
||||||
|
newSize := int(float64(len(p.preallocated)) * 0.9)
|
||||||
|
if newSize < p.preallocSize {
|
||||||
|
newSize = p.preallocSize
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove excess preallocated buffers
|
||||||
|
if newSize < len(p.preallocated) {
|
||||||
|
p.preallocated = p.preallocated[:newSize]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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,9 +191,10 @@ 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--
|
||||||
|
if frame.refCount <= 0 {
|
||||||
|
frame.refCount = 0
|
||||||
frame.length = 0
|
frame.length = 0
|
||||||
frame.data = frame.data[:0]
|
frame.data = frame.data[:0]
|
||||||
frame.mutex.Unlock()
|
frame.mutex.Unlock()
|
||||||
|
|
@ -219,7 +219,15 @@ func (p *ZeroCopyFramePool) Put(frame *ZeroCopyAudioFrame) {
|
||||||
|
|
||||||
// Return to sync.Pool
|
// Return to sync.Pool
|
||||||
p.pool.Put(frame)
|
p.pool.Put(frame)
|
||||||
|
// Metrics collection removed
|
||||||
|
if false {
|
||||||
atomic.AddInt64(&p.counter, 1)
|
atomic.AddInt64(&p.counter, 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
frame.mutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics recording removed - granular metrics collector was unused
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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{
|
||||||
|
|
|
||||||
26
main.go
26
main.go
|
|
@ -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,13 +77,6 @@ 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
|
|
||||||
// This prevents "no client connected" errors during quality changes
|
|
||||||
go func() {
|
|
||||||
// Give the audio output server time to initialize and start listening
|
|
||||||
// Increased delay to reduce frame drops during connection establishment
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
|
|
||||||
// Start audio relay system for main process
|
// Start audio relay system for main process
|
||||||
// If there's an active WebRTC session, use its audio track
|
// If there's an active WebRTC session, use its audio track
|
||||||
var audioTrack *webrtc.TrackLocalStaticSample
|
var audioTrack *webrtc.TrackLocalStaticSample
|
||||||
|
|
@ -90,13 +89,7 @@ func startAudioSubprocess() error {
|
||||||
|
|
||||||
if err := audio.StartAudioRelay(audioTrack); err != nil {
|
if err := audio.StartAudioRelay(audioTrack); err != nil {
|
||||||
logger.Error().Err(err).Msg("failed to start audio relay")
|
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()
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
<div>
|
||||||
<MenuButton
|
<MenuButton
|
||||||
as={Button}
|
as={Button}
|
||||||
theme="light"
|
theme="light"
|
||||||
TrailingIcon={LuEllipsisVertical}
|
TrailingIcon={LuEllipsisVertical}
|
||||||
size="MD"
|
size="MD"
|
||||||
></MenuButton>
|
></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"
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -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}
|
||||||
|
|
@ -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 },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
Number of data packets lost during transmission.
|
||||||
|
</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>
|
||||||
|
) : isMetricSupported(inboundRtpStats, "packetsLost") ? (
|
||||||
|
<StatChart
|
||||||
|
data={createChartArray(inboundRtpStats, "packetsLost")}
|
||||||
|
domain={[0, 100]}
|
||||||
|
unit=" packets"
|
||||||
/>
|
/>
|
||||||
<Metric
|
) : (
|
||||||
title="Round-Trip Time"
|
<div className="flex flex-col items-center space-y-1">
|
||||||
description="Round-trip time for the active ICE candidate pair between peers."
|
<p className="text-black">Metric not supported</p>
|
||||||
stream={iceCandidatePairStats}
|
</div>
|
||||||
metric="currentRoundTripTime"
|
)}
|
||||||
map={x => ({
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-black dark:text-white">
|
||||||
|
Round-Trip Time
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-slate-700 dark:text-slate-300">
|
||||||
|
Time taken for data to travel from source to destination and back
|
||||||
|
</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>
|
||||||
|
) : isMetricSupported(candidatePairStats, "currentRoundTripTime") ? (
|
||||||
|
<StatChart
|
||||||
|
data={createChartArray(
|
||||||
|
candidatePairStats,
|
||||||
|
"currentRoundTripTime",
|
||||||
|
).map(x => {
|
||||||
|
return {
|
||||||
date: x.date,
|
date: x.date,
|
||||||
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
|
stat: x.stat ? Math.round(x.stat * 1000) : null,
|
||||||
|
};
|
||||||
})}
|
})}
|
||||||
domain={[0, 600]}
|
domain={[0, 600]}
|
||||||
unit=" ms"
|
unit=" ms"
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center space-y-1">
|
||||||
|
<p className="text-black">Metric not supported</p>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
{/* Video Group */}
|
</div>
|
||||||
<div className="space-y-3">
|
</GridCard>
|
||||||
<SettingsSectionHeader
|
</div>
|
||||||
title="Video"
|
<div className="space-y-2">
|
||||||
description="The video stream from the JetKVM to the client."
|
<div>
|
||||||
/>
|
<h2 className="text-lg font-semibold text-black dark:text-white">
|
||||||
|
Jitter
|
||||||
{/* RTP Jitter */}
|
</h2>
|
||||||
<Metric
|
<p className="text-sm text-slate-700 dark:text-slate-300">
|
||||||
title="Network Stability"
|
Variation in packet delay, affecting video smoothness.{" "}
|
||||||
badge="Jitter"
|
</p>
|
||||||
badgeTheme="light"
|
</div>
|
||||||
description="How steady the flow of inbound video packets is across the network."
|
<GridCard>
|
||||||
stream={inboundVideoRtpStats}
|
<div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
|
||||||
metric="jitter"
|
{inboundRtpStats.size === 0 ? (
|
||||||
map={x => ({
|
<div className="flex flex-col items-center space-y-1">
|
||||||
|
<p className="text-slate-700">Waiting for data...</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<StatChart
|
||||||
|
data={createChartArray(inboundRtpStats, "jitter").map(x => {
|
||||||
|
return {
|
||||||
date: x.date,
|
date: x.date,
|
||||||
metric: x.metric != null ? Math.round(x.metric * 1000) : null,
|
stat: x.stat ? Math.round(x.stat * 1000) : null,
|
||||||
|
};
|
||||||
})}
|
})}
|
||||||
domain={[0, 10]}
|
domain={[0, 300]}
|
||||||
unit=" ms"
|
unit=" ms"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
{/* Playback Delay */}
|
</div>
|
||||||
<Metric
|
</GridCard>
|
||||||
title="Playback Delay"
|
</div>
|
||||||
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly."
|
<div className="space-y-2">
|
||||||
badge="Jitter Buffer Avg. Delay"
|
<div>
|
||||||
badgeTheme="light"
|
<h2 className="text-lg font-semibold text-black dark:text-white">
|
||||||
data={jitterBufferAvgDelayData}
|
Frames per second
|
||||||
gate={inboundVideoRtpStats}
|
</h2>
|
||||||
supported={
|
<p className="text-sm text-slate-700 dark:text-slate-300">
|
||||||
someIterable(
|
Number of video frames displayed per second.
|
||||||
inboundVideoRtpStats,
|
</p>
|
||||||
([, x]) => x.jitterBufferDelay != null,
|
</div>
|
||||||
) &&
|
<GridCard>
|
||||||
someIterable(
|
<div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
|
||||||
inboundVideoRtpStats,
|
{inboundRtpStats.size === 0 ? (
|
||||||
([, x]) => x.jitterBufferEmittedCount != null,
|
<div className="flex flex-col items-center space-y-1">
|
||||||
)
|
<p className="text-slate-700">Waiting for data...</p>
|
||||||
}
|
</div>
|
||||||
domain={[0, 30]}
|
) : (
|
||||||
unit=" ms"
|
<StatChart
|
||||||
/>
|
data={createChartArray(inboundRtpStats, "framesPerSecond").map(
|
||||||
|
x => {
|
||||||
{/* Packets Lost */}
|
return {
|
||||||
<Metric
|
date: x.date,
|
||||||
title="Packets Lost"
|
stat: x.stat ? x.stat : null,
|
||||||
description="Count of lost inbound video RTP packets."
|
};
|
||||||
stream={inboundVideoRtpStats}
|
},
|
||||||
metric="packetsLost"
|
)}
|
||||||
domain={[0, 100]}
|
|
||||||
unit=" packets"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Frames Per Second */}
|
|
||||||
<Metric
|
|
||||||
title="Frames per second"
|
|
||||||
description="Number of inbound video frames displayed per second."
|
|
||||||
stream={inboundVideoRtpStats}
|
|
||||||
metric="framesPerSecond"
|
|
||||||
domain={[0, 80]}
|
domain={[0, 80]}
|
||||||
unit=" fps"
|
unit=" fps"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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: [
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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,10 +89,7 @@ export default function SettingsVideoRoute() {
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
|
||||||
const handleStreamQualityChange = (factor: string) => {
|
const handleStreamQualityChange = (factor: string) => {
|
||||||
send(
|
send("setStreamQualityFactor", { factor: Number(factor) }, (resp: JsonRpcResponse) => {
|
||||||
"setStreamQualityFactor",
|
|
||||||
{ factor: Number(factor) },
|
|
||||||
(resp: JsonRpcResponse) => {
|
|
||||||
if ("error" in resp) {
|
if ("error" in resp) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
`Failed to set stream quality: ${resp.error.data || "Unknown error"}`,
|
`Failed to set stream quality: ${resp.error.data || "Unknown error"}`,
|
||||||
|
|
@ -107,25 +97,20 @@ export default function SettingsVideoRoute() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.success(
|
notifications.success(`Stream quality set to ${streamQualityOptions.find(x => x.value === factor)?.label}`);
|
||||||
`Stream quality set to ${streamQualityOptions.find(x => x.value === factor)?.label}`,
|
|
||||||
);
|
|
||||||
setStreamQuality(factor);
|
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,11 +205,10 @@ 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"
|
||||||
|
|
@ -261,14 +245,12 @@ export default function SettingsVideoRoute() {
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="primary"
|
theme="primary"
|
||||||
text="Set Custom EDID"
|
text="Set Custom EDID"
|
||||||
loading={edidLoading}
|
|
||||||
onClick={() => handleEDIDChange(customEdidValue)}
|
onClick={() => handleEDIDChange(customEdidValue)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
size="SM"
|
size="SM"
|
||||||
theme="light"
|
theme="light"
|
||||||
text="Restore to default"
|
text="Restore to default"
|
||||||
loading={edidLoading}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCustomEdidValue(null);
|
setCustomEdidValue(null);
|
||||||
handleEDIDChange(defaultEdid);
|
handleEDIDChange(defaultEdid);
|
||||||
|
|
@ -277,7 +259,6 @@ export default function SettingsVideoRoute() {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Fieldset>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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([
|
||||||
|
|
|
||||||
30
webrtc.go
30
webrtc.go
|
|
@ -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()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue