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
278 lines
7.1 KiB
Go
278 lines
7.1 KiB
Go
//go:build cgo
|
|
// +build cgo
|
|
|
|
package audio
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
// Component name constants for logging
|
|
const (
|
|
AudioOutputSupervisorComponent = "audio-output-supervisor"
|
|
)
|
|
|
|
// Restart configuration is now retrieved from centralized config
|
|
func getMaxRestartAttempts() int {
|
|
return Config.MaxRestartAttempts
|
|
}
|
|
|
|
func getRestartWindow() time.Duration {
|
|
return Config.RestartWindow
|
|
}
|
|
|
|
func getRestartDelay() time.Duration {
|
|
return Config.RestartDelay
|
|
}
|
|
|
|
func getMaxRestartDelay() time.Duration {
|
|
return Config.MaxRestartDelay
|
|
}
|
|
|
|
// AudioOutputSupervisor manages the audio output server subprocess lifecycle
|
|
type AudioOutputSupervisor struct {
|
|
*BaseSupervisor
|
|
|
|
// Restart management
|
|
restartAttempts []time.Time
|
|
|
|
// Environment variables for OPUS configuration
|
|
opusEnv []string
|
|
|
|
// Callbacks
|
|
onProcessStart func(pid int)
|
|
onProcessExit func(pid int, exitCode int, crashed bool)
|
|
onRestart func(attempt int, delay time.Duration)
|
|
}
|
|
|
|
// NewAudioOutputSupervisor creates a new audio output server supervisor
|
|
func NewAudioOutputSupervisor() *AudioOutputSupervisor {
|
|
return &AudioOutputSupervisor{
|
|
BaseSupervisor: NewBaseSupervisor("audio-output-supervisor"),
|
|
restartAttempts: make([]time.Time, 0),
|
|
}
|
|
}
|
|
|
|
// SetCallbacks sets optional callbacks for process lifecycle events
|
|
func (s *AudioOutputSupervisor) SetCallbacks(
|
|
onStart func(pid int),
|
|
onExit func(pid int, exitCode int, crashed bool),
|
|
onRestart func(attempt int, delay time.Duration),
|
|
) {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
s.onProcessStart = onStart
|
|
|
|
// Wrap the exit callback to include restart tracking
|
|
if onExit != nil {
|
|
s.onProcessExit = func(pid int, exitCode int, crashed bool) {
|
|
if crashed {
|
|
s.recordRestartAttempt()
|
|
}
|
|
onExit(pid, exitCode, crashed)
|
|
}
|
|
} else {
|
|
s.onProcessExit = func(pid int, exitCode int, crashed bool) {
|
|
if crashed {
|
|
s.recordRestartAttempt()
|
|
}
|
|
}
|
|
}
|
|
|
|
s.onRestart = onRestart
|
|
}
|
|
|
|
// SetOpusConfig sets OPUS configuration parameters as environment variables
|
|
// for the audio output subprocess
|
|
func (s *AudioOutputSupervisor) SetOpusConfig(bitrate, complexity, vbr, signalType, bandwidth, dtx int) {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
// Store OPUS parameters as environment variables
|
|
s.opusEnv = []string{
|
|
"JETKVM_OPUS_BITRATE=" + strconv.Itoa(bitrate),
|
|
"JETKVM_OPUS_COMPLEXITY=" + strconv.Itoa(complexity),
|
|
"JETKVM_OPUS_VBR=" + strconv.Itoa(vbr),
|
|
"JETKVM_OPUS_SIGNAL_TYPE=" + strconv.Itoa(signalType),
|
|
"JETKVM_OPUS_BANDWIDTH=" + strconv.Itoa(bandwidth),
|
|
"JETKVM_OPUS_DTX=" + strconv.Itoa(dtx),
|
|
}
|
|
}
|
|
|
|
// Start begins supervising the audio output server process
|
|
func (s *AudioOutputSupervisor) Start() error {
|
|
if !atomic.CompareAndSwapInt32(&s.running, 0, 1) {
|
|
return fmt.Errorf("audio output supervisor is already running")
|
|
}
|
|
|
|
s.logSupervisorStart()
|
|
s.createContext()
|
|
|
|
// Recreate channels in case they were closed by a previous Stop() call
|
|
s.initializeChannels()
|
|
|
|
// Reset restart tracking on start
|
|
s.mutex.Lock()
|
|
s.restartAttempts = s.restartAttempts[:0]
|
|
s.mutex.Unlock()
|
|
|
|
// Start the supervision loop
|
|
go s.supervisionLoop()
|
|
|
|
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component started successfully")
|
|
return nil
|
|
}
|
|
|
|
// Stop gracefully stops the audio server and supervisor
|
|
func (s *AudioOutputSupervisor) Stop() {
|
|
if !atomic.CompareAndSwapInt32(&s.running, 1, 0) {
|
|
return // Already stopped
|
|
}
|
|
|
|
s.logSupervisorStop()
|
|
|
|
// Signal stop and wait for cleanup
|
|
s.closeStopChan()
|
|
s.cancelContext()
|
|
|
|
// Wait for process to exit
|
|
select {
|
|
case <-s.processDone:
|
|
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped gracefully")
|
|
case <-time.After(Config.OutputSupervisorTimeout):
|
|
s.logger.Warn().Str("component", AudioOutputSupervisorComponent).Msg("component did not stop gracefully, forcing termination")
|
|
s.forceKillProcess("audio output server")
|
|
}
|
|
|
|
s.logger.Info().Str("component", AudioOutputSupervisorComponent).Msg("component stopped")
|
|
}
|
|
|
|
// supervisionLoop is the main loop that manages the audio output process
|
|
func (s *AudioOutputSupervisor) supervisionLoop() {
|
|
// Configure supervision parameters
|
|
config := SupervisionConfig{
|
|
ProcessType: "audio output server",
|
|
Timeout: Config.OutputSupervisorTimeout,
|
|
EnableRestart: true,
|
|
MaxRestartAttempts: getMaxRestartAttempts(),
|
|
RestartWindow: getRestartWindow(),
|
|
RestartDelay: getRestartDelay(),
|
|
MaxRestartDelay: getMaxRestartDelay(),
|
|
}
|
|
|
|
// Configure callbacks
|
|
callbacks := ProcessCallbacks{
|
|
OnProcessStart: s.onProcessStart,
|
|
OnProcessExit: s.onProcessExit,
|
|
OnRestart: s.onRestart,
|
|
}
|
|
|
|
// Use the base supervision loop template
|
|
s.SupervisionLoop(
|
|
config,
|
|
callbacks,
|
|
s.startProcess,
|
|
s.shouldRestart,
|
|
s.calculateRestartDelay,
|
|
)
|
|
}
|
|
|
|
// startProcess starts the audio server process
|
|
func (s *AudioOutputSupervisor) startProcess() error {
|
|
execPath, err := os.Executable()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get executable path: %w", err)
|
|
}
|
|
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
// Build command arguments (only subprocess flag)
|
|
args := []string{"--audio-output-server"}
|
|
|
|
// Create new command
|
|
s.cmd = exec.CommandContext(s.ctx, execPath, args...)
|
|
s.cmd.Stdout = os.Stdout
|
|
s.cmd.Stderr = os.Stderr
|
|
|
|
// Set environment variables for OPUS configuration
|
|
s.cmd.Env = append(os.Environ(), s.opusEnv...)
|
|
|
|
// Start the process
|
|
if err := s.cmd.Start(); err != nil {
|
|
return fmt.Errorf("failed to start audio output server process: %w", err)
|
|
}
|
|
|
|
s.processPID = s.cmd.Process.Pid
|
|
s.logger.Info().Int("pid", s.processPID).Strs("args", args).Strs("opus_env", s.opusEnv).Msg("audio server process started")
|
|
|
|
// Add process to monitoring
|
|
s.processMonitor.AddProcess(s.processPID, "audio-output-server")
|
|
|
|
if s.onProcessStart != nil {
|
|
s.onProcessStart(s.processPID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// shouldRestart determines if the process should be restarted
|
|
func (s *AudioOutputSupervisor) shouldRestart() bool {
|
|
if atomic.LoadInt32(&s.running) == 0 {
|
|
return false // Supervisor is stopping
|
|
}
|
|
|
|
s.mutex.RLock()
|
|
defer s.mutex.RUnlock()
|
|
|
|
// Clean up old restart attempts outside the window
|
|
now := time.Now()
|
|
var recentAttempts []time.Time
|
|
for _, attempt := range s.restartAttempts {
|
|
if now.Sub(attempt) < getRestartWindow() {
|
|
recentAttempts = append(recentAttempts, attempt)
|
|
}
|
|
}
|
|
s.restartAttempts = recentAttempts
|
|
|
|
return len(s.restartAttempts) < getMaxRestartAttempts()
|
|
}
|
|
|
|
// recordRestartAttempt records a restart attempt
|
|
func (s *AudioOutputSupervisor) recordRestartAttempt() {
|
|
s.mutex.Lock()
|
|
defer s.mutex.Unlock()
|
|
|
|
s.restartAttempts = append(s.restartAttempts, time.Now())
|
|
}
|
|
|
|
// calculateRestartDelay calculates the delay before next restart attempt
|
|
func (s *AudioOutputSupervisor) calculateRestartDelay() time.Duration {
|
|
s.mutex.RLock()
|
|
defer s.mutex.RUnlock()
|
|
|
|
// Exponential backoff based on recent restart attempts
|
|
attempts := len(s.restartAttempts)
|
|
if attempts == 0 {
|
|
return getRestartDelay()
|
|
}
|
|
|
|
// Calculate exponential backoff: 2^attempts * base delay
|
|
delay := getRestartDelay()
|
|
for i := 0; i < attempts && delay < getMaxRestartDelay(); i++ {
|
|
delay *= 2
|
|
}
|
|
|
|
if delay > getMaxRestartDelay() {
|
|
delay = getMaxRestartDelay()
|
|
}
|
|
|
|
return delay
|
|
}
|