mirror of
https://github.com/jetkvm/kvm.git
synced 2025-09-16 08:38:14 +00:00
Replace hardcoded values with centralized config constants for better maintainability and flexibility. This includes sleep durations, buffer sizes, thresholds, and various audio processing parameters. The changes affect multiple components including buffer pools, latency monitoring, IPC, and audio processing. This refactoring makes it easier to adjust parameters without modifying individual files. Key changes: - Replace hardcoded sleep durations with config values - Centralize buffer sizes and pool configurations - Move thresholds and limits to config - Update audio quality presets to use config values
272 lines
6.5 KiB
Go
272 lines
6.5 KiB
Go
package audio
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"sync"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/jetkvm/kvm/internal/logging"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
// AudioInputSupervisor manages the audio input server subprocess
|
|
type AudioInputSupervisor struct {
|
|
cmd *exec.Cmd
|
|
cancel context.CancelFunc
|
|
mtx sync.Mutex
|
|
running bool
|
|
logger zerolog.Logger
|
|
client *AudioInputClient
|
|
processMonitor *ProcessMonitor
|
|
}
|
|
|
|
// NewAudioInputSupervisor creates a new audio input supervisor
|
|
func NewAudioInputSupervisor() *AudioInputSupervisor {
|
|
return &AudioInputSupervisor{
|
|
logger: logging.GetDefaultLogger().With().Str("component", "audio-input-supervisor").Logger(),
|
|
client: NewAudioInputClient(),
|
|
processMonitor: GetProcessMonitor(),
|
|
}
|
|
}
|
|
|
|
// Start starts the audio input server subprocess
|
|
func (ais *AudioInputSupervisor) Start() error {
|
|
ais.mtx.Lock()
|
|
defer ais.mtx.Unlock()
|
|
|
|
if ais.running {
|
|
return fmt.Errorf("audio input supervisor already running")
|
|
}
|
|
|
|
// Create context for subprocess management
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
ais.cancel = cancel
|
|
|
|
// Get current executable path
|
|
execPath, err := os.Executable()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get executable path: %w", err)
|
|
}
|
|
|
|
// Create command for audio input server subprocess
|
|
cmd := exec.CommandContext(ctx, execPath, "--audio-input-server")
|
|
cmd.Env = append(os.Environ(),
|
|
"JETKVM_AUDIO_INPUT_IPC=true", // Enable IPC mode
|
|
)
|
|
|
|
// Set process group to allow clean termination
|
|
cmd.SysProcAttr = &syscall.SysProcAttr{
|
|
Setpgid: true,
|
|
}
|
|
|
|
ais.cmd = cmd
|
|
ais.running = true
|
|
|
|
// Start the subprocess
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
ais.running = false
|
|
cancel()
|
|
return fmt.Errorf("failed to start audio input server: %w", err)
|
|
}
|
|
|
|
ais.logger.Info().Int("pid", cmd.Process.Pid).Msg("Audio input server subprocess started")
|
|
|
|
// Add process to monitoring
|
|
ais.processMonitor.AddProcess(cmd.Process.Pid, "audio-input-server")
|
|
|
|
// Monitor the subprocess in a goroutine
|
|
go ais.monitorSubprocess()
|
|
|
|
// Connect client to the server
|
|
go ais.connectClient()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the audio input server subprocess
|
|
func (ais *AudioInputSupervisor) Stop() {
|
|
ais.mtx.Lock()
|
|
defer ais.mtx.Unlock()
|
|
|
|
if !ais.running {
|
|
return
|
|
}
|
|
|
|
ais.running = false
|
|
|
|
// Disconnect client first
|
|
if ais.client != nil {
|
|
ais.client.Disconnect()
|
|
}
|
|
|
|
// Cancel context to signal subprocess to stop
|
|
if ais.cancel != nil {
|
|
ais.cancel()
|
|
}
|
|
|
|
// Try graceful termination first
|
|
if ais.cmd != nil && ais.cmd.Process != nil {
|
|
ais.logger.Info().Int("pid", ais.cmd.Process.Pid).Msg("Stopping audio input server subprocess")
|
|
|
|
// Send SIGTERM
|
|
err := ais.cmd.Process.Signal(syscall.SIGTERM)
|
|
if err != nil {
|
|
ais.logger.Warn().Err(err).Msg("Failed to send SIGTERM to audio input server")
|
|
}
|
|
|
|
// Wait for graceful shutdown with timeout
|
|
done := make(chan error, 1)
|
|
go func() {
|
|
done <- ais.cmd.Wait()
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
ais.logger.Info().Msg("Audio input server subprocess stopped gracefully")
|
|
case <-time.After(GetConfig().InputSupervisorTimeout):
|
|
// Force kill if graceful shutdown failed
|
|
ais.logger.Warn().Msg("Audio input server subprocess did not stop gracefully, force killing")
|
|
err := ais.cmd.Process.Kill()
|
|
if err != nil {
|
|
ais.logger.Error().Err(err).Msg("Failed to kill audio input server subprocess")
|
|
}
|
|
}
|
|
}
|
|
|
|
ais.cmd = nil
|
|
ais.cancel = nil
|
|
}
|
|
|
|
// IsRunning returns whether the supervisor is running
|
|
func (ais *AudioInputSupervisor) IsRunning() bool {
|
|
ais.mtx.Lock()
|
|
defer ais.mtx.Unlock()
|
|
return ais.running
|
|
}
|
|
|
|
// IsConnected returns whether the client is connected to the audio input server
|
|
func (ais *AudioInputSupervisor) IsConnected() bool {
|
|
if !ais.IsRunning() {
|
|
return false
|
|
}
|
|
return ais.client.IsConnected()
|
|
}
|
|
|
|
// GetClient returns the IPC client for sending audio frames
|
|
func (ais *AudioInputSupervisor) GetClient() *AudioInputClient {
|
|
return ais.client
|
|
}
|
|
|
|
// GetProcessMetrics returns current process metrics if the process is running
|
|
func (ais *AudioInputSupervisor) GetProcessMetrics() *ProcessMetrics {
|
|
ais.mtx.Lock()
|
|
defer ais.mtx.Unlock()
|
|
|
|
if ais.cmd == nil || ais.cmd.Process == nil {
|
|
return nil
|
|
}
|
|
|
|
pid := ais.cmd.Process.Pid
|
|
metrics := ais.processMonitor.GetCurrentMetrics()
|
|
for _, metric := range metrics {
|
|
if metric.PID == pid {
|
|
return &metric
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// monitorSubprocess monitors the subprocess and handles unexpected exits
|
|
func (ais *AudioInputSupervisor) monitorSubprocess() {
|
|
if ais.cmd == nil {
|
|
return
|
|
}
|
|
|
|
pid := ais.cmd.Process.Pid
|
|
err := ais.cmd.Wait()
|
|
|
|
// Remove process from monitoring
|
|
ais.processMonitor.RemoveProcess(pid)
|
|
|
|
ais.mtx.Lock()
|
|
defer ais.mtx.Unlock()
|
|
|
|
if ais.running {
|
|
// Unexpected exit
|
|
if err != nil {
|
|
ais.logger.Error().Err(err).Msg("Audio input server subprocess exited unexpectedly")
|
|
} else {
|
|
ais.logger.Warn().Msg("Audio input server subprocess exited unexpectedly")
|
|
}
|
|
|
|
// Disconnect client
|
|
if ais.client != nil {
|
|
ais.client.Disconnect()
|
|
}
|
|
|
|
// Mark as not running
|
|
ais.running = false
|
|
ais.cmd = nil
|
|
|
|
ais.logger.Info().Msg("Audio input server subprocess monitoring stopped")
|
|
}
|
|
}
|
|
|
|
// connectClient attempts to connect the client to the server
|
|
func (ais *AudioInputSupervisor) connectClient() {
|
|
// Wait briefly for the server to start (reduced from 500ms)
|
|
time.Sleep(GetConfig().DefaultSleepDuration)
|
|
|
|
err := ais.client.Connect()
|
|
if err != nil {
|
|
ais.logger.Error().Err(err).Msg("Failed to connect to audio input server")
|
|
return
|
|
}
|
|
|
|
ais.logger.Info().Msg("Connected to audio input server")
|
|
}
|
|
|
|
// SendFrame sends an audio frame to the subprocess (convenience method)
|
|
func (ais *AudioInputSupervisor) SendFrame(frame []byte) error {
|
|
if ais.client == nil {
|
|
return fmt.Errorf("client not initialized")
|
|
}
|
|
|
|
if !ais.client.IsConnected() {
|
|
return fmt.Errorf("client not connected")
|
|
}
|
|
|
|
return ais.client.SendFrame(frame)
|
|
}
|
|
|
|
// SendFrameZeroCopy sends a zero-copy frame to the subprocess
|
|
func (ais *AudioInputSupervisor) SendFrameZeroCopy(frame *ZeroCopyAudioFrame) error {
|
|
if ais.client == nil {
|
|
return fmt.Errorf("client not initialized")
|
|
}
|
|
|
|
if !ais.client.IsConnected() {
|
|
return fmt.Errorf("client not connected")
|
|
}
|
|
|
|
return ais.client.SendFrameZeroCopy(frame)
|
|
}
|
|
|
|
// SendConfig sends a configuration update to the subprocess (convenience method)
|
|
func (ais *AudioInputSupervisor) SendConfig(config InputIPCConfig) error {
|
|
if ais.client == nil {
|
|
return fmt.Errorf("client not initialized")
|
|
}
|
|
|
|
if !ais.client.IsConnected() {
|
|
return fmt.Errorf("client not connected")
|
|
}
|
|
|
|
return ais.client.SendConfig(config)
|
|
}
|