mirror of
https://github.com/jetkvm/kvm.git
synced 2025-09-16 08:38:14 +00:00
This change replaces all instances of GetConfig() function calls with direct access to the Config variable throughout the audio package. The modification improves performance by eliminating function call overhead and simplifies the codebase by removing unnecessary indirection. The commit also includes minor optimizations in validation logic and connection handling, while maintaining all existing functionality. Error handling remains robust with appropriate fallbacks when config values are not available. Additional improvements include: - Enhanced connection health monitoring in UnifiedAudioClient - Optimized validation functions using cached config values - Reduced memory allocations in hot paths - Improved error recovery during quality changes
191 lines
5.5 KiB
Go
191 lines
5.5 KiB
Go
//go:build cgo
|
|
// +build cgo
|
|
|
|
package audio
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// Removed unused AudioOutputStreamer struct - actual streaming uses direct functions
|
|
|
|
var (
|
|
outputStreamingRunning int32
|
|
outputStreamingCancel context.CancelFunc
|
|
outputStreamingLogger *zerolog.Logger
|
|
)
|
|
|
|
func getOutputStreamingLogger() *zerolog.Logger {
|
|
if outputStreamingLogger == nil {
|
|
logger := logging.GetDefaultLogger().With().Str("component", "audio-output-streaming").Logger()
|
|
outputStreamingLogger = &logger
|
|
}
|
|
return outputStreamingLogger
|
|
}
|
|
|
|
// Removed unused NewAudioOutputStreamer function
|
|
|
|
// Removed unused AudioOutputStreamer.Start method
|
|
|
|
// Removed unused AudioOutputStreamer.Stop method
|
|
|
|
// Removed unused AudioOutputStreamer.streamLoop method
|
|
|
|
// Removed unused AudioOutputStreamer.processingLoop method
|
|
|
|
// Removed unused AudioOutputStreamer.statisticsLoop method
|
|
|
|
// Removed unused AudioOutputStreamer.reportStatistics method
|
|
|
|
// Removed all unused AudioOutputStreamer methods
|
|
|
|
// StartAudioOutputStreaming starts audio output streaming (capturing system audio)
|
|
func StartAudioOutputStreaming(send func([]byte)) error {
|
|
// Initialize audio monitoring (latency tracking and cache cleanup)
|
|
InitializeAudioMonitoring()
|
|
|
|
if !atomic.CompareAndSwapInt32(&outputStreamingRunning, 0, 1) {
|
|
return ErrAudioAlreadyRunning
|
|
}
|
|
|
|
// Initialize CGO audio capture with retry logic
|
|
var initErr error
|
|
for attempt := 0; attempt < 3; attempt++ {
|
|
if initErr = CGOAudioInit(); initErr == nil {
|
|
break
|
|
}
|
|
getOutputStreamingLogger().Warn().Err(initErr).Int("attempt", attempt+1).Msg("Audio initialization failed, retrying")
|
|
time.Sleep(time.Duration(attempt+1) * 100 * time.Millisecond)
|
|
}
|
|
if initErr != nil {
|
|
atomic.StoreInt32(&outputStreamingRunning, 0)
|
|
return fmt.Errorf("failed to initialize audio after 3 attempts: %w", initErr)
|
|
}
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
outputStreamingCancel = cancel
|
|
|
|
// Start audio capture loop
|
|
go func() {
|
|
defer func() {
|
|
CGOAudioClose()
|
|
atomic.StoreInt32(&outputStreamingRunning, 0)
|
|
getOutputStreamingLogger().Info().Msg("Audio output streaming stopped")
|
|
}()
|
|
|
|
getOutputStreamingLogger().Info().Str("socket_path", getOutputSocketPath()).Msg("Audio output streaming started, connected to output server")
|
|
buffer := make([]byte, GetMaxAudioFrameSize())
|
|
|
|
consecutiveErrors := 0
|
|
maxConsecutiveErrors := Config.MaxConsecutiveErrors
|
|
errorBackoffDelay := Config.RetryDelay
|
|
maxErrorBackoff := Config.MaxRetryDelay
|
|
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
default:
|
|
// Capture audio frame with enhanced error handling and initialization checking
|
|
n, err := CGOAudioReadEncode(buffer)
|
|
if err != nil {
|
|
consecutiveErrors++
|
|
getOutputStreamingLogger().Warn().
|
|
Err(err).
|
|
Int("consecutive_errors", consecutiveErrors).
|
|
Msg("Failed to read/encode audio")
|
|
|
|
// Check if this is an initialization error (C error code -1)
|
|
if strings.Contains(err.Error(), "C error code -1") {
|
|
getOutputStreamingLogger().Error().Msg("Audio system not initialized properly, forcing reinitialization")
|
|
// Force immediate reinitialization for init errors
|
|
consecutiveErrors = maxConsecutiveErrors
|
|
}
|
|
|
|
// Implement progressive backoff for consecutive errors
|
|
if consecutiveErrors >= maxConsecutiveErrors {
|
|
getOutputStreamingLogger().Error().
|
|
Int("consecutive_errors", consecutiveErrors).
|
|
Msg("Too many consecutive audio errors, attempting recovery")
|
|
|
|
// Try to reinitialize audio system
|
|
CGOAudioClose()
|
|
time.Sleep(errorBackoffDelay)
|
|
if initErr := CGOAudioInit(); initErr != nil {
|
|
getOutputStreamingLogger().Error().
|
|
Err(initErr).
|
|
Msg("Failed to reinitialize audio system")
|
|
// Exponential backoff for reinitialization failures
|
|
errorBackoffDelay = time.Duration(float64(errorBackoffDelay) * Config.BackoffMultiplier)
|
|
if errorBackoffDelay > maxErrorBackoff {
|
|
errorBackoffDelay = maxErrorBackoff
|
|
}
|
|
} else {
|
|
getOutputStreamingLogger().Info().Msg("Audio system reinitialized successfully")
|
|
consecutiveErrors = 0
|
|
errorBackoffDelay = Config.RetryDelay // Reset backoff
|
|
}
|
|
} else {
|
|
// Brief delay for transient errors
|
|
time.Sleep(Config.ShortSleepDuration)
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Success - reset error counters
|
|
if consecutiveErrors > 0 {
|
|
consecutiveErrors = 0
|
|
errorBackoffDelay = Config.RetryDelay
|
|
}
|
|
|
|
if n > 0 {
|
|
// Get frame buffer from pool to reduce allocations
|
|
frame := GetAudioFrameBuffer()
|
|
frame = frame[:n] // Resize to actual frame size
|
|
copy(frame, buffer[:n])
|
|
|
|
// Validate frame before sending
|
|
if err := ValidateAudioFrame(frame); err != nil {
|
|
getOutputStreamingLogger().Warn().Err(err).Msg("Frame validation failed, dropping frame")
|
|
PutAudioFrameBuffer(frame)
|
|
continue
|
|
}
|
|
|
|
send(frame)
|
|
// Return buffer to pool after sending
|
|
PutAudioFrameBuffer(frame)
|
|
RecordFrameReceived(n)
|
|
}
|
|
// Small delay to prevent busy waiting
|
|
time.Sleep(Config.ShortSleepDuration)
|
|
}
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
// StopAudioOutputStreaming stops audio output streaming
|
|
func StopAudioOutputStreaming() {
|
|
if atomic.LoadInt32(&outputStreamingRunning) == 0 {
|
|
return
|
|
}
|
|
|
|
if outputStreamingCancel != nil {
|
|
outputStreamingCancel()
|
|
outputStreamingCancel = nil
|
|
}
|
|
|
|
// Wait for streaming to stop
|
|
for atomic.LoadInt32(&outputStreamingRunning) == 1 {
|
|
time.Sleep(Config.ShortSleepDuration)
|
|
}
|
|
}
|